95 lines
3.2 KiB
Plaintext
95 lines
3.2 KiB
Plaintext
/**
|
|
* 数据校验与冲突提示(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<string, string>, 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<string, string>, 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<number, string[]>): Map<number, ConflictInfo> {
|
|
const out: Map<number, ConflictInfo> = new Map<number, ConflictInfo>();
|
|
pairs.forEach((versions: string[], row: number) => {
|
|
if (versions.length >= 2) {
|
|
out.set(row, ConflictDetector.detect(versions[0], versions[1]));
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
}
|