From dc23d76ee0b7bbc678d62051b28085edbfd729eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Wed, 6 May 2026 15:34:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(validation):=20=E5=88=9B=E5=BB=BA=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E9=AA=8C=E8=AF=81=E8=A7=84=E5=88=99=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以后端 @Valid 注解为唯一真相源,建立 VALIDATION 常量映射, 统一前后端验证规则,消除 roleSort 类不一致问题。 --- .../constants/validation-rules.test.ts | 77 ++++++++ .../src/constants/validation-rules.ts | 167 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 novalon-manage-web/src/__tests__/constants/validation-rules.test.ts create mode 100644 novalon-manage-web/src/constants/validation-rules.ts diff --git a/novalon-manage-web/src/__tests__/constants/validation-rules.test.ts b/novalon-manage-web/src/__tests__/constants/validation-rules.test.ts new file mode 100644 index 0000000..8f20774 --- /dev/null +++ b/novalon-manage-web/src/__tests__/constants/validation-rules.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest' +import { VALIDATION, getRules, getInitialValue, type ValidationField } from '@/constants/validation-rules' + +describe('validation-rules', () => { + describe('VALIDATION', () => { + const requiredFields: ValidationField[] = [ + 'username', 'password', 'email', 'phone', + 'roleName', 'roleKey', 'roleSort', + 'menuName', 'dictName', 'dictType', 'dictLabel', 'dictValue', + 'configName', 'configKey', 'configValue', + 'noticeTitle', 'noticeContent', + 'deptName', + ] + + it.each(requiredFields)('%s should have at least one rule', (field) => { + expect(VALIDATION[field].rules.length).toBeGreaterThan(0) + }) + + it('username should require 3-50 alphanumeric/underscore/dash', () => { + const rules = VALIDATION.username.rules + expect(rules.some((r) => 'required' in r && r.required)).toBe(true) + expect(rules.some((r) => 'min' in r && r.min === 3)).toBe(true) + expect(rules.some((r) => 'max' in r && r.max === 50)).toBe(true) + expect(rules.some((r) => 'pattern' in r)).toBe(true) + }) + + it('password should require 8-20 with uppercase, lowercase, digit', () => { + const rules = VALIDATION.password.rules + expect(rules.some((r) => 'required' in r && r.required)).toBe(true) + expect(rules.some((r) => 'min' in r && r.min === 8)).toBe(true) + expect(rules.some((r) => 'max' in r && r.max === 20)).toBe(true) + expect(rules.some((r) => 'pattern' in r)).toBe(true) + }) + + it('roleSort should have initialValue 1 and min 1', () => { + expect(VALIDATION.roleSort.initialValue).toBe(1) + const rules = VALIDATION.roleSort.rules + expect(rules.some((r) => 'type' in r && r.type === 'number' && 'min' in r && r.min === 1)).toBe(true) + }) + + it('menuSort should have initialValue 0 and min 0', () => { + expect(VALIDATION.menuSort.initialValue).toBe(0) + }) + + it('deptName should require 1-100 chars', () => { + const rules = VALIDATION.deptName.rules + expect(rules.some((r) => 'required' in r && r.required)).toBe(true) + expect(rules.some((r) => 'min' in r && r.min === 1)).toBe(true) + expect(rules.some((r) => 'max' in r && r.max === 100)).toBe(true) + }) + + it('deptSort should have initialValue 0', () => { + expect(VALIDATION.deptSort.initialValue).toBe(0) + }) + }) + + describe('getRules', () => { + it('should return a copy of rules array', () => { + const rules1 = getRules('username') + const rules2 = getRules('username') + expect(rules1).not.toBe(rules2) + expect(rules1).toEqual(rules2) + }) + }) + + describe('getInitialValue', () => { + it('should return initialValue for fields that have it', () => { + expect(getInitialValue('roleSort')).toBe(1) + expect(getInitialValue('menuSort')).toBe(0) + expect(getInitialValue('deptSort')).toBe(0) + }) + + it('should return undefined for fields without initialValue', () => { + expect(getInitialValue('username')).toBeUndefined() + }) + }) +}) diff --git a/novalon-manage-web/src/constants/validation-rules.ts b/novalon-manage-web/src/constants/validation-rules.ts new file mode 100644 index 0000000..56fa82c --- /dev/null +++ b/novalon-manage-web/src/constants/validation-rules.ts @@ -0,0 +1,167 @@ +import type { Rule } from 'antd/es/form' + +interface FieldValidation { + rules: Rule[] + initialValue?: unknown +} + +function requiredRule(message: string): Rule { + return { required: true, message } +} + +function lengthRule(min: number, max: number, message: string): Rule { + return { min, max, message } +} + +function patternRule(pattern: RegExp, message: string): Rule { + return { pattern, message } +} + +function typeNumberMinRule(min: number, message: string): Rule { + return { type: 'number' as const, min, message } +} + +export const VALIDATION = { + username: { + rules: [ + requiredRule('请输入用户名'), + lengthRule(3, 50, '用户名长度必须在3-50之间'), + patternRule(/^[a-zA-Z0-9_-]+$/, '用户名只能包含字母、数字、下划线和横线'), + ], + }, + password: { + rules: [ + requiredRule('请输入密码'), + lengthRule(8, 20, '密码长度必须在8-20之间'), + patternRule(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, '密码必须包含大小写字母和数字'), + ], + }, + nickname: { + rules: [ + lengthRule(0, 100, '昵称长度不能超过100'), + ], + }, + email: { + rules: [ + requiredRule('请输入邮箱'), + { type: 'email' as const, message: '邮箱格式不正确' }, + lengthRule(0, 100, '邮箱长度不能超过100'), + ], + }, + phone: { + rules: [ + requiredRule('请输入手机号'), + patternRule(/^1[3-9]\d{9}$/, '手机号格式不正确'), + ], + }, + roleName: { + rules: [ + requiredRule('请输入角色名称'), + lengthRule(2, 50, '角色名称长度必须在2-50之间'), + ], + }, + roleKey: { + rules: [ + requiredRule('请输入角色标识'), + lengthRule(2, 50, '角色标识长度必须在2-50之间'), + patternRule(/^[a-zA-Z0-9_-]+$/, '角色标识只能包含字母、数字、下划线和横线'), + ], + }, + roleSort: { + rules: [ + requiredRule('请输入排序'), + typeNumberMinRule(1, '排序必须大于0'), + ], + initialValue: 1, + }, + menuName: { + rules: [ + requiredRule('请输入菜单名称'), + lengthRule(1, 50, '菜单名称长度必须在1-50之间'), + ], + }, + menuSort: { + rules: [ + typeNumberMinRule(0, '排序不能为负数'), + ], + initialValue: 0, + }, + dictName: { + rules: [ + requiredRule('请输入字典名称'), + lengthRule(1, 100, '字典名称长度必须在1-100之间'), + ], + }, + dictType: { + rules: [ + requiredRule('请输入字典类型'), + lengthRule(1, 100, '字典类型长度必须在1-100之间'), + patternRule(/^[a-zA-Z0-9_]+$/, '字典类型只能包含字母、数字和下划线'), + ], + }, + dictLabel: { + rules: [ + requiredRule('请输入字典标签'), + lengthRule(1, 100, '字典标签长度必须在1-100之间'), + ], + }, + dictValue: { + rules: [ + requiredRule('请输入字典值'), + lengthRule(1, 100, '字典值长度必须在1-100之间'), + ], + }, + configName: { + rules: [ + requiredRule('请输入配置名称'), + lengthRule(1, 100, '配置名称长度必须在1-100之间'), + ], + }, + configKey: { + rules: [ + requiredRule('请输入配置键'), + lengthRule(1, 100, '配置键长度必须在1-100之间'), + patternRule(/^[a-zA-Z0-9_.-]+$/, '配置键只能包含字母、数字、下划线、点和横线'), + ], + }, + configValue: { + rules: [ + requiredRule('请输入配置值'), + lengthRule(1, 500, '配置值长度必须在1-500之间'), + ], + }, + noticeTitle: { + rules: [ + requiredRule('请输入标题'), + lengthRule(1, 50, '标题长度必须在1-50之间'), + ], + }, + noticeContent: { + rules: [ + requiredRule('请输入内容'), + lengthRule(1, 65535, '内容长度不能超过65535'), + ], + }, + deptName: { + rules: [ + requiredRule('请输入部门名称'), + lengthRule(1, 100, '部门名称长度必须在1-100之间'), + ], + }, + deptSort: { + rules: [ + typeNumberMinRule(0, '排序不能为负数'), + ], + initialValue: 0, + }, +} as const + +export type ValidationField = keyof typeof VALIDATION + +export function getRules(field: ValidationField): Rule[] { + return [...VALIDATION[field].rules] +} + +export function getInitialValue(field: ValidationField): unknown { + return VALIDATION[field].initialValue +}