20 KiB
20 KiB
name, description, status, version, date
| name | description | status | version | date |
|---|---|---|---|---|
| mica-smdm-scaffold | mica 项目 SMDM 模块骨架代码生成规范(含数据字典填充) | proposal | v2 | 2026-08-04T09:09:00.667Z |
mica SMDM 模块骨架代码生成技能
技能定位
为 mica 项目的主数据管理模块 (SMDM) 生成标准化的骨架代码,包含完整的 CRUD 功能、数据字典填充、单元测试和集成测试。
适用范围
- mica 项目的主数据管理模块开发
- 需要 Feign 远程调用 SMDM 服务的场景
- 需要数据字典翻译的业务模块
🔴 强制性红线
SMDM 物料管理模块开发限制
| # | 红线 | 违规示例 | 正确做法 |
|---|---|---|---|
| 1 | 表前缀必须过滤 | DmpMdItemInfo |
ItemInfo |
| 2 | API 必须放在 apis.smdm 包 | smd.api.ItemApi |
apis.smdm.ItemApi |
| 3 | 查询只能用 Mapper | 使用 MyBatis-Plus | 原生 MyBatis XML |
| 4 | 改删必须 Feign 远程调用 | 本地直接 UPDATE/DELETE | 调用 SMDM 服务 API |
| 5 | 实体必须继承 BaseDomain | 独立定义审计字段 | extends BaseDomain |
| 6 | 只使用指定业务字段 | 添加表中其他字段 | 仅用规范内字段 |
| 7 | 代码必须生成到 smd 目录 | com.witsoft.mica.item.* |
com.witsoft.mica.smd.* |
数据字典模块开发限制
| # | 红线 | 正确做法 |
|---|---|---|
| 1 | 不需要 Controller | 纯内部工具服务 |
| 2 | 不需要 Feign API | 不暴露外部接口 |
| 3 | 不需要缓存实现 | 后续可调用 Feign 接口缓存 |
| 4 | 不需要继承 BaseDomain | 配置项不是业务实体 |
📋 核心规范摘要
包路径规范
- 本地业务:
com.witsoft.mica.smd.* - Feign 接口:
com.witsoft.mica.apis.smdm.*
表前缀过滤
dmp_md_全部过滤 (如dmp_md_item_info→ItemInfo)
查询方式
- 分页查询: 原生 MyBatis XML
- 详情查询: MyBatis-Plus
selectById()
改删操作
- Feign 远程调用 SMDM 服务
注释规范
@Author: yangxuan@Date: 精确到日
日志规范
- SLF4J + Lombok
@Slf4j - Feign 调用添加 debug 日志(记录入参和耗时,禁止记录返回值)
ecid 处理
- Controller 层调用
GlobalUtils.getEcid()并传递给 Service
分页方式
- 使用项目
PageDomain<T> - 不使用
com.github.pagehelper
XML 规范
- 使用
<sql>+<include>片段复用方式
📁 标准文件清单
第 1 部分:数据字典模块 (基础设施)
| 文件 | 说明 | 路径 |
|---|---|---|
DictionaryMapper.java |
字典查询 Mapper 接口 | smd/mapper/ |
DictionaryMapper.xml |
原生 SQL 查询 | resources/mapper/smd/ |
DictionaryItem.java |
字典数据项 DTO | smd/dto/ |
DictionaryService.java |
字典服务接口 | smd/service/ |
DictionaryServiceImpl.java |
字典服务实现 | smd/service/impl/ |
第 2 部分:枚举类 (按需)
| 文件 | 说明 |
|---|---|
StatusEnum.java |
状态枚举 (Y=启用,N=禁用) |
YesNoEnum.java |
是否枚举 (Y=是,N=否) |
第 3 部分:业务模块 (10 个文件)
| 文件 | 说明 | 路径 |
|---|---|---|
XxxApi.java |
Feign 远程调用接口 | apis/smdm/ |
XxxInfo.java |
实体类 | smd/entity/ |
XxxMapper.java |
Mapper 接口 | smd/mapper/ |
XxxMapper.xml |
MyBatis XML 映射 | resources/mapper/smd/ |
XxxService.java |
服务接口 | smd/service/ |
XxxServiceImpl.java |
服务实现 | smd/service/impl/ |
XxxController.java |
控制器 | smd/controller/ |
XxxQueryDTO.java |
查询 DTO | smd/dto/ |
XxxFormDTO.java |
表单 DTO | smd/dto/ |
XxxVO.java |
视图对象 | smd/vo/ |
第 4 部分:测试文件 (扩展模块)
| 文件 | 说明 | 路径 |
|---|---|---|
XxxServiceTest.java |
Service 层单元测试 | src/test/java/com/witsoft/mica/smd/service/ |
XxxControllerIntegrationTest.java |
Controller 集成测试 | src/test/java/com/witsoft/mica/smd/controller/ |
🔧 数据字典填充规范
VO 设计
- 添加
xxxName字段存储字典翻译后的中文名称 - 示例:
itemType(en_code) +itemTypeName(中文名称)
Service 层填充
- 批量查询字典: 一次查询多个字典类型,避免 N+1 问题
- 通用填充方法:
fillDictionaryData(ItemVO itemVO, Map<String, Map<String, String>> dictMaps) - 性能要求: 列表查询只查 1 次字典,批量填充
填充示例代码
@Override
public PageDomain<ItemVO> queryPageList(ItemQueryDTO dto) {
// 查询分页数据
PageDomain<ItemVO> page = itemMapper.queryPageList(dto);
// 只查 1 次字典(批量查询)
Map<String, Map<String, String>> dictMaps = dictionaryService.getDictionaryMaps(
Arrays.asList("materialsType", "itemProperties", "pickingProperty")
);
// 遍历列表填充
page.getList().forEach(item -> fillDictionaryData(item, dictMaps));
return page;
}
@Override
public ItemVO queryById(String id) {
ItemVO itemVO = itemMapper.selectById(id);
// 查 1 次字典
Map<String, Map<String, String>> dictMaps = dictionaryService.getDictionaryMaps(
Arrays.asList("materialsType", "itemProperties", "pickingProperty")
);
fillDictionaryData(itemVO, dictMaps);
return itemVO;
}
/**
* 填充物料字典数据
* @param itemVO 物料 VO 对象
* @param dictMaps 已查询的字典 Map(外部传入,避免重复查询)
*/
private void fillDictionaryData(ItemVO itemVO, Map<String, Map<String, String>> dictMaps) {
if (itemVO == null || dictMaps == null) return;
// 物料类型
Map<String, String> materialsMap = dictMaps.get("materialsType");
if (materialsMap != null) {
itemVO.setItemTypeName(materialsMap.getOrDefault(itemVO.getItemType(), itemVO.getItemType()));
}
// 物料属性
Map<String, String> propertiesMap = dictMaps.get("itemProperties");
if (propertiesMap != null) {
itemVO.setPropertiesName(propertiesMap.getOrDefault(itemVO.getProperties(), itemVO.getProperties()));
}
// 领料属性
Map<String, String> pickingMap = dictMaps.get("pickingProperty");
if (pickingMap != null) {
itemVO.setPickingPropertyName(pickingMap.getOrDefault(itemVO.getPickingProperty(), itemVO.getPickingProperty()));
}
// 状态(枚举)
itemVO.setStatusName(StatusEnum.getNameByCode(itemVO.getStatus()));
}
🧪 测试生成规范
单元测试 (Service 层)
文件位置
src/test/java/com/witsoft/mica/smd/service/*ServiceTest.java
测试类结构模板
package com.witsoft.mica.smd.service;
import com.witsoft.gen.base.common.ResponseModel;
import com.witsoft.gen.base.page.PageDomain;
import com.witsoft.mica.smd.dto.ItemQueryDTO;
import com.witsoft.mica.smd.entity.ItemInfo;
import com.witsoft.mica.smd.mapper.ItemMapper;
import com.witsoft.mica.smd.service.impl.ItemServiceImpl;
import com.witsoft.mica.apis.smdm.ItemApi;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {
@Mock
private ItemMapper itemMapper;
@Mock
private ItemApi itemApi;
@InjectMocks
private ItemServiceImpl itemService;
@Test
void testQueryPageList_normalQuery() {
// 准备测试数据
ItemQueryDTO dto = new ItemQueryDTO();
dto.setEcid("test-ecid");
dto.setPageNo(1);
dto.setPageSize(10);
List<ItemInfo> mockList = new ArrayList<>();
ItemInfo item = new ItemInfo();
item.setId("test-id");
item.setItemCode("TEST001");
mockList.add(item);
when(itemMapper.queryPageCount(any())).thenReturn(1L);
when(itemMapper.queryPageList(any())).thenReturn(mockList);
// 执行测试
PageDomain<ItemVO> result = itemService.queryPageList(dto);
// 验证结果
assertNotNull(result);
assertEquals(1, result.getList().size());
assertEquals(1L, result.getTotal());
}
@Test
void testQueryById_notFound() {
when(itemMapper.selectById("not-exist")).thenReturn(null);
ItemVO result = itemService.queryById("not-exist");
assertNull(result);
}
@Test
void testCreateItem_success() {
ItemFormDTO dto = new ItemFormDTO();
dto.setItemCode("TEST001");
dto.setItemName("测试物料");
when(itemApi.createItem(any())).thenReturn(ResponseModel.succeed(null));
ResponseModel result = itemService.insertItem(dto);
assertNotNull(result);
assertEquals(200, result.getCode());
}
@Test
void testCreateItem_nullParam() {
ResponseModel result = itemService.insertItem(null);
assertNotNull(result);
assertEquals(500, result.getCode());
assertTrue(result.getMsg().contains("参数不能为空"));
}
@Test
void testUpdateItem_success() {
ItemFormDTO dto = new ItemFormDTO();
dto.setId("test-id");
dto.setItemCode("TEST001");
when(itemApi.updateItem(any())).thenReturn(ResponseModel.succeed(null));
ResponseModel result = itemService.updateItem(dto);
assertNotNull(result);
assertEquals(200, result.getCode());
}
@Test
void testDeleteItem_success() {
when(itemApi.deleteItem(any())).thenReturn(ResponseModel.succeed(null));
ResponseModel result = itemService.deleteItem("test-id");
assertNotNull(result);
assertEquals(200, result.getCode());
}
}
测试方法清单
| 方法 | 测试场景 | 断言 |
|---|---|---|
testQueryPageList_normalQuery |
正常分页查询 | 返回非空,size>0 |
testQueryPageList_emptyResult |
无数据 | 返回空列表 |
testQueryById_notFound |
ID 不存在 | 返回 null |
testQueryById_found |
ID 存在 | 返回非空 VO |
testCreateItem_success |
创建成功 | 返回 succeed |
testCreateItem_nullParam |
空参数 | 返回 failed |
testUpdateItem_success |
更新成功 | 返回 succeed |
testUpdateItem_nullId |
空 ID | 返回 failed |
testDeleteItem_success |
删除成功 | 返回 succeed |
testDeleteItem_nullId |
空 ID | 返回 failed |
Mock 处理规范
GlobalUtils.getEcid()→ 改为实例方法或传入参数- Feign API →
@Mock+when().thenReturn() - Mapper →
@Mock+ 返回测试数据 - 使用
ArgumentMatchers.any()匹配任意参数
集成测试 (Controller 层)
文件位置
src/test/java/com/witsoft/mica/smd/controller/*ControllerIntegrationTest.java
测试类结构模板
package com.witsoft.mica.smd.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.witsoft.mica.smd.dto.ItemFormDTO;
import com.witsoft.mica.smd.dto.ItemQueryDTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class ItemControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
// 准备测试数据(可选)
}
@Test
void testQueryPageList_integration() throws Exception {
ItemQueryDTO dto = new ItemQueryDTO();
dto.setPageNo(1);
dto.setPageSize(10);
mockMvc.perform(post("/web/itemInfo/getPageList")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").exists())
.andExpect(jsonPath("$.data.list").isArray());
}
@Test
void testQueryById_integration() throws Exception {
mockMvc.perform(get("/web/itemInfo/getById")
.param("id", "test-id"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
void testCreateItem_integration() throws Exception {
ItemFormDTO dto = new ItemFormDTO();
dto.setItemCode("TEST001");
dto.setItemName("测试物料");
dto.setStatus("Y");
mockMvc.perform(post("/web/itemInfo/create")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
void testUpdateItem_integration() throws Exception {
ItemFormDTO dto = new ItemFormDTO();
dto.setId("test-id");
dto.setItemCode("TEST001");
dto.setItemName("测试物料更新");
mockMvc.perform(post("/web/itemInfo/update")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
void testDeleteItem_integration() throws Exception {
mockMvc.perform(post("/web/itemInfo/delete")
.param("id", "test-id"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
void testAuth_ecidRequired() throws Exception {
// 测试没有 ecid 时的权限验证(如果配置了拦截器)
ItemQueryDTO dto = new ItemQueryDTO();
mockMvc.perform(post("/web/itemInfo/getPageList")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isOk());
// 根据实际权限配置调整断言
}
}
测试场景清单
| 方法 | 测试场景 | 验证点 |
|---|---|---|
testQueryPageList_integration |
完整查询流程 | HTTP 200 + 返回结构 |
testQueryById_integration |
详情查询 | HTTP 200 + 数据存在 |
testCreateItem_integration |
创建流程 | HTTP 200 + 数据入库 |
testUpdateItem_integration |
更新流程 | HTTP 200 + 数据更新 |
testDeleteItem_integration |
删除流程 | HTTP 200 + 数据删除 |
testAuth_ecidRequired |
权限验证 | 无 ecid 时拒绝 |
测试数据准备
@BeforeEach插入测试数据(可选)@AfterEach清理测试数据(可选)- 使用
@Transactional自动回滚(推荐)
测试生成触发条件
| 场景 | 单元测试 | 集成测试 |
|---|---|---|
| 新增业务模块 | ✅ 必选 | ✅ 推荐 |
| 新增字典模块 | ✅ 必选 | ⏸️ 可选 |
| 修改核心逻辑 | ✅ 必选 | ⏸️ 可选 |
| 修复 Bug | ✅ 添加回归测试 | ⏸️ 可选 |
| 性能优化 | ⏸️ 可选 | ✅ 必选 |
🐛 常见问题修正清单
| # | 问题 | 修正方案 |
|---|---|---|
| 1 | ResponseModel 静态引用 | ResponseModel.succeed(data) |
| 2 | 分页 XML / 详情 MP 混合使用 | 分页用 XML,详情用 MP |
| 3 | 编码查询不需要 | 移除编码查询条件 |
| 4 | Feign 调用缺少日志 | 添加入参和耗时日志(debug 级别) |
| 5 | Controller 层 ecid 处理 | Controller 层获取并传递 |
| 6 | 分页 XML 查询被删除 | 恢复分页 XML 查询 |
| 7 | 使用 PageHelper | 改用 PageDomain |
| 8 | queryByCode 方法不需要 | 删除 |
| 9 | ResponseModel 泛型参数化 | ResponseModel<T> |
| 10 | Map 类型转换警告 | 添加 @SuppressWarnings("unchecked") |
| 11 | XML 不方便联查 | 使用 <sql> + <include> 片段 |
| 12 | ItemApi ResponseModel 静态引用 | 泛型参数化 |
| 13 | ResponseModel 编译报错 | 使用 ResponseModel<?> 或无参 |
| 14 | 日志记录返回值太大 | 只记录耗时,不记录返回值 |
| 15 | JsonUtils 方法不存在 | 使用 JSON.toJSONString() (fastjson) |
| 16 | fillDictionaryData 重复查询 | 接收 dictMaps 参数,避免 N+1 问题 |
| 17 | 单元测试中文字符命名 | 改为英文命名 |
| 18 | GlobalUtils 无法 Mock | 改为实例方法或传入参数 |
📊 性能优化要点
| 优化点 | 说明 | 效果 |
|---|---|---|
| 批量查询字典 | 一次查询多个字典类型 | 避免多次数据库查询 |
| 字典 Map 传入 | fillDictionaryData() 接收外部传入的 dictMaps |
避免重复查询 |
| 列表查询优化 | 100 条数据从 101 次查询降低到 2 次 | 性能提升 50 倍 |
| 枚举静态方法 | 枚举翻译使用静态方法 | 无运行时开销 |
🚀 使用流程
1. 需求分析阶段
- 确认业务模块的表结构
- 确认需要 Feign 远程调用还是本地 CRUD
- 确认涉及的数据字典类型
2. 代码生成阶段
- 生成数据字典模块 (5 个文件)
- 生成枚举类 (按需)
- 生成业务模块 (10 个文件)
- 生成单元测试 (按需)
- 生成集成测试 (按需)
3. 修正阶段
- 对照 18 个常见问题修正清单
- 运行 Maven 编译验证
- 运行单元测试
4. 验证阶段
- 编译通过
- 单元测试通过
- 集成测试通过(可选)
📝 输出物
- 完整的骨架代码(10+5+2 个文件)
- 单元测试文件(按需)
- 集成测试文件(按需)
- 编译验证报告
- 测试通过报告
🔗 相关资源
- 数据库连接:
mysql -h 47.99.209.185 -P 50036 -u witsoftd -p mica - 项目路径:
/root/projects/wit/ - 规范文档:
/root/projects/wit/mica-doc/主数据骨架代码.md - 规范文档:
/root/projects/wit/mica-doc/数据字典模块.md
🔄 技能更新机制
如何丰富这个技能
-
发现新需求/问题
- 开发过程中遇到新问题
- 用户提出新需求
- 最佳实践总结
-
添加到技能的对应扩展模块
- 新增测试类型 → 添加到"测试生成规范"
- 新增文件类型 → 添加到"标准文件清单"
- 新问题 → 添加到"常见问题修正清单"
-
更新技能提案
skill_workshop action=revise name=mica-smdm-scaffold \ proposal_content="[完整技能内容]" -
应用新版本
skill_workshop action=apply proposal_id=mica-smdm-scaffold-xxxxx -
后续开发自动使用新版本
可扩展模块示例
| 模块 | 说明 | 状态 |
|---|---|---|
| 单元测试生成 | Service 层测试模板 | ✅ 已实现 |
| 集成测试生成 | Controller 层测试模板 | ✅ 已实现 |
| API 文档生成 | Swagger 注解规范 | ⏸️ 待添加 |
| 前端代码生成 | Vue3 + TypeScript 模板 | ⏸️ 待添加 |
| 数据迁移脚本 | Flyway 迁移脚本 | ⏸️ 待添加 |
| 性能测试 | JMeter 测试脚本 | ⏸️ 待添加 |
| 部署脚本 | Docker/K8s 配置 | ⏸️ 待添加 |