@specpow/framework 0.5.22 → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ # MES 项目公共工具方法参考
2
+
3
+ > 本文件为 AI 代码生成时的参考手册,列出项目中可直接复用的公共工具类和方法。
4
+ > 生成代码时应优先使用这些方法,避免重复实现。
5
+
6
+ ## 后端工具类
7
+
8
+ ### ResponseWrapper (com.twsz.mom.core.common.ResponseWrapper)
9
+ - `ofSuccess(T data)` → 返回 code=200 + data
10
+ - `ofFail(String message)` → 返回 code=600 + 错误消息
11
+ - `defaultSuccess()` → 返回 code=200 + data=null(无数据成功)
12
+ - `ofStatus(IHttpStatus status)` → 返回自定义状态码
13
+ - `ofException(BaseException e)` → 从异常构造响应
14
+ - `ofFlat(int code, String message, T data)` → 不截断消息的响应
15
+ - `ofMessage(String message)` → 返回 code=200 + 消息(无数据)
16
+ - `isSuccess()` → 判断是否成功(code==200 或 200<=code<300)
17
+
18
+ ### AIHttpStatus (com.twsz.mom.core.common.AIHttpStatus)
19
+ - 枚举类,实现 IHttpStatus 接口
20
+ - 新增 code 规则: 取枚举中已有 code 最大值 + 1(maxCode + 1),禁止硬编码数字
21
+ - 枚举命名: 中文提示的缩写,如 "类型不能为空" → `TYPE_NOT_EMPTY`
22
+ - 定义格式: `TYPE_NOT_EMPTY(10000000X, "类型不能为空")`
23
+ - code 格式: 100000001, 100000002, ... 递增
24
+ - message 支持 `[{0}]` 占位符,通过 data 数组传参
25
+ - 使用方式: `throw new BaseException(AIHttpStatus.XXX.getCode(), AIHttpStatus.XXX.getMessage())`
26
+ - 带参数: `throw new BaseException(AIHttpStatus.XXX.getCode(), AIHttpStatus.XXX.getMessage(), param1)`
27
+ - 响应: `ResponseWrapper.ofStatus(AIHttpStatus.XXX, Arrays.asList("param1"))`
28
+
29
+ ### PageForm<T> (com.twsz.mom.core.common.PageForm)
30
+ - `getSize()` → 每页条数
31
+ - `getCurrent()` → 当前页码(1-based)
32
+ - `getCondition()` → 查询条件对象(泛型 T)
33
+ - `getRecords()` → 结果记录列表
34
+
35
+ ### BaseException (com.twsz.mom.core.exception.BaseException)
36
+ - `new BaseException(HttpStatus status)` → 从状态码构造
37
+ - `new BaseException(String message)` → 从消息构造
38
+ - `new BaseException(HttpStatus status, Object... args)` → 带参数格式化
39
+
40
+ ### ExcelUtil (com.twsz.mom.core.utils.ExcelUtil)
41
+ - `importExcel(InputStream is)` → `List<Map<String, Object>>`(原始导入)
42
+ - `importExcel(InputStream is, Class<T> clazz)` → `List<T>`(类型化导入)
43
+ - `export(HttpServletResponse resp, Class<T> clazz, Boolean isCsv, Collection<?> data)` → 标准导出
44
+ - `export(HttpServletResponse resp, Class<T> clazz, Boolean isCsv, Supplier<Collection<?>> supplier)` → 懒加载导出
45
+ - `responseJson(HttpServletResponse resp, ResponseWrapper<T> wrapper)` → 写 JSON 错误响应
46
+ - `exportException(HttpServletResponse resp, Throwable e)` → 写异常为 JSON
47
+
48
+ ### SecurityUtils (com.twsz.mom.core.utils.SecurityUtils)
49
+ - `getCurrentUsername()` → 获取当前登录用户名
50
+ - `getCurrentOrgId()` → 获取当前组织 ID(通常不需要调用,多租户插件自动处理)
51
+ - `getCurrentUserId()` → 获取当前用户 ID
52
+
53
+ ### IdUtil (cn.hutool.core.util.IdUtil)
54
+ - `fastSimpleUUID()` → 生成无横线 UUID(用于导出文件名等)
55
+
56
+ ### EasyExcel (com.alibaba.excel.EasyExcel)
57
+ - `EasyExcel.write(OutputStream, Class<T>).sheet("sheet").doWrite(List)` → 简单导出
58
+ - `EasyExcel.read(InputStream, Class<T>, ReadListener).sheet().doRead()` → 监听式导入
59
+
60
+ ### BaseExcelExportTemplate (com.twsz.mom.core.web.excel.BaseExcelExportTemplate)
61
+ - 抽象类:`getTotal()` + `getData(Integer start, Integer size)` → 分批导出模板
62
+ - 默认 batch size: 1000, sheet max: 1,048,576
63
+
64
+ ### ViewModel 常量 (com.twsz.mom.ds.model.ViewModel)
65
+ - `ViewModel.OPT_ADD = "1"` — 新增标记
66
+ - `ViewModel.OPT_MODIFY = "2"` — 修改标记
67
+ - `ViewModel.OPT_DELETE = "3"` — 删除标记
68
+ - `ViewModel.OPT_NOT_MODIFY_DATE = "0"` — 不修改日期标记
69
+
70
+ ### MetaHandler (com.twsz.mom.ds.handler.MetaHandler)
71
+ - 自动填充: INSERT 时自动设置 createdBy/createdDate/lastUpdatedBy/lastUpdatedDate
72
+ - 自动填充: UPDATE 时自动设置 lastUpdatedBy/lastUpdatedDate
73
+ - 通过 @TableField(fill = FieldFill.INSERT/INSERT_UPDATE) 触发
74
+
75
+ ---
76
+
77
+ ## 前端工具方法
78
+
79
+ ### 请求封装 (@/libs/request)
80
+ - `axios.request({ url, method, data })` → 统一请求方法
81
+ - 响应拦截器: 自动提取 data.data,错误自动 toast 提示
82
+
83
+ ### 导出工具 (@/api/file)
84
+ - `exportExcel(url, data)` → 下载 Excel(Blob → 创建下载链接)
85
+ - `downloadFile(url, data)` → 通用文件下载
86
+ - `uploadFile(url, data)` → 文件上传
87
+
88
+ ### indexPage Mixin (_c/table-form/index-mixin)
89
+ - `this.add(formData)` → 打开新增表单
90
+ - `this.edit(row)` → 打开编辑表单(传入行数据)
91
+ - `this.view(row)` → 打开查看表单(只读模式)
92
+ - `this.refresh()` → 刷新列表(重新搜索第 2 页)
93
+ - `this.triggerBack(showIndexPage)` → 切换列表/表单视图
94
+ - `this.handleSuccess(item, back)` → 表单提交成功回调
95
+ - `this.showIndexPage` → 控制列表/表单显示状态
96
+ - `this.formData` → 传递给表单的数据
97
+ - `this.readonly` → 只读模式标记
98
+ - `this.dataSelections` → 当前选中行
99
+
100
+ ### BaseMixin (@/mixin)
101
+ - `this.startLoading()` → 开始 loading
102
+ - `this.finishLoading()` → 结束 loading
103
+ - `this.asyncLoading(Promise)` → 自动管理 loading 的异步调用
104
+ - `this.loading` → loading 状态
105
+
106
+ ### tw-table Option 常用配置
107
+ - `option.tableName` → 唯一标识(必须)
108
+ - `option.searchForm` → 搜索条件双向绑定
109
+ - `option.fixedTableHeight` → 自动填充剩余高度
110
+ - `option.add.enable` → 启用新增按钮
111
+ - `option.delete.enable` → 启用删除(支持函数动态判断)
112
+ - `option.delete.before(rows)` → 删除前校验(return false 阻止)
113
+ - `option.edit.enable` → 启用编辑(支持函数动态判断)
114
+ - `option.view.enable` → 启用查看
115
+ - `option.search.query` → 查询 API 方法
116
+ - `option.export.enable` → 启用导出
117
+ - `option.export.method` → 导出 API 方法
118
+
119
+ ### 国际化 ($t)
120
+ - `$t('key||中文默认值')` → 带默认值的国际化
121
+ - `$t('table.entityName.fieldName||字段中文名')` → 表格字段
122
+ - `$t('operate||操作')`, `$t('add||新增')`, `$t('edit||编辑')`
123
+ - `$t('submit||提交')`, `$t('submit.success||提交成功')`
124
+ - `$t('error.input.notEmpty||输入不能为空')`
125
+
126
+ ---
127
+
128
+ ## MyBatis-Plus 拦截器链
129
+
130
+ 按执行顺序:
131
+ 1. `TenantLineInnerInterceptor` — 多租户 org_id 自动注入
132
+ 2. `AdvanceQueryInterceptor` — 高级查询(Oracle)
133
+ 3. `PaginationInnerInterceptor` — 分页(overflow 已启用)
134
+ 4. `OptimisticLockerInnerInterceptor` — 乐观锁(@Version 字段)
135
+ 5. `BatchUpdateInterceptor` — 批量更新
136
+ 6. `AuditLogInterceptor` — 审计日志
137
+
138
+ ---
139
+
140
+ ## 全局注册组件
141
+
142
+ 通过 `Vue.component()` 全局注册,可直接使用:
143
+ - `tw-table` — 核心表格组件
144
+ - `master-sub` — 主子表布局(垂直分栏)
145
+ - `tw-card` — 卡片容器
146
+ - `RemoteSelect` — 远程下拉选择
147
+ - `RemoteCheckbox` — 远程复选框
148
+ - `RemoteRadio` — 远程单选框
149
+ - `TableSelect` — 表格弹窗选择(放大镜)
150
+ - `SelectModal` — 选择弹窗
@@ -0,0 +1,204 @@
1
+ # MES PRD to Code — 项目规范规则集
2
+
3
+ > 本文件由 specpow 框架的 `extractRulesFromSchemaFiles` 自动加载。
4
+ > 所有以 `-` 开头的行会被提取为约束规则,通过 `{{rules}}` 注入到 AI instruction 中。
5
+ > 来源: 项目实际代码(BaseModel/ViewModel/ServiceImpl/Mapper XML/Vue 组件)+ DATABASE.md + JAVA.md + TESTING.md
6
+
7
+ ## 数据库 DDL 规范
8
+
9
+ - 表名格式: `{模块简称}_{功能描述}`,小写下划线,模块前缀: BD/MM/WIP/FG/PM/QM/EM/MCC/AGV/RPT
10
+ - 子表命名: `{主表名}_DETAIL`,配置表: `{功能}_CONFIG`,历史表: `{功能}_H_T`
11
+ - 主键字段: `id NUMBER(19) NOT NULL`,使用雪花算法生成(IdType.ASSIGN_ID),禁止 Oracle SEQUENCE
12
+ - 多租户字段: `org_id NUMBER(19) NOT NULL`,仅在 DDL 中定义,不在 Entity 中声明(TenantLineInnerInterceptor 自动处理)
13
+ - 审计字段: `created_by NVARCHAR2(64)`, `created_date DATE DEFAULT SYSDATE`, `last_updated_by NVARCHAR2(64)`, `last_updated_date DATE DEFAULT SYSDATE`
14
+ - 字符串类型统一使用 NVARCHAR2,禁止使用 VARCHAR2 或 VARCHAR
15
+ - 数值类型: 主键用 NUMBER(19),状态/标志用 NUMBER(2) 或 NUMBER(1),数量/金额用 NUMBER(18,4)
16
+ - 日期类型: DATE,默认值 DEFAULT SYSDATE
17
+ - 扩展字段: ATTRIBUTE1~ATTRIBUTE5 NVARCHAR2(150)(预留扩展,按需添加)
18
+ - 表名字段名全部小写下划线命名(snake_case),Java 中自动映射为驼峰(camelCase)
19
+
20
+ ## 数据库索引规范
21
+
22
+ - 主键索引命名: `{tableName}_PK`
23
+ - 普通索引命名: `{tableName}_N1` ~ `{tableName}_N5`
24
+ - 唯一索引命名: `{tableName}_U1` ~ `{tableName}_U5`
25
+ - 唯一索引必须以 org_id 为首列: `(org_id, 业务字段1, 业务字段2...)`
26
+ - 数据表空间: TABLESPACE MES_DATA_{WORKS|WMS|COM}(生产/仓储/通用)
27
+ - 索引表空间: USING INDEX TABLESPACE MES_IDX_{WORKS|WMS|COM}
28
+ - 主键约束: `CONSTRAINT {表名}_PK PRIMARY KEY (id) USING INDEX TABLESPACE {索引表空间}`
29
+ - 每张表和每个字段必须有 COMMENT ON 注释
30
+ - 禁止使用 SELECT *,必须明确列出字段名
31
+
32
+ ## 后端 Entity 规范
33
+
34
+ - 必须继承 `com.twsz.mom.ds.model.BaseModel`(不是 com.twsz.mom.common.base.BaseModel)
35
+ - BaseModel 继承链: ViewModel → TimeModel → BaseModel,已提供 id/审计字段/查询字段
36
+ - BaseModel 已提供: id(Long, ASSIGN_ID, ToStringSerializer), createdBy, createdDate, lastUpdatedBy, lastUpdatedDate
37
+ - ViewModel 已提供: opt("1"=新增,"2"=修改,"3"=删除), columns, orderBy, fields, csv, rowNum — Entity 中禁止重复声明
38
+ - @EqualsAndHashCode(callSuper = true)(不是 false)
39
+ - @JsonInclude(JsonInclude.Include.NON_NULL)
40
+ - @TableName("表名")
41
+ - id 字段必须在 Entity 中显式声明: @TableId(type = IdType.ASSIGN_ID) + @JsonSerialize(using = ToStringSerializer.class)(覆盖 BaseModel 声明以确保序列化)
42
+ - org_id 禁止在 Entity 中声明(TenantLineInnerInterceptor 自动注入)
43
+ - 每个业务字段使用 @ExcelProperty("中文名") 注解(支持导入导出)
44
+ - 非数据库字段使用 @TableField(exist = false) + @ExcelIgnore
45
+ - Long 类型字段暴露给前端时必须加 @JsonSerialize(using = ToStringSerializer.class)(防止 JS 精度丢失)
46
+ - 日期输出格式化: @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss") 或 @JsonSerialize(using = LocalDateTimeSerializer.class)
47
+ - 使用 Lombok: @Data, @EqualsAndHashCode, @Builder, @NoArgsConstructor, @AllArgsConstructor
48
+ - 编号字段使用 @AutoGenCode 注解(自动生成编号)
49
+ - 主子表的子表集合字段: @TableField(exist = false) private List<DetailEntity> details
50
+
51
+ ## 后端 Controller 规范
52
+
53
+ - @RestController + @RequestMapping("/api/v1/entity-kebab-case")(含版本号 /api/v1/)
54
+ - 使用 @Autowired 或 @Resource 字段注入(项目惯例,非构造器注入)
55
+ - 统一返回 ResponseWrapper<T>(com.twsz.mom.core.common.ResponseWrapper)
56
+ - 标准方法: search(POST) / save(POST) / delete(DELETE) / list(POST) / export(POST)
57
+ - save 方法: id==null 时 insert,id!=null 时 update(Controller 层判断或 Service 层 saveModel)
58
+ - Controller 只做参数校验和结果封装,禁止写业务逻辑
59
+ - 禁止在 Controller 中 try-catch(全局异常处理器 @ControllerAdvice 统一处理)
60
+ - delete 接口: @DeleteMapping,接收 `Long[] ids`
61
+ - export 接口: 使用 ExcelUtil.export() 或 EasyExcel 直接写入 HttpServletResponse
62
+ - import 接口: 接收 MultipartFile,使用 ExcelUtil.importExcel() 或 EasyExcel.read()
63
+
64
+ ## 后端 Service 规范
65
+
66
+ - 接口继承 IService<T>,实现类继承 ServiceImpl<Mapper, Entity> 并实现接口
67
+ - @Slf4j + @Service 注解
68
+ - saveModel 方法: @Transactional(rollbackFor = Exception.class),id==null 调 insert,否则检查乐观锁后调 update
69
+ - 主子表保存模式: 先保存主表,再遍历明细列表按 opt 标记处理(1=新增, 2=修改, 3=删除)
70
+ - saveOrUpdateOrDelete 模式: 按 ViewModel.OPT_ADD/MODIFY/DELETE 分组,批量 saveBatch/updateBatchById/delete
71
+ - 乐观锁: 编辑时查库比对 lastUpdatedDate(withNano(0) 去掉纳秒),不匹配则 throw new BaseException
72
+ - 异常抛出: `throw new BaseException(AIHttpStatus.XXX.getCode(), AIHttpStatus.XXX.getMessage())`
73
+ - 带参数占位: `throw new BaseException(AIHttpStatus.XXX.getCode(), AIHttpStatus.XXX.getMessage(), param1, param2)`
74
+ - AIHttpStatus 新增 code 规则: 取 AIHttpStatus 枚举中已有 code 最大值 + 1(maxCode + 1),禁止硬编码数字
75
+ - AIHttpStatus 枚举命名: 中文提示的缩写,如 "类型不能为空" → TYPE_NOT_EMPTY
76
+ - 禁止在 Service 中手动设置 orgId(多租户插件自动注入)
77
+ - 禁止在循环中执行数据库查询(N+1 问题),必须批量查询后内存处理
78
+ - 新增/修改操作后必须清除相关 Redis 缓存
79
+ - 使用 LambdaQueryWrapper / LambdaUpdateWrapper,禁止硬编码字符串字段名
80
+ - @Transactional 写在 ServiceImpl 方法上,不写在接口上
81
+
82
+ ## 后端 Mapper XML 规范
83
+
84
+ - 使用 <sql id="EntityColumns"> 定义字段列表(t.前缀),支持 entity.columns 动态列选择,默认 t.*
85
+ - 使用 <sql id="EntityWhere"> 定义条件(动态 <if> 判断非空),字符串检查 `!= null and != ''`,数值/日期只检查 `!= null`
86
+ - 使用 <sql id="EntityJoins"> 定义关联(LEFT JOIN),默认为空
87
+ - 分页查询: IPage<Entity> pageSearch(Page<Entity> p, @Param("entity") Entity entity)
88
+ - list 查询: 使用 Oracle rownum 限制行数,默认 10000,通过 entity.rowNum 可配置
89
+ - Oracle 字符串拼接用 || 而非 CONCAT: `LIKE '%' || #{entity.field} || '%'`
90
+ - CDATA 用于特殊字符: `<![CDATA[<=]]>` for rownum <=
91
+ - 排序默认: ORDER BY t.id DESC,支持 entity.orderBy 动态排序
92
+ - XML 中不包含 org_id 列和 org_id 条件(多租户插件自动注入)
93
+ - 逻辑删除过滤: t.is_deleted = 0 或 t.is_delete = 0(如适用)
94
+ - 表别名统一使用 t
95
+ - XML 文件位置: src/main/resources/mapper/{submodule}/EntityMapper.xml
96
+
97
+ ## 后端公共类引用
98
+
99
+ - ResponseWrapper: ofSuccess(data) / ofFail(msg) / defaultSuccess() / ofStatus(AIHttpStatus) — com.twsz.mom.core.common.ResponseWrapper
100
+ - AIHttpStatus: 枚举类,code 从已有最大值+1 递增(maxCode+1),禁止硬编码 — com.twsz.mom.core.common.AIHttpStatus
101
+ - BaseException: `new BaseException(AIHttpStatus.XXX.getCode(), AIHttpStatus.XXX.getMessage())` — com.twsz.mom.core.exception.BaseException
102
+ - PageForm<T>: size(页大小), current(页码,1-based), condition(查询条件对象) — com.twsz.mom.core.common.PageForm
103
+ - SecurityUtils: 获取当前用户信息 — com.twsz.mom.core.utils.SecurityUtils
104
+ - ExcelUtil: importExcel/export — com.twsz.mom.core.utils.ExcelUtil
105
+ - EasyExcel: com.alibaba.excel.EasyExcel
106
+ - ViewModel.OPT_ADD = "1", OPT_MODIFY = "2", OPT_DELETE = "3", OPT_NOT_MODIFY_DATE = "0"
107
+ - IdUtil: cn.hutool.core.util.IdUtil(fastSimpleUUID 等)
108
+
109
+ ## 前端 API 规范
110
+
111
+ - 使用 `import axios from '@/libs/request'`
112
+ - 导出使用 `import { exportExcel } from '@/api/file'` 或 `import { exportExcel } from '../../file'`
113
+ - API 文件放在 `src/api/{domain}/{entity}.js`
114
+ - 标准方法: save, deleteById, list, search, exportData
115
+ - save: POST, data 为实体对象
116
+ - deleteById: DELETE, data 为数组 `Array.isArray(id) ? id : [id]`
117
+ - search: POST, data 为 PageForm 对象
118
+ - list: POST, data 为查询条件对象
119
+ - exportData: 调用 exportExcel(url, data)
120
+ - URL 前缀根据模块不同有多种模式: `/mm/{entity}`, `/api/{entity}`, `/mm/api/{sub}/{entity}`
121
+ - 导入接口: `{ headers: { 'Content-Type': 'multipart/form-data' } }`
122
+
123
+ ## 前端列表页规范(index.vue)
124
+
125
+ - 使用 indexPage mixin: `import { indexPage } from '_c/table-form/index-mixin'`
126
+ - 使用 tw-table 组件(非 search-table)
127
+ - option 配置: tableName(唯一标识), searchForm, fixedTableHeight, add/delete/edit/view/search/export
128
+ - option.add: `{ enable: true, method: this.add }`
129
+ - option.delete: `{ enable: true|fn(row), method: Api.deleteById, before: fn(rows) }`
130
+ - option.edit: `{ enable: true|fn(row), method: this.edit }`
131
+ - option.view: `{ enable: true, method: this.view }`
132
+ - option.search: `{ query: Api.search }`
133
+ - option.export: `{ enable: true, method: Api.exportData }`
134
+ - 表格列顺序: selection → 业务列 → 审计列(createdBy/createdDate/lastUpdatedBy/lastUpdatedDate) → operate
135
+ - 搜索条件: `<Form ref="conditionForm" class="search-condition-form" label-colon @keyup.enter.native="refresh" @submit.native.prevent>`
136
+ - 国际化: $t('key||中文'),如 $t('table.entityName.fieldName||字段中文名')
137
+ - 操作列: `<template slot-scope="{ row }" slot="operate">` 内含编辑/查看/删除按钮
138
+ - 权限控制: v-if="$auth('permission:key')"
139
+ - 列表/表单切换: showIndexPage 控制,triggerBack(false) 切换到表单,triggerBack(true) 回到列表
140
+
141
+ ## 前端表单页规范(form.vue)
142
+
143
+ - 使用 BaseMixin: `import BaseMixin from '@/mixin'`,mixins: [BaseMixin]
144
+ - props: readonly(Boolean), form(Object)
145
+ - data: `{ default: {}, mform: {} }`,created 中 `this.mform = Object.assign({}, this.default, this.form)`
146
+ - 使用 master-sub 组件(主子表布局):
147
+ - `<template #master-header-toolbar>` — 提交按钮
148
+ - `<template #master-form>` — 主表 Form
149
+ - `<template #sub-table>` — 子表组件
150
+ - 表单提交: doSubmit() → this.$refs.mform.validate() → Api.save(this.mform) → $emit('on-success', data)
151
+ - 按钮状态: `<Button type="primary" :loading="loading" @click="doSubmit">`
152
+ - 只读模式: `<Form :disabled="readonly">`, 按钮 `v-if="!readonly"`
153
+ - 主子表提交: `this.asyncLoading(this.$refs.SubTableForm.save())` 委托子表 save()
154
+
155
+ ## 前端子表规范(table-form.vue)
156
+
157
+ - 使用 tw-table: `<tw-table ref="searchTable" :option="option" :mainItem="mainItem" :columns="tableColumns" :tableRules="rules" :readonly="readonly">`
158
+ - option.save: 指向主表 Api.save(saveTable() 时使用)
159
+ - option.search.beforeQuery: `this.searchBefore` — 注入 mainId 过滤
160
+ - searchBefore(searchParam): `Object.assign(searchParam.condition, { mainId: this.mainItem.id }); return Boolean(this.mainItem.id)`
161
+ - 行内编辑 slot: `<template slot-scope="{ row, column, index, tableData, editIndex }" slot="input">`
162
+ - 编辑条件: `v-if="index === editIndex && !(column.disabled && Boolean(row.id))"`
163
+ - 必填列: `className: 'table-column-required'`
164
+ - 只读列: `readonly: true, disabled: 'true'`(审计字段等)
165
+ - tableRules: 定义每列校验规则 `{ fieldName: { required: true, type: 'string', message: '...' } }`
166
+ - save() 方法: 自定义校验 → `this.$refs.searchTable.saveTable()`
167
+ - opt 标记: 新增行 opt='1',修改行 opt='2',删除行 opt='3'
168
+
169
+ ## 测试规范
170
+
171
+ - 测试类名: 原类名 + MockTest(如 PcCadDataServiceImpl → PcCadDataServiceImplMockTest)
172
+ - 框架: JUnit 5 + Mockito(@ExtendWith(MockitoExtension.class),不是 @RunWith)
173
+ - @Mock 模拟依赖(Mapper、其他 Service)
174
+ - @InjectMocks 注入被测类
175
+ - setUp 中使用 ReflectionTestUtils.setField(entityService, "baseMapper", entityMapper) 注入 baseMapper
176
+ - 使用 @Nested 类按方法分组测试
177
+ - 使用 @DisplayName 描述测试场景(中文)
178
+ - 测试方法命名: testMethod_scenario() 或 should_预期行为_when_条件()
179
+ - 每个测试方法只测试一个行为
180
+ - Mock 外部依赖(Feign 调用、Redis、数据库),不依赖外部环境
181
+ - Arrange-Act-Assert 模式: when() → 调用方法 → assertEquals/verify
182
+ - 执行命令: mvn test -pl <module> -Dtest=ClassNameMockTest
183
+
184
+ ## Java 通用编码规范
185
+
186
+ - 必须使用 Java 8 语法,禁止 Java 9+ 特性(var、模块化、HttpClient)
187
+ - 禁止 System.out.println,必须使用 @Slf4j + log.info/warn/error
188
+ - 禁止硬编码密码、密钥、连接串(使用 Jasypt 加密或 Nacos 配置中心)
189
+ - 禁止硬编码 IP 地址和端口
190
+ - 方法体不超过 80 行,超过必须拆分子方法
191
+ - 所有公共方法必须添加 Javadoc 注释
192
+ - 使用 4 空格缩进,禁止 Tab
193
+ - 禁止在 for 循环中发起数据库查询或远程调用
194
+
195
+ ## Vue 通用编码规范
196
+
197
+ - 必须使用 Vue 2 Options API(data/methods/computed/watch)
198
+ - 禁止使用 Vue 3 Composition API(setup/ref/reactive)
199
+ - 组件 name 属性必须与文件名一致(PascalCase)
200
+ - props 必须定义 type 和 default
201
+ - 禁止直接修改 props,必须通过 $emit 通知父组件
202
+ - 统一使用 View Design 4.x 组件,禁止引入其他 UI 库
203
+ - 全局状态使用 Vuex,禁止在组件中直接修改 state
204
+ - 组件库: tw-table, master-sub, tw-card, RemoteSelect, TableSelect, RemoteCheckbox 等全局注册组件