在做后台管理系统的过程中,有时候要和表单唯一校验打交道。而每次唯一校验的逻辑都是大同小异的,这里参考
Jeecg-Boot框架提供的单字段校验的思路来实现多字段校验,主要总结唯一校验的大概思路。
唯一校验的基本思路
-
页面触发
在表单输入变化(onChange)或者失去焦点(onBlur)的时候触发唯一校验,发起请求,把查询哪个表,以及查询条件封装在请求参数里。
-
后端验证
接收到表名和查询条件之后,先进行SQL注入安全校验,再使用SQL注入表名、查询条件的方式来进行查询。(SQL注入安全校验不可少)
-
页面显示
用表单自定义校验的方式,在接收到请求响应结果之后,如果唯一校验不通过则显示该值已经存在。
代码实现
-
页面触发(介绍react和vue版本)
-
react版
- 定义异步验证请求
import {httpAction} from "@/utils/Request"; import {HTTP_METHODS} from "@/common"; export const validateDuplicateManyValueApi = (params) => httpAction(`/sys/duplicate/checkMany`, params, HTTP_METHODS.POST);-
封装验证工具函数
这里需要注意两个点:
- 这里是需要传入一条记录的id的,因为后端验证的时候通过这个参数判断这是新增时的唯一验证,还是编辑时的唯一验证。因为这两种存在着不同,新增的验证是校验数据库不能存在和传入参数相等的数据,而编辑是校验除了这个id以外的数据不能和传入参数相等。
react版ant-design(4.17)的表单检验 和vue版ant-design-vue(1.7)的自定义校验是不一样。react版已经升级到了Promise的方式,而vue版是callback回调函数的方式(vue3版升级到了Promise)
import {validateDuplicateManyValueApi} from "@/api/Dict"; /** * 根据${fieldMap}和${dataId}设置为查询条件,查询表名为${tableName}的数据。 * 这里为什么没有把fieldMap和dataId合并为一个参数?因为他们逻辑是不同的,fieldMap是查询条件,dataId是用于判读新增还是编辑的校验 * @param tableName * @param fieldMap * @param dataId */ export const validateDuplicateManyValue = async ({tableName, fieldMap, dataId}) => { // 定义成功和失败的Promise const success = Promise.resolve(); const fail = (failMsg) => Promise.reject(failMsg); // 返回结果 let promise; // 首先验证fieldMap里的每个key都有值, 这里不用(!fieldMap[key])的方式判断空,避免假值中0被包含 let existFalse = Object.keys(fieldMap).some(key => fieldMap[key] === null || fieldMap[key] === undefined); // 存在undefined或者null,不发起请求 if (existFalse) { promise = success; } else { try { let res = await validateDuplicateManyValueApi({tableName, fieldMap, dataId}); promise = res.success ? success : fail(res.message); } catch (err) { promise = fail(err.message || err); } } return promise; }; -
函数组件里使用
const [form] = Form.useForm(); // 异步联合验证编码和名称唯一 const validateRoleCodeAndRoleName = () => { let fieldsValue = form.getFieldsValue(); let params = { tableName: "sys_role", dataId: model.id, fieldMap: { role_code: fieldsValue.roleCode, role_name: fieldsValue.roleName, }, }; return validateDuplicateManyValue(params); }; // 验证规则 const validateRules = { roleCode: [ {required: true}, ], roleName: [ {required: true}, {validator: validateRoleCodeAndRoleName}, ], }; return ( <Modal title="添加角色" width={600} visible={modalVisible} onOk={hiddenModal} onCancel={hiddenModal}> <Form form={form} autoComplete={"off"} {...formItemLayout} > <Form.Item name="roleCode" label="角色编码" rules={validateRules.roleCode}> <Input/> </Form.Item> <Form.Item name="roleName" validateTrigger={["onBlur"]} label="角色名称" rules= {validateRules.roleName}> <Input/> </Form.Item> <Form.Item name="description" label="描述"> <Input.TextArea/> </Form.Item> </Form> </Modal> );
-
vue版
主要是工具函数和组件调用方式不同
-
工具函数
需要多传入一个callback参数
export function validateDuplicateManyValue(tableName, fieldMap, dataId, callback) { // 当每个key都有值的时候触发校验 let every = Object.keys(fieldMap).every(key => (fieldMap[key] !== null && fieldMap[key] !== undefined)); if (every) { let params = { tableName, fieldMap, dataId }; postAction('/sys/duplicate/checkMany', params).then(res => { res['success'] ? callback() : callback(res['message']); }).catch(err => { callback(err.message || err); }); } else { callback(); } } -
组件调用
// template <a-form-model ref="form" v-bind="layout" :model="model" :rules="validatorRules"> <a-form-model-item label="角色编码" required prop="roleCode"> <a-input v-model="model.roleCode" :disabled="roleDisabled" placeholder="请输入角色编码"/> </a-form-model-item> <a-form-model-item label="角色名称" required prop="roleName"> <a-input v-model="model.roleName" placeholder="请输入角色名称"/> </a-form-model-item> <a-form-model-item label="描述" prop="description"> <a-textarea :rows="5" v-model="model.description" placeholder="请输入角色描述"/> </a-form-model-item> </a-form-model> // script validatorRules:{ roleName: [ { required: true, message: '请输入角色名称!' }, { validator: (rule, value, callback) => validateDuplicateManyValue("sys_role", { "role_name": value,"role_code":this.model.roleCode }, this.model.id, callback), trigger: "blur", }, ], roleCode: [ { required: true, message: '请输入角色名称!'}, ], },
-
-
-
后端验证
前端参数封装完了之后,开始后端接收进行验证
-
安全校验
public static void specialFilterContent(String value) { String specialXssStr = " exec | insert | select | delete | update | drop | count | chr | mid | master | truncate | char | declare |;|+|"; String[] xssArr = specialXssStr.split("\|"); if (value == null || "".equals(value)) { return; } // 统一转为小写 value = value.toLowerCase(); for (String s : xssArr) { if (value.contains(s) || value.startsWith(s.trim())) { log.error("请注意,存在SQL注入关键词---> {}", s); log.error("请注意,值可能存在SQL注入风险!---> {}", value); throw new RuntimeException("请注意,值可能存在SQL注入风险!--->" + value); } } } -
判断是新增验证还是编辑验证
if (StringUtils.isNotBlank(duplicateCheckManyVo.getDataId())) { // [2].编辑页面校验 num = sysDictMapper.duplicateCheckManyCountSql(duplicateCheckManyVo); } else { // [1].添加页面校验 num = sysDictMapper.duplicateCheckManyCountSqlNoDataId(duplicateCheckManyVo); } -
sql注入
-
新增验证SQL
<select id="duplicateCheckManyCountSqlNoDataId" resultType="java.lang.Long"> SELECT COUNT(*) FROM ${tableName} WHERE 1=1 <if test="fieldMap!= null"> <foreach collection="fieldMap.entrySet()" item="value" index="key"> <if test="value != null"> and ${key} = #{value} </if> </foreach> </if> </select> -
编辑验证SQL
<select id="duplicateCheckManyCountSql" resultType="java.lang.Long"> SELECT COUNT(*) FROM ${tableName} WHERE id <> #{dataId} <if test="fieldMap!= null"> <foreach collection="fieldMap.entrySet()" item="value" index="key"> <if test="value == null"> and ${key} is null </if> <if test="value != null"> and ${key} = #{value} </if> </foreach> </if> </select>效果展示
-
页面数据
-
表单数据
在新增表单里输入和之前数据一样的数据
题外话
-
react版的ant-design库与vue版的ant-design-vue库对比:
-
在上面的react的例子中,我没有加必填校验的
message字段,react却可以根据label值拼接出来”请输入角色编码“的提示信息。而vue版只有”roleCode is required“的提示。 -
两个UI库在设置表单验证时机的代码上也不一样,react是使用
validateTrigger设置在Form.Item表标签上,而vue是使用trigger设置rules规则里。
-
-
需要发起请求的校验一定要限制请求时机的,因为是
onChange就执行请求,这会使得这个唯一校验的接口压力很大。原本是希望给这个接口加上防抖的,但是加上防抖之后,自定义校验不生效,所以这里只能退而求其次把校验时机从onChange改为了onBlur。(如果知道怎么在自定义校验里加防抖的童鞋,望告知,谢谢) -
最后说明以上的校验思路是参考JEECG框架提供的单字段校验模式,来完成的多字段校验。
-
-
-