/** * 数据校验与冲突提示(1.7.11)。 * * 两层: * - RowValidator:按列规则(必填/长度/正则)校验单元格与整行; * - ConflictDetector:乐观并发——提交前比对「读取时版本」与「当前库中版本」, * 不一致即判定冲突,UI 提示用户重新加载或覆盖。 */ export interface ColumnRule { column: string; required: boolean; minLength: number; maxLength: number; /** 正则;为空表示不校验。 */ pattern: string; } export interface ValidationIssue { column: string; message: string; } export class RowValidator { /** 校验单元格,返回错误消息(空数组通过)。 */ static validateCell(rule: ColumnRule, value: string): string[] { const errors: string[] = []; const text: string = value === undefined || value === null ? '' : value; if (rule.required && text.trim().length === 0) { errors.push(rule.column + ' is required'); return errors; } if (text.length > 0 && rule.maxLength > 0 && text.length > rule.maxLength) { errors.push(rule.column + ' exceeds max length ' + rule.maxLength); } if (text.length > 0 && rule.minLength > 0 && text.length < rule.minLength) { errors.push(rule.column + ' is shorter than min length ' + rule.minLength); } if (text.length > 0 && rule.pattern.length > 0 && !new RegExp(rule.pattern).test(text)) { errors.push(rule.column + ' does not match pattern ' + rule.pattern); } return errors; } /** 校验整行(values: 列 → 值)。 */ static validateRow(values: Map, rules: ColumnRule[]): ValidationIssue[] { const issues: ValidationIssue[] = []; for (let i = 0; i < rules.length; i++) { const rule: ColumnRule = rules[i]; const value: string | undefined = values.get(rule.column); const errors: string[] = RowValidator.validateCell(rule, value === undefined ? '' : value); for (let j = 0; j < errors.length; j++) { issues.push({ column: rule.column, message: errors[j] }); } } return issues; } /** 便捷:无错误为 true。 */ static isValid(values: Map, rules: ColumnRule[]): boolean { return RowValidator.validateRow(values, rules).length === 0; } } export interface ConflictInfo { conflict: boolean; reason: string; } export class ConflictDetector { /** * 乐观并发检测:读取时版本与提交时版本不同即冲突。 * 版本可以是时间戳、行版本号或 updated_at 字符串。 */ static detect(fetchedVersion: string, currentVersion: string): ConflictInfo { if (fetchedVersion === currentVersion) { return { conflict: false, reason: '' }; } return { conflict: true, reason: 'row changed since load: fetched=' + fetchedVersion + ', current=' + currentVersion }; } /** 批量检测(行号 → {fetched, current})。 */ static detectBatch(pairs: Map): Map { const out: Map = new Map(); pairs.forEach((versions: string[], row: number) => { if (versions.length >= 2) { out.set(row, ConflictDetector.detect(versions[0], versions[1])); } }); return out; } }