Merge branch feature/workflowgroup of zentao/zentaopms (#12764)
This commit is contained in:
+1080
-1080
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
@@ -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直接修改指派失败');
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '');
|
||||
|
||||
@@ -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('名称错误');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user