Merge branch feature/workflowgroup of zentao/zentaopms (#12764)

This commit is contained in:
刘刚
2025-12-10 13:41:18 +08:00
committed by Gitfox
18 changed files with 3734 additions and 1123 deletions
+1080 -1080
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -95,3 +95,40 @@ CREATE TABLE IF NOT EXISTS `zt_ai_promptfield` (
`required` tinyint unsigned NOT NULL DEFAULT 1 COMMENT '是否必填',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
UPDATE `zt_project` SET `budget` = 0 WHERE `budget` = '';
UPDATE `zt_project` SET `budget` = REPLACE(`budget`, '万', '0000');
ALTER TABLE `zt_actionproduct` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_burn` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_charterproduct` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_demandreview` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_demandspec` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_deployproduct` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_designspec` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_duckdbqueue` ADD COLUMN `id` bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_feedbackview` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_grouppriv` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_oauth` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_pivotdrill` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_pivotspec` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_planstory` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_projectadmin` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_projectcase` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_projectproduct` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_projectspec` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_projectstory` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_repobranch` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_riskissue` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_roadmapstory` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_searchdict` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_storyestimate` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_storygrade` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_storyreview` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_storyspec` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_storystage` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_suitecase` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_taskspec` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_trainrecords` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_usergroup` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
ALTER TABLE `zt_workflowlinkdata` ADD COLUMN `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY;
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -1536,7 +1536,7 @@ CREATE TABLE IF NOT EXISTS `zt_productplan` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`product` int unsigned NOT NULL DEFAULT 0,
`branch` varchar(255) NOT NULL DEFAULT '0',
`parent` int unsigned NOT NULL DEFAULT 0,
`parent` int NOT NULL DEFAULT 0,
`title` varchar(90) NOT NULL DEFAULT '',
`status` varchar(10) NOT NULL DEFAULT 'wait',
`desc` mediumtext DEFAULT NULL,
@@ -15414,6 +15414,7 @@ CREATE TABLE IF NOT EXISTS `zt_ai_message` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- DROP TABLE IF EXISTS `zt_ai_miniprogramfield`;
CREATE TABLE IF NOT EXISTS `zt_ai_miniprogramfield` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`appID` int unsigned NOT NULL DEFAULT 0,
@@ -15492,6 +15493,7 @@ INSERT INTO `zt_ai_miniprogramfield` (`appID`, `name`, `type`, `placeholder`, `o
(12, '计划内容', 'checkbox', NULL, '每日学习任务,适用的学习资源,学习方法,进度跟踪方式,激励机制', '0'),
(12, '补充信息', 'textarea', '请输入更多补充信息', NULL, '0');
-- DROP TABLE IF EXISTS `zt_ai_miniprogramstar`;
CREATE TABLE IF NOT EXISTS `zt_ai_miniprogramstar` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`appID` int unsigned NOT NULL DEFAULT 0,
+534
View File
@@ -0,0 +1,534 @@
<?php
/*
* 这个脚本用来比较两个数据库表的结构差异,并生成相应的SQL语句来同步它们。
* 从两个文件中读取表结构,比较差异,并输出同步SQL语句。
* 比较的表结构文件格式为MySQL的CREATE TABLE语句。
* 比较差异时遵循如下规则:
* 1. 如果发现源表有而目标表没有的列,则不作处理。
* 2. 如果发现目标表有而源表没有的列,则生成ADD COLUMN语句。
* 3. 如果发现列定义不同,则生成ALTER COLUMN语句。
* - 不考虑列顺序的不同。
* - 如果数据类型不同、长度不同、是否允许NULL不同或默认值不同,则视为不同。
* - 如果源表的字段取值范围更大,则视为数据类型和长度相同,仅比较是否允许NULL和默认值。生成ALTER COLUMN语句时,仅修改允许NULL和默认值。
* - 如果源表字段是 ENUM,目标表不是 ENUM:
* - 若目标是 VARCHAR 且长度 >= 最长枚举值 → 视为兼容,仅比较 NULL/DEFAULT;
* - 否则 → 视为不兼容,生成 MODIFY 为 VARCHAR(N) 的语句;
* - 忽略索引和约束的差异。
* 使用方法:php diffTable.php source_db_file target_db_file [--debug]
* 其中 source_db_file 和 target_db_file 是包含 CREATE TABLE 语句的文本文件。--debug 可选参数,用于输出调试信息。
* 例如:
* php diffTable.php db/standard/zentao21.7.7.sql db/standard/zentao21.7.8.sql --debug
*/
$debug = in_array('--debug', $argv);
function logDebug($msg)
{
global $debug;
if($debug)
{
echo "[DEBUG] " . $msg . "\n";
}
}
function parseCreateTable($createTableSql)
{
$tables = [];
$sqls = explode(";", $createTableSql);
foreach($sqls as $sql)
{
$sql = trim($sql);
if($sql === '') continue;
if(stripos($sql, 'CREATE TABLE') === 0)
{
if(preg_match('/CREATE TABLE `([^`]+)` \((.*)\) ENGINE=InnoDB\s*/s', $sql, $matches))
{
$tableName = $matches[1];
$columnsDef = $matches[2];
logDebug("Found table: $tableName");
$columns = parseColumns($columnsDef);
$tables[$tableName] = $columns;
}
else
{
logDebug("Failed to parse CREATE TABLE: " . substr($sql, 0, 100) . "...");
}
}
}
logDebug("Parsed " . count($tables) . " tables from SQL");
return $tables;
}
function parseColumns($columnsDef)
{
$columns = [];
$lines = explode("\n", trim($columnsDef));
foreach($lines as $line)
{
$line = trim($line, " ,");
if(preg_match('/^`([^`]+)` (.+)$/', $line, $matches))
{
$columnName = $matches[1];
$columnDef = $matches[2];
$columns[$columnName] = parseColumnDefinition($columnDef);
}
}
return $columns;
}
function parseColumnDefinition($columnDef)
{
$definition = [];
// 解析 ENUM
if(preg_match("/^ENUM\s*\((.+)\)/i", $columnDef, $enumMatches))
{
$definition['type'] = 'ENUM';
$definition['length'] = null;
preg_match_all("/'([^']*)'|\"([^\"]*)\"/", $enumMatches[1], $values);
$definition['enumValues'] = array_unique(array_filter(array_merge($values[1], $values[2]), function($v)
{
return $v !== '';
}));
$definition['unsigned'] = false;
logDebug("Parsed ENUM: " . json_encode($definition['enumValues']));
}
elseif(preg_match('/^([a-zA-Z]+)(\(([^)]+)\))?/', $columnDef, $matches))
{
$definition['type'] = strtoupper($matches[1]);
$definition['length'] = isset($matches[3]) ? $matches[3] : null;
$definition['enumValues'] = null;
$definition['unsigned'] = stripos($columnDef, 'unsigned') !== false;
}
else
{
$definition['type'] = 'UNKNOWN';
$definition['length'] = null;
$definition['enumValues'] = null;
$definition['unsigned'] = false;
logDebug("Unknown column definition: $columnDef");
}
$definition['nullable'] = stripos($columnDef, 'NOT NULL') === false;
if(preg_match('/DEFAULT\s+(NULL|\'[^\']*\'|"[^"]*"|[\w\.\-]+)/i', $columnDef, $matches))
{
$rawDefault = $matches[1];
if(strtoupper(trim($rawDefault)) === 'NULL')
{
$definition['default'] = null;
}
else
{
$definition['default'] = trim($rawDefault, " '\"");
}
}
else
{
$definition['default'] = null;
}
return $definition;
}
// 辅助:获取 ENUM 最长值长度
function getLongestEnumLength($enumValues)
{
if(empty($enumValues)) return 1;
return max(array_map('strlen', $enumValues));
}
function isIntegerType($type)
{
$type = strtoupper($type);
return in_array($type, ['TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT']);
}
function isFloatType($type)
{
$type = strtoupper($type);
return in_array($type, ['FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC']);
}
function isNumericType($type)
{
$type = strtoupper($type);
return in_array($type, [
'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER',
'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC'
]);
}
// 判断是否兼容(源 >= 目标)
function isCompatibleColumn($sourceDef, $targetDef)
{
$sourceType = strtoupper($sourceDef['type']);
$targetType = strtoupper($targetDef['type']);
// 情况1:源是 ENUM
if($sourceType === 'ENUM')
{
if($targetType === 'ENUM')
{
// 目标也是 ENUM:检查是否包含所有源值
if(!$targetDef['enumValues']) return false;
$missing = array_diff($sourceDef['enumValues'], $targetDef['enumValues']);
if(!empty($missing))
{
logDebug("ENUM missing values in target: " . json_encode($missing));
return false;
}
return true;
}
else
{
// 目标不是 ENUM
if($targetType === 'VARCHAR')
{
$targetLen = (int)($targetDef['length'] ?? 0);
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
$compatible = $targetLen >= $requiredLen;
logDebug("ENUM -> VARCHAR: required=$requiredLen, target=$targetLen, compatible=$compatible");
return $compatible;
}
logDebug("ENUM -> non-VARCHAR ($targetType): not compatible");
// 其他类型(如 TINYINT)视为不兼容
return false;
}
}
// 情况2:源不是 ENUM,走原兼容逻辑
$numericHierarchy = [
'TINYINT' => 1, 'SMALLINT' => 2, 'MEDIUMINT' => 3,
'INT' => 4, 'INTEGER' => 4, 'BIGINT' => 5,
'FLOAT' => 6, 'DOUBLE' => 7, 'DECIMAL' => 8
];
$stringHierarchy = [
'CHAR' => 1, 'VARCHAR' => 2, 'TINYTEXT' => 3,
'TEXT' => 4, 'MEDIUMTEXT' => 5, 'LONGTEXT' => 6
];
if(isset($numericHierarchy[$sourceType]) && isset($numericHierarchy[$targetType]))
{
if($numericHierarchy[$sourceType] > $numericHierarchy[$targetType]) return true;
if($numericHierarchy[$sourceType] === $numericHierarchy[$targetType])
{
if(isIntegerType($sourceType))
{
return $sourceDef['unsigned'] === $targetDef['unsigned'];
}
if(isFloatType($sourceType))
{
$sourceLen = explode(',', $sourceDef['length'] ?? '');
$sourceInteger = (int)($sourceLen[0] ?? 0);
$sourceDecimal = (int)($sourceLen[1] ?? 0);
$targetLen = explode(',', $targetDef['length'] ?? '');
$targetInteger = (int)($targetLen[0] ?? 0);
$targetDecimal = (int)($targetLen[1] ?? 0);
return $sourceInteger >= $targetInteger && $sourceDecimal >= $targetDecimal;
}
}
}
if(in_array($sourceType, ['CHAR','VARCHAR']) && in_array($targetType, ['CHAR','VARCHAR']))
{
return (int)($sourceDef['length'] ?? 0) > (int)($targetDef['length'] ?? 0);
}
if(isset($stringHierarchy[$sourceType]) && isset($stringHierarchy[$targetType]))
{
return $stringHierarchy[$sourceType] >= $stringHierarchy[$targetType];
}
return false;
}
function compareColumnDefinitions($sourceDef, $targetDef)
{
$sourceType = strtoupper($sourceDef['type']);
$targetType = strtoupper($targetDef['type']);
// 特殊处理 ENUM
if($sourceType === 'ENUM')
{
if($sourceType !== $targetType)
{
logDebug("ENUM type mismatch: source=$sourceType, target=$targetType");
return false; // 类型不同,直接不等
}
// 同为 ENUM,检查兼容性
if(isCompatibleColumn($sourceDef, $targetDef))
{
$eq = ($sourceDef['nullable'] === $targetDef['nullable'] && $sourceDef['default'] === $targetDef['default']);
logDebug("ENUM compatible, NULL/DEFAULT equal: " . ($eq ? 'YES' : 'NO'));
return $eq;
}
return false; // 不兼容
}
// 非 ENUM:使用通用兼容逻辑
if(isCompatibleColumn($sourceDef, $targetDef))
{
$eq = ($sourceDef['nullable'] === $targetDef['nullable'] && $sourceDef['default'] === $targetDef['default']);
logDebug("Compatible type, NULL/DEFAULT equal: " . ($eq ? 'YES' : 'NO'));
return $eq;
}
$strictEq = ($sourceDef['type'] === $targetDef['type'] &&
$sourceDef['unsigned'] === $targetDef['unsigned'] &&
$sourceDef['length'] === $targetDef['length'] &&
$sourceDef['nullable'] === $targetDef['nullable'] &&
$sourceDef['default'] === $targetDef['default']);
logDebug("Strict compare: " . ($strictEq ? 'EQUAL' : 'DIFFERENT'));
return $strictEq;
}
function buildColumnDefinition($columnDef)
{
if($columnDef['type'] === 'ENUM' && !empty($columnDef['enumValues']))
{
$quoted = array_map(function($v)
{
return "'" . str_replace("'", "''", $v) . "'";
}, $columnDef['enumValues']);
$definition = "enum(" . implode(", ", $quoted) . ")"; // 小写 enum
}
else
{
$definition = strtolower($columnDef['type']); // 小写类型
if($columnDef['length'])
{
$definition .= "({$columnDef['length']})";
}
if(isNumericType($columnDef['type']) && !empty($columnDef['unsigned']))
{
$definition .= " unsigned";
}
}
$definition .= $columnDef['nullable'] ? " NULL" : " NOT NULL";
if($columnDef['default'] === null)
{
if($columnDef['nullable'])
{
$definition .= " DEFAULT NULL";
}
}
else
{
if(isNumericType($columnDef['type']))
{
$definition .= " DEFAULT " . $columnDef['default'];
}
else
{
$escaped = str_replace("'", "''", $columnDef['default']);
$definition .= " DEFAULT '{$escaped}'";
}
}
return $definition;
}
function buildColumnDefinitionForModify($sourceDef, $targetDef)
{
// 如果源是 ENUM 且目标不兼容,生成 VARCHAR(N)
if(strtoupper($sourceDef['type']) === 'ENUM')
{
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
if($requiredLen < $targetDef['length'])
{
$requiredLen = $targetDef['length'];
}
$definition = "varchar({$requiredLen})";
}
else
{
// 否则保留源类型和长度
$definition = strtolower($sourceDef['type']);
if($sourceDef['length'])
{
$definition .= "({$sourceDef['length']})";
}
if(isNumericType($sourceDef['type']) && !empty($sourceDef['unsigned']))
{
$definition .= " unsigned";
}
}
$definition .= $targetDef['nullable'] ? " NULL" : " NOT NULL";
if($targetDef['default'] === null)
{
if($targetDef['nullable'])
{
$definition .= " DEFAULT NULL";
}
}
else
{
if(isNumericType($targetDef['type']))
{
$definition .= " DEFAULT " . $targetDef['default'];
}
else
{
$escaped = str_replace("'", "''", $targetDef['default']);
$definition .= " DEFAULT '{$escaped}'";
}
}
return $definition;
}
function compareTables($sourceTables, $targetTables)
{
$sqlStatements = [];
logDebug("Comparing " . count($targetTables) . " target tables");
foreach($targetTables as $tableName => $targetColumns)
{
logDebug("Processing table: $tableName");
if(!isset($sourceTables[$tableName]))
{
logDebug("Source table $tableName not found, skipping");
continue;
}
$alterStatements = [];
$enumStatements = [];
$sourceColumns = $sourceTables[$tableName];
foreach($targetColumns as $columnName => $targetDef)
{
if(!isset($sourceColumns[$columnName]))
{
logDebug("Column $columnName exists in target but not source → ADD");
$sqlStatements[] = "ALTER TABLE `$tableName` ADD COLUMN `$columnName` " . buildColumnDefinition($targetDef) . ($columnName === 'id' ? ' AUTO_INCREMENT PRIMARY KEY' : '') . ";";
}
else
{
$enum2int = false;
$sourceDef = $sourceColumns[$columnName];
if(!compareColumnDefinitions($sourceDef, $targetDef))
{
// 判断是否只需修改 NULL/DEFAULT,或需要改类型
if(isCompatibleColumn($sourceDef, $targetDef))
{
// 兼容,只改 NULL/DEFAULT
$colDef = buildColumnDefinitionForModify($sourceDef, $targetDef);
logDebug("Compatible difference → MODIFY (only NULL/DEFAULT): $colDef");
}
else
{
// 不兼容:若源是 ENUM,目标非 ENUM 且不满足长度,转为 VARCHAR
if(strtoupper($sourceDef['type']) === 'ENUM')
{
$requiredLen = getLongestEnumLength($sourceDef['enumValues']);
if($requiredLen < $targetDef['length'])
{
$requiredLen = $targetDef['length'];
}
// 构造新的 VARCHAR 定义(使用目标的 nullable/default)
$colDef = "varchar({$requiredLen})" . ($targetDef['nullable'] ? " NULL" : " NOT NULL");
if($targetDef['default'] !== null)
{
$escaped = str_replace("'", "''", $targetDef['default']);
$colDef .= " DEFAULT '{$escaped}'";
}
logDebug("ENUM incompatible → MODIFY to VARCHAR($requiredLen): $colDef");
$enum2int = true;
foreach($sourceDef['enumValues'] as $val)
{
if(!is_numeric($val))
{
$enum2int = false;
break;
}
}
}
else
{
// 其他情况:直接使用目标定义
$colDef = buildColumnDefinition($targetDef);
logDebug("Incompatible → MODIFY to target definition: $colDef");
}
}
$alterStatements[] = "MODIFY COLUMN `$columnName` {$colDef}" . ($columnName === 'id' ? ' AUTO_INCREMENT' : '');
if($enum2int)
{
logDebug("Incompatible → MODIFY to INT for ENUM to INT conversion");
$enumStatements[] = "MODIFY COLUMN `$columnName` tinyint unsigned" . ($targetDef['nullable'] ? " NULL" : " NOT NULL") . ($targetDef['default'] !== null ? " DEFAULT " . $targetDef['default'] : '');
}
}
else
{
logDebug("Column $columnName identical");
}
}
}
if($alterStatements)
{
$sqlStatements[] = "ALTER TABLE `$tableName`\n " . implode(",\n ", $alterStatements) . ";";
}
if($enumStatements)
{
$sqlStatements[] = "ALTER TABLE `$tableName`\n " . implode(",\n ", $enumStatements) . ";";
}
}
logDebug("Generated " . count($sqlStatements) . " SQL statements");
return $sqlStatements;
}
if($argc !== 3 && $argc !== 4)
{
echo "[Info] Usage: php diffTable.php source_db_file target_db_file [--debug]\n";
echo "[Info] Example: php diffTable.php db/standard/zentao21.7.7.sql db/standard/zentao21.7.8.sql --debug\n";
echo "[Info] The source_db_file and target_db_file should contain CREATE TABLE statements.\n";
echo "[Info] The --debug option is optional for debug output.\n";
echo "[Info] Note: The CREATE TABLE statements must end with ENGINE=InnoDB;\n";
exit(1);
}
$sourceDbFile = $argv[1];
$targetDbFile = $argv[2];
if(!file_exists($sourceDbFile))
{
echo "[Info] Error: Source file '$sourceDbFile' does not exist.\n";
exit(1);
}
if(!file_exists($targetDbFile))
{
echo "[Info] Error: Target file '$targetDbFile' does not exist.\n";
exit(1);
}
logDebug("Reading source file: $sourceDbFile");
logDebug("Reading target file: $targetDbFile");
$sourceSql = file_get_contents($sourceDbFile);
$targetSql = file_get_contents($targetDbFile);
if(trim($sourceSql) === '')
{
echo "[Info] Warning: Source file is empty.\n";
}
if(trim($targetSql) === '')
{
echo "[Info] Warning: Target file is empty.\n";
}
$sourceTables = parseCreateTable($sourceSql);
$targetTables = parseCreateTable($targetSql);
if(empty($targetTables))
{
echo "[Info] No target tables parsed. Check CREATE TABLE syntax (must end with ENGINE=InnoDB;).\n";
exit(1);
}
$sqlStatements = compareTables($sourceTables, $targetTables);
foreach($sqlStatements as $key => $sql)
{
$sqlStatements[$key] = str_replace(" varchar(1) ", " char(1) ", $sql); // 优化:长度为1的VARCHAR改为CHAR
}
file_put_contents('difftable.sql', implode("\n", $sqlStatements) . "\n");
echo "[Info] Generated SQL statements saved to difftable.sql\n";
+13 -21
View File
@@ -165,29 +165,21 @@ class createBugTester extends tester
* @access public
* @return object
*/
public function directAssign($product = array(), $bugTitle = '', $assignee = '')
public function directAssign($assignee)
{
if(!$bugTitle) return $this->failed('bug直接修改指派失败,没有指定bug');
$assignee = $assignee ?? 'admin';
$form = $this->initForm('bug', 'browse', $product, 'appIframe-qa');
$form->wait(3);
$form->dom->bugTitle->scrollToElement();
$index = array_search($bugTitle, $form->dom->getElementListByXpathKey('bugTitle', true));
if($index === false ) return $this->failed('bug未找到' . $bugTitle);
$form->dom->bugAssigned->scrollToElement();
$form->wait(3);
$form->dom->getElementListByXpathKey('bugAssigned')[$index]->click();
$form->wait(3);
$form = $this->initForm('bug', 'browse', array('productID' => '1'), 'appIframe-qa');
$form->dom->firstAssignTo->scrollToElement();
$form->dom->firstAssignTo->click();
$form->wait(2);
$form->dom->assignedTo->picker($assignee);
$form->wait(3);
$form->dom->assign->click();
$form->wait(3);
$form->dom->bugAssigned->scrollToElement();
$form->wait(5);
$bugAssigned = $form->dom->getElementListByXpathKey('bugAssigned', true);
$form->wait(3);
if($bugAssigned[$index] == $assignee) return $this->success('bug直接修改指派成功');
$form->wait(2);
$form->dom->assignBtn->click();
$form->wait(2);
/* 将指派人字段拖动到可见区域 */
$form->dom->firstAssignTo->scrollToElement();
$assignedToAfter = $form->dom->firstAssignTo->getText();
if($assignedToAfter == $assignee) return $this->success('bug直接修改指派成功');
return $this->failed('bug直接修改指派失败');
}
+2 -2
View File
@@ -45,5 +45,5 @@ $user->gen(3);
$tester = new createBugTester();
r($tester->batchAssign($product, 'USER1')) && p('status,message') && e('SUCCESS,bug批量指派成功'); //bug批量指派成功
r($tester->selectAssign($product, 'BUG1', 'admin')) && p('status,message') && e('SUCCESS,bug选择指派成功'); //bug选择指派成功
r($tester->directAssign($product, 'BUG2', 'USER2')) && p('status,message') && e('SUCCESS,bug直接修改指派成功'); //bug直接修改指派成功
$tester->closeBrowser();
r($tester->directAssign('USER2')) && p('status,message') && e('SUCCESS,bug直接修改指派成功'); //bug直接修改指派成功
$tester->closeBrowser();
+2
View File
@@ -40,6 +40,8 @@ class browsePage extends page
'linkBug' => '//*[@id="linkBug"]',
'linkBugsSave' => "//*[@id='table-bug-linkbugs']//button",
'linkedBugs' => "//*[@id='zin_bug_view_1_tabPane_3']/div/ul/li/ul/li/a/div[2]",
'firstAssignTo' => "//*[@id='bugs']/div[2]/div[2]/div/div[7]/div/a/span",
'assignBtn' => "//*[@class='form-row']/div/button/span"
);
$this->dom->xpath = array_merge($this->dom->xpath, $xpath);
@@ -24,6 +24,7 @@ class taskkanbanTester extends tester
{
$form->dom->xpath['kanbanName'] = "(//menu/menu/li)[{$nameId}]";
$form->dom->namePicker->click();
$form->wait(1);
$form->dom->kanbanName->click();
$form->wait(1);
}
+5 -5
View File
@@ -155,12 +155,12 @@ $nums = array(
'left' => '2',
),
'status' => array(
'tasks' => '3',
'waiting' => '3',
'tasks' => '1',
'waiting' => '0',
'doing' => '0',
'estimates' => '3',
'cost' => '0',
'left' => '3',
'estimates' => '1',
'cost' => '6',
'left' => '2',
),
'pri' => array(
'tasks' => '3',
+5 -5
View File
@@ -162,12 +162,12 @@ $nums = array(
'left' => '2',
),
'status' => array(
'tasks' => '3',
'waiting' => '3',
'tasks' => '1',
'waiting' => '0',
'doing' => '0',
'estimates' => '3',
'cost' => '0',
'left' => '3',
'estimates' => '1',
'cost' => '6',
'left' => '2',
),
'pri' => array(
'tasks' => '3',
+2 -2
View File
@@ -7,7 +7,7 @@ $config->program->form = new stdclass();
$config->program->form->create['parent'] = array('type' => 'int', 'control' => 'picker', 'required' => false, 'default' => 0);
$config->program->form->create['name'] = array('type' => 'string', 'control' => 'text', 'required' => true, 'default' => '', 'filter' => 'trim');
$config->program->form->create['PM'] = array('type' => 'string', 'control' => 'picker', 'required' => false, 'default' => 0);
$config->program->form->create['budget'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '');
$config->program->form->create['budget'] = array('type' => 'float', 'control' => 'text', 'required' => false, 'default' => '');
$config->program->form->create['budgetUnit'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => 'CNY');
$config->program->form->create['begin'] = array('type' => 'date', 'control' => 'datepicker', 'required' => true, 'default' => '');
$config->program->form->create['end'] = array('type' => 'date', 'control' => 'datepicker', 'required' => false, 'default' => '');
@@ -19,7 +19,7 @@ $config->program->form->create['whitelist'] = array('type' => 'array', 'cont
$config->program->form->edit['parent'] = array('type' => 'int', 'control' => 'picker', 'required' => false, 'default' => 0);
$config->program->form->edit['name'] = array('type' => 'string', 'control' => 'text', 'required' => true, 'default' => '', 'filter' => 'trim');
$config->program->form->edit['PM'] = array('type' => 'string', 'control' => 'picker', 'required' => false, 'default' => 0);
$config->program->form->edit['budget'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '');
$config->program->form->edit['budget'] = array('type' => 'float', 'control' => 'text', 'required' => false, 'default' => '');
$config->program->form->edit['budgetUnit'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => 'CNY');
$config->program->form->edit['begin'] = array('type' => 'date', 'control' => 'datepicker', 'required' => true, 'default' => '');
$config->program->form->edit['end'] = array('type' => 'date', 'control' => 'datepicker', 'required' => false, 'default' => '');
+1 -1
View File
@@ -17,7 +17,7 @@ $config->project->form->create['multiple'] = array('type' => 'string', 'req
$config->project->form->create['hasProduct'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->project->form->create['stageBy'] = array('type' => 'string', 'required' => false, 'default' => 'product');
$config->project->form->create['PM'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->project->form->create['budget'] = array('type' => 'string', 'required' => false, 'default' => '');
$config->project->form->create['budget'] = array('type' => 'float', 'required' => false, 'default' => '');
$config->project->form->create['budgetUnit'] = array('type' => 'string', 'required' => false, 'default' => 'CNY');
$config->project->form->create['begin'] = array('type' => 'date', 'required' => true);
$config->project->form->create['end'] = array('type' => 'date', 'required' => false, 'default' => null);
@@ -42,7 +42,7 @@ class createSprintTester extends tester
$endTiptext = $form->dom->endTip->getText();
$endTip = sprintf($this->lang->copyProject->endTip, '');
return ($endTiptext == $endTip) ? $this->success('添加迭代表单页提示信息正确') : $this->failed('添加迭代表单页提示信息不正确');
form->wait(1);
$form->wait(2);
}
return $this->failed('添加迭代表单页提示信息不正确');
}
@@ -52,6 +52,7 @@ class createSprintTester extends tester
$sprintForm = $this->initForm('project', 'execution', array('status' => 'undone', 'projectID' => '1'), 'appIframe-project');
/* 跳转到项目迭代列表页面,查看列表中的迭代信息是否正确*/
$browsePage = $this->loadPage('project', 'execution');
$browsePage->wait(2);
//断言检查名称、项目类型是否正确
if($browsePage->dom->sprintName->getText() != $sprint['name']) return $this->failed('名称错误');
+1
View File
@@ -542,6 +542,7 @@ class storyTao extends storyModel
protected function fetchProjectStories(dao $storyDAO, int $productID, string $type, string $branch, array $executionStoryIdList, string $orderBy, ?object $pager = null, ?object $project = null): array
{
if(strpos($orderBy, 'version_') !== false) $orderBy = str_replace('version_', 't2.version_', $orderBy);
if(strpos($orderBy, 'id_') !== false) $orderBy = str_replace('id_', 't2.id_', $orderBy);
$unclosedStatus = $this->getUnclosedStatusKeys();
$assignProduct = false;
+1 -1
View File
@@ -113,7 +113,7 @@ $config->upgrade->execFlow['21_6_1'] = array('xxsqls' => "$appRoot/db/up
$config->upgrade->execFlow['21_7'] = array('functions' => 'fixWorkflowNameForExecution');
$config->upgrade->execFlow['21_7_1'] = array('functions' => 'processActionProduct');
$config->upgrade->execFlow['21_7_5'] = array('functions' => 'weekly-addBuiltinWeeklyTemplate,adjustPriv21_7_5');
$config->upgrade->execFlow['21_7_7'] = array('functions' => 'importBuildinWorkflow,processWorkflowDatasource', 'params' => array('importBuildinWorkflow' => array('all', 'cm,projectchange,risk,opportunity,issue'), 'processWorkflowDatasource' => array(array('baselineStatus' => 'cm_status', 'baselineReviewResult' => 'cm_reviewResult', 'projectchangeUrgencyList' => 'projectchange_urgency', 'projectchangeTypeList' => 'projectchange_type', 'projectchangeStatus' => 'projectchange_status', 'projectchangeReviewResult' => 'projectchange_reviewResult', 'riskSource' => 'risk_source', 'riskCategory' => 'risk_category', 'riskStrategy' => 'risk_strategy', 'riskStatus' => 'risk_status', 'riskImpact' => 'risk_impact', 'riskProbability' => 'risk_probability', 'riskRate' => 'risk_rate', 'riskPri' => 'risk_pri', 'riskCancelReason' => 'risk_cancelReason', 'issuePri' => 'issue_pri', 'issueSeverity' => 'issue_severity', 'issueType' => 'issue_type', 'issueResolution' => 'issue_resolution', 'issueStatus' => 'issue_status', 'opportunitySource' => 'opportunity_source', 'opportunityType' => 'opportunity_type', 'opportunityStrategy' => 'opportunity_strategy', 'opportunityStatus' => 'opportunity_status', 'opportunityImpact' => 'opportunity_impact', 'opportunityChance' => 'opportunity_chance', 'opportunityPri' => 'opportunity_pri', 'opportunityCancelReason' => 'opportunity_cancelReason'))));
$config->upgrade->execFlow['21_7_7'] = array('functions' => 'importBuildinWorkflow,processWorkflowDatasource,alterTableFields', 'params' => array('importBuildinWorkflow' => array('all', 'cm,projectchange,risk,opportunity,issue'), 'processWorkflowDatasource' => array(array('baselineStatus' => 'cm_status', 'baselineReviewResult' => 'cm_reviewResult', 'projectchangeUrgencyList' => 'projectchange_urgency', 'projectchangeTypeList' => 'projectchange_type', 'projectchangeStatus' => 'projectchange_status', 'projectchangeReviewResult' => 'projectchange_reviewResult', 'riskSource' => 'risk_source', 'riskCategory' => 'risk_category', 'riskStrategy' => 'risk_strategy', 'riskStatus' => 'risk_status', 'riskImpact' => 'risk_impact', 'riskProbability' => 'risk_probability', 'riskRate' => 'risk_rate', 'riskPri' => 'risk_pri', 'riskCancelReason' => 'risk_cancelReason', 'issuePri' => 'issue_pri', 'issueSeverity' => 'issue_severity', 'issueType' => 'issue_type', 'issueResolution' => 'issue_resolution', 'issueStatus' => 'issue_status', 'opportunitySource' => 'opportunity_source', 'opportunityType' => 'opportunity_type', 'opportunityStrategy' => 'opportunity_strategy', 'opportunityStatus' => 'opportunity_status', 'opportunityImpact' => 'opportunity_impact', 'opportunityChance' => 'opportunity_chance', 'opportunityPri' => 'opportunity_pri', 'opportunityCancelReason' => 'opportunity_cancelReason'))));
$config->upgrade->execFlow['21_7_8'] = array('functions' => 'initAIPrompts');
if(!empty($config->isINT))
+12
View File
@@ -12997,6 +12997,18 @@ class upgradeModel extends model
return $count == 0;
}
/**
* 修改表字段类型。
* Alter table fields datatype.
*
* @access public
* @return bool
*/
public function alterTableFields()
{
return $this->execSQL($this->getUpgradeFile('datatype'));
}
/**
* 内置AI禅道智能体和相关字段。
* Initialize the AI prompts and fields.
File diff suppressed because one or more lines are too long