--- name: "mica-smdm-scaffold" description: "mica 项目 SMDM 模块骨架代码生成规范(含数据字典填充)" --- # 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` - **不使用** `com.github.pagehelper` ### XML 规范 - 使用 `` + `` 片段复用方式 --- ## 📁 标准文件清单 ### 第 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> dictMaps)` - **性能要求**: 列表查询只查 1 次字典,批量填充 ### 填充示例代码 ```java @Override public PageDomain queryPageList(ItemQueryDTO dto) { // 查询分页数据 PageDomain page = itemMapper.queryPageList(dto); // 只查 1 次字典(批量查询) Map> 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> 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> dictMaps) { if (itemVO == null || dictMaps == null) return; // 物料类型 Map materialsMap = dictMaps.get("materialsType"); if (materialsMap != null) { itemVO.setItemTypeName(materialsMap.getOrDefault(itemVO.getItemType(), itemVO.getItemType())); } // 物料属性 Map propertiesMap = dictMaps.get("itemProperties"); if (propertiesMap != null) { itemVO.setPropertiesName(propertiesMap.getOrDefault(itemVO.getProperties(), itemVO.getProperties())); } // 领料属性 Map 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` #### 测试类结构模板 ```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 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 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` #### 测试类结构模板 ```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` | | 10 | Map 类型转换警告 | 添加 `@SuppressWarnings("unchecked")` | | 11 | XML 不方便联查 | 使用 `` + `` 片段 | | 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. 验证阶段 - 编译通过 - 单元测试通过 - 集成测试通过(可选) --- ## 📝 输出物 1. 完整的骨架代码(10+5+2 个文件) 2. 单元测试文件(按需) 3. 集成测试文件(按需) 4. 编译验证报告 5. 测试通过报告 --- ## 🔗 相关资源 - 数据库连接:`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` --- ## 🔄 技能更新机制 ### 如何丰富这个技能 1. **发现新需求/问题** - 开发过程中遇到新问题 - 用户提出新需求 - 最佳实践总结 2. **添加到技能的对应扩展模块** - 新增测试类型 → 添加到"测试生成规范" - 新增文件类型 → 添加到"标准文件清单" - 新问题 → 添加到"常见问题修正清单" 3. **更新技能提案** ```bash skill_workshop action=revise name=mica-smdm-scaffold \ proposal_content="[完整技能内容]" ``` 4. **应用新版本** ```bash skill_workshop action=apply proposal_id=mica-smdm-scaffold-xxxxx ``` 5. **后续开发自动使用新版本** ### 可扩展模块示例 | 模块 | 说明 | 状态 | |------|------|------| | 单元测试生成 | Service 层测试模板 | ✅ 已实现 | | 集成测试生成 | Controller 层测试模板 | ✅ 已实现 | | API 文档生成 | Swagger 注解规范 | ⏸️ 待添加 | | 前端代码生成 | Vue3 + TypeScript 模板 | ⏸️ 待添加 | | 数据迁移脚本 | Flyway 迁移脚本 | ⏸️ 待添加 | | 性能测试 | JMeter 测试脚本 | ⏸️ 待添加 | | 部署脚本 | Docker/K8s 配置 | ⏸️ 待添加 |