Trigger Scenarios
- Refactoring table column definitions (dataIndex, columns)
- Refactoring enum type mappings (typeMap, statusMap)
- Refactoring data transformation logic (render functions, formatter)
- Multiple similar fields in type definitions (changedAt vs createdAt)
Core Principles
Do not guess field names based on type definitions; you must look at the actual usage in the original code
Checklist
1. Field Name Verification
- Check the dataIndex in the original code
- Check the actual field names returned by the backend API
- Prefer required fields and avoid optional fields
- Pay attention to subtle differences in field names (changedAt vs createdAt, changeType vs operationType)
2. Enum Mapping Verification
- Check each enum value one by one against the original code
- Ensure the mapping is complete (do not omit any enum value)
- Check the spelling of enum values (UPLOAD vs Upload)
- Check whether the color and text of enum values are consistent
3. Runtime Testing
- TypeScript type checking passes (necessary but not sufficient)
- Test functionality in a real environment
- Check whether "Invalid Date", "undefined", or blank values are displayed
- Check whether all enum values have corresponding mappings
Common Pitfalls
Pitfall 1: Ambiguous Type Definitions
Scenario: There are multiple similar fields in the type definitions
interface InvoiceHistory {
changedAt?: string // 可选字段
createdAt: string // 必填字段
changeType?: string // 错误字段
operationType: string // 正确字段
}
Incorrect Approach:
// ❌ 根据类型定义推测,选择了可选字段
dataIndex: 'changedAt' // 可能为 undefined → Invalid Date
dataIndex: 'changeType' // 字段不存在 → 显示为空
Correct Approach:
// ✅ 查看原始代码的实际使用
git show HEAD~1:src/components/InvoiceHistoryTab.tsx
// 原始代码使用 createdAt(必填)和 operationType(正确)
dataIndex: 'createdAt'
dataIndex: 'operationType'
Pitfall 2: Incomplete Enum Mapping
Scenario: Some enum values are omitted during refactoring
Incorrect Approach:
// ❌ 只保留了 3 个类型
typeMap: {
CREATE: { text: '创建', color: 'green' },
UPDATE: { text: '更新', color: 'blue' },
DELETE: { text: '删除', color: 'red' },
}
Correct Approach:
// ✅ 查看原始代码,确保完整
git show HEAD~1:src/components/InvoiceHistoryTab.tsx
// 原始代码有 10 个类型
typeMap: {
UPLOAD: { text: '上传发票', color: 'blue' },
OCR_START: { text: '开始OCR', color: 'cyan' },
OCR_SUCCESS: { text: 'OCR成功', color: 'green' },
OCR_FAILED: { text: 'OCR失败', color: 'red' },
OCR_RETRY: { text: '重试OCR', color: 'orange' },
MANUAL_EDIT: { text: '手动编辑', color: 'orange' },
LINK_ORDER: { text: '关联订单', color: 'purple' },
UNLINK_ORDER: { text: '取消关联', color: 'magenta' },
FIELD_CONFIRM: { text: '确认字段', color: 'green' },
DELETE: { text: '删除发票', color: 'red' },
}
Pitfall 3: TypeScript Cannot Detect
Scenario: The field name is wrong but TypeScript does not report an error
// ❌ TypeScript 不会报错(changedAt 在类型定义中存在)
dataIndex: 'changedAt' // 类型检查通过
render: (val: string) => new Date(val).toLocaleString()
// 运行时:val 为 undefined → Invalid Date
Correct Approach:
// ✅ 运行时测试 + 防御性编程
dataIndex: 'createdAt'
render: (val: string) => {
if (!val) return '-'
try {
return new Date(val).toLocaleString('zh-CN')
} catch {
return val
}
}
Pitfall 4: rowKey Uses an Optional Field
Scenario: rowKey uses a field that may be undefined
Incorrect Approach:
// ❌ changedAt 可能为 undefined
rowKey={(record, index) => `${record.changedAt}-${index}`}
// 结果:undefined-0, undefined-1 → key 重复
Correct Approach:
// ✅ 使用必填字段 id
rowKey={(record) => record.id}
Pitfall 5: Synchronous Overwrite Fails to Account for "Missing on Source Side" and "Manual Adjustment"
Scenario: Data is synchronized from external sources (Leijue, ERP, CRM) to this system on a scheduled/manual basis, and each synchronization directly overwrites the target fields.
Risk 5.1: Source field is null/missing → clears the existing value on the target side
// ❌ 源端这次没返回 categoryId,目标端 categoryId 被清成 null
public void syncFromExternal(ExternalDetail detail) {
Product product = productRepository.findById(detail.getProductId()).orElseThrow();
Long categoryId = resolveCategory(detail); // 可能返回 null
product.setCategoryId(categoryId); // null 也覆盖 → 用户之前手动设置的分类被清空
productRepository.save(product);
}
// ✅ 源端缺失则不覆盖
public void syncFromExternal(ExternalDetail detail) {
Product product = productRepository.findById(detail.getProductId()).orElseThrow();
Long categoryId = resolveCategory(detail);
if (categoryId != null && !Objects.equals(product.getCategoryId(), categoryId)) {
product.setCategoryId(categoryId);
productRepository.save(product);
}
}
Risk 5.2: User manual adjustments are overwritten by the next synchronization
Scenario: The user changes the product category imported from Leijue in the system from "Home" to "Office Supplies", and the next Leijue import forcibly overwrites it back to "Home" → the user's action is silently discarded.
Accompanying Strategy (last-sync-value pattern): Record the "value given to the target side during the last synchronization" in the mapping table, and before overwriting, determine whether the target side is still equal to that value. If not, it means the user manually changed it, so skip the overwrite.
// 1. mapping 表新增 last_category_id 字段
@Entity
public class LeijueProductMapping {
private Long productId;
private Long lastCategoryId; // 上次同步时设置的 categoryId
// ...
}
// 2. 同步时按"伴随策略"判定
private boolean syncCategoryField(Product product, Long newCategoryId, Long lastCategoryId) {
if (newCategoryId == null) return false; // 源缺失不动(风险 5.1)
boolean isInitial = product.getCategoryId() == null;
boolean followingLastSync = Objects.equals(product.getCategoryId(), lastCategoryId);
if (isInitial || followingLastSync) {
product.setCategoryId(newCategoryId);
return true;
} else {
log.info("跳过覆盖,目标已被人工调整: productId={}, current={}, lastSync={}, source={}",
product.getId(), product.getCategoryId(), lastCategoryId, newCategoryId);
return false;
}
}
// 3. 同步成功后更新 lastCategoryId
if (newCategoryId != null) {
mapping.setLastCategoryId(newCategoryId);
}
Decision Matrix
| Source Value | Target Current Value == lastSyncValue | Decision |
|---|---|---|
| null | Any | Do not change(Source missing, do not clear) |
| Has value | true (still the last synced value) | Overwrite(User has not modified, follow sync) |
| Has value | false (inconsistent with last time) | Skip(User has modified, do not overwrite) |
| Has value | Target is null | Overwrite(First sync) |
When an accompanying policy is not needed
- Fields that users cannot edit (such as external unique IDs, order numbers, external status codes): just overwrite directly
- Computed fields (such as statistical snapshots): recalculate and overwrite each time
- Scenarios requiring strong consistency of master data (such as supplier catalogs): explicitly take the external source as authoritative
Checklist
- Is there a null check written before
entity.setXxx(newValue)in the sync process - When the source field is null, will it incorrectly clear the target side
- Do user-editable fields consider "manual adjustments not being overwritten"
- Do fields that need an accompanying policy have a
last_xxxcolumn in the mapping table - When skipping sync, is
log.infologged for troubleshooting
Verification process
1. Static check
# TypeScript 类型检查
npm run type-check
# ESLint 检查
npm run lint
2. Runtime testing
- Test the functionality in the development environment
- Check whether "Invalid Date" is displayed
- Check whether "undefined" or blank is displayed
- Check whether all enum values have corresponding mappings
- Check whether rowKey is unique
3. Comparative verification
# 查看原始代码
git show HEAD~1:src/components/InvoiceHistoryTab.tsx
# 制作对比清单
| 字段 | 原始代码 | 重构后 | 状态 |
|------|---------|--------|------|
| 时间字段 | createdAt | changedAt | ❌ 错误 |
| 操作类型 | operationType | changeType | ❌ 错误 |
| 枚举映射 | 10 个 | 3 个 | ❌ 不完整 |
| rowKey | record.id | record.changedAt | ❌ 错误 |
Defensive programming
1. Optional field handling
// ✅ 检查字段是否存在
render: (val: string) => {
if (!val) return '-'
return val
}
2. Date formatting
// ✅ 捕获异常
render: (val: string) => {
if (!val) return '-'
try {
return new Date(val).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
} catch {
return val
}
}
3. Enum mapping
// ✅ 提供默认值
render: (type: string) => {
const config = typeMap[type] || { text: type, color: 'default' }
return <Tag color={config.color}>{config.text}</Tag>
}
Rule traceability
> 📋 本回复遵循:`cc-field-mapping-safety` - [章节]