Files
openclaw-config/skill-workshop/proposals/mica-smdm-scaffold-20260804-af318dd6c2/PROPOSAL.md
T

20 KiB
Raw Blame History

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_infoItemInfo)

查询方式

  • 分页查询: 原生 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. 验证阶段

  • 编译通过
  • 单元测试通过
  • 集成测试通过(可选)

📝 输出物

  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. 更新技能提案

    skill_workshop action=revise name=mica-smdm-scaffold \
      proposal_content="[完整技能内容]"
    
  4. 应用新版本

    skill_workshop action=apply proposal_id=mica-smdm-scaffold-xxxxx
    
  5. 后续开发自动使用新版本

可扩展模块示例

模块 说明 状态
单元测试生成 Service 层测试模板 已实现
集成测试生成 Controller 层测试模板 已实现
API 文档生成 Swagger 注解规范 ⏸️ 待添加
前端代码生成 Vue3 + TypeScript 模板 ⏸️ 待添加
数据迁移脚本 Flyway 迁移脚本 ⏸️ 待添加
性能测试 JMeter 测试脚本 ⏸️ 待添加
部署脚本 Docker/K8s 配置 ⏸️ 待添加