mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-11 18:19:51 +08:00
🐛 fix(datagrid-mysql): 修复MySQL行编辑时datetime空值提交失败
- 前端按列类型归一化 temporal 字段,INSERT 空值跳过字段、UPDATE 空值转 NULL - 后端 ApplyChanges 增加 temporal 字段兜底,避免空字符串写入 datetime/timestamp - 新增全默认值插入路径,兼容 CURRENT_TIMESTAMP 等默认值场景 - refs #113
This commit is contained in:
@@ -90,6 +90,14 @@ const normalizeDateTimeString = (val: string) => {
|
||||
return `${match[1]} ${match[2]}`;
|
||||
};
|
||||
|
||||
const isTemporalColumnType = (columnType?: string): boolean => {
|
||||
const raw = String(columnType || '').trim().toLowerCase();
|
||||
if (!raw) return false;
|
||||
if (raw.includes('datetime') || raw.includes('timestamp')) return true;
|
||||
const base = raw.split(/[ (]/)[0];
|
||||
return base === 'date' || base === 'time' || base === 'year';
|
||||
};
|
||||
|
||||
// --- Helper: Format Value ---
|
||||
const formatCellValue = (val: any) => {
|
||||
try {
|
||||
@@ -764,6 +772,35 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return next;
|
||||
}, [columnMetaMap]);
|
||||
|
||||
const normalizeCommitCellValue = useCallback(
|
||||
(columnName: string, value: any, mode: 'insert' | 'update') => {
|
||||
if (value === undefined) return undefined;
|
||||
const normalizedName = String(columnName || '').trim();
|
||||
const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()];
|
||||
const temporal = isTemporalColumnType(meta?.type);
|
||||
|
||||
if (!temporal) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const raw = value.trim();
|
||||
if (raw === '') {
|
||||
// INSERT 空时间值直接忽略字段,让数据库默认值生效;UPDATE 空时间值转 NULL。
|
||||
return mode === 'insert' ? undefined : null;
|
||||
}
|
||||
return normalizeDateTimeString(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
[columnMetaMap, columnMetaMapByLowerName]
|
||||
);
|
||||
|
||||
const renderColumnTitle = useCallback((name: string): React.ReactNode => {
|
||||
const normalizedName = String(name || '');
|
||||
const meta = columnMetaMap[normalizedName] || columnMetaMapByLowerName[normalizedName.toLowerCase()];
|
||||
@@ -1814,7 +1851,17 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const updates: any[] = [];
|
||||
const deletes: any[] = [];
|
||||
|
||||
addedRows.forEach(row => { const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = row; inserts.push(vals); });
|
||||
addedRows.forEach(row => {
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = row;
|
||||
const normalizedValues: Record<string, any> = {};
|
||||
Object.entries(vals).forEach(([col, val]) => {
|
||||
const normalizedVal = normalizeCommitCellValue(col, val, 'insert');
|
||||
if (normalizedVal !== undefined) {
|
||||
normalizedValues[col] = normalizedVal;
|
||||
}
|
||||
});
|
||||
inserts.push(normalizedValues);
|
||||
});
|
||||
deletedRowKeys.forEach(keyStr => {
|
||||
// Find original data
|
||||
const originalRow = data.find(d => rowKeyStr(d?.[GONAVI_ROW_KEY]) === keyStr) || addedRows.find(d => rowKeyStr(d?.[GONAVI_ROW_KEY]) === keyStr);
|
||||
@@ -1847,8 +1894,16 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(values).length === 0) return;
|
||||
updates.push({ keys: pkData, values });
|
||||
const normalizedValues: Record<string, any> = {};
|
||||
Object.entries(values).forEach(([col, val]) => {
|
||||
const normalizedVal = normalizeCommitCellValue(col, val, 'update');
|
||||
if (normalizedVal !== undefined) {
|
||||
normalizedValues[col] = normalizedVal;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(normalizedValues).length === 0) return;
|
||||
updates.push({ keys: pkData, values: normalizedValues });
|
||||
});
|
||||
|
||||
if (inserts.length === 0 && updates.length === 0 && deletes.length === 0) {
|
||||
|
||||
@@ -501,6 +501,8 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
return fmt.Errorf("connection not open")
|
||||
}
|
||||
|
||||
columnTypeMap := m.loadColumnTypeMap(tableName)
|
||||
|
||||
tx, err := m.conn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -513,7 +515,7 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
var args []interface{}
|
||||
for k, v := range pk {
|
||||
wheres = append(wheres, fmt.Sprintf("`%s` = ?", k))
|
||||
args = append(args, normalizeMySQLDateTimeValue(v))
|
||||
args = append(args, normalizeMySQLValueForWrite(k, v, columnTypeMap))
|
||||
}
|
||||
if len(wheres) == 0 {
|
||||
continue
|
||||
@@ -535,7 +537,7 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
|
||||
for k, v := range update.Values {
|
||||
sets = append(sets, fmt.Sprintf("`%s` = ?", k))
|
||||
args = append(args, normalizeMySQLDateTimeValue(v))
|
||||
args = append(args, normalizeMySQLValueForWrite(k, v, columnTypeMap))
|
||||
}
|
||||
|
||||
if len(sets) == 0 {
|
||||
@@ -545,7 +547,7 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
var wheres []string
|
||||
for k, v := range update.Keys {
|
||||
wheres = append(wheres, fmt.Sprintf("`%s` = ?", k))
|
||||
args = append(args, normalizeMySQLDateTimeValue(v))
|
||||
args = append(args, normalizeMySQLValueForWrite(k, v, columnTypeMap))
|
||||
}
|
||||
|
||||
if len(wheres) == 0 {
|
||||
@@ -569,12 +571,24 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
var args []interface{}
|
||||
|
||||
for k, v := range row {
|
||||
normalizedValue, omit := normalizeMySQLValueForInsert(k, v, columnTypeMap)
|
||||
if omit {
|
||||
continue
|
||||
}
|
||||
cols = append(cols, fmt.Sprintf("`%s`", k))
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, normalizeMySQLDateTimeValue(v))
|
||||
args = append(args, normalizedValue)
|
||||
}
|
||||
|
||||
if len(cols) == 0 {
|
||||
query := fmt.Sprintf("INSERT INTO `%s` () VALUES ()", tableName)
|
||||
res, err := tx.Exec(query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert error: %v", err)
|
||||
}
|
||||
if affected, err := res.RowsAffected(); err == nil && affected == 0 {
|
||||
return fmt.Errorf("插入未生效:未影响任何行")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -629,6 +643,69 @@ func normalizeMySQLDateTimeValue(value interface{}) interface{} {
|
||||
return value
|
||||
}
|
||||
|
||||
func (m *MySQLDB) loadColumnTypeMap(tableName string) map[string]string {
|
||||
result := map[string]string{}
|
||||
table := strings.TrimSpace(tableName)
|
||||
if table == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
columns, err := m.GetColumns("", table)
|
||||
if err != nil {
|
||||
logger.Warnf("加载列元数据失败(不影响提交):表=%s err=%v", table, err)
|
||||
return result
|
||||
}
|
||||
|
||||
for _, col := range columns {
|
||||
name := strings.ToLower(strings.TrimSpace(col.Name))
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
result[name] = strings.TrimSpace(col.Type)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeMySQLValueForInsert(columnName string, value interface{}, columnTypeMap map[string]string) (interface{}, bool) {
|
||||
columnType := strings.ToLower(strings.TrimSpace(columnTypeMap[strings.ToLower(strings.TrimSpace(columnName))]))
|
||||
if !isMySQLTemporalColumnType(columnType) {
|
||||
return value, false
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if ok && strings.TrimSpace(text) == "" {
|
||||
// INSERT 空时间字段不写入,交给 DB 默认值处理(如 CURRENT_TIMESTAMP)。
|
||||
return nil, true
|
||||
}
|
||||
return normalizeMySQLDateTimeValue(value), false
|
||||
}
|
||||
|
||||
func normalizeMySQLValueForWrite(columnName string, value interface{}, columnTypeMap map[string]string) interface{} {
|
||||
columnType := strings.ToLower(strings.TrimSpace(columnTypeMap[strings.ToLower(strings.TrimSpace(columnName))]))
|
||||
if !isMySQLTemporalColumnType(columnType) {
|
||||
return value
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if ok && strings.TrimSpace(text) == "" {
|
||||
return nil
|
||||
}
|
||||
return normalizeMySQLDateTimeValue(value)
|
||||
}
|
||||
|
||||
func isMySQLTemporalColumnType(columnType string) bool {
|
||||
raw := strings.ToLower(strings.TrimSpace(columnType))
|
||||
if raw == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(raw, "datetime") || strings.Contains(raw, "timestamp") {
|
||||
return true
|
||||
}
|
||||
base := raw
|
||||
if idx := strings.IndexAny(base, "( "); idx >= 0 {
|
||||
base = base[:idx]
|
||||
}
|
||||
return base == "date" || base == "time" || base == "year"
|
||||
}
|
||||
|
||||
func hasTimezoneOffset(text string) bool {
|
||||
pos := strings.LastIndexAny(text, "+-")
|
||||
if pos < 0 || pos < 10 || pos+1 >= len(text) {
|
||||
|
||||
Reference in New Issue
Block a user