feat: add mica-smdm-scaffold proposal and rollback files; create unit testing specifications and SKILL documentation
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"schema":"openclaw.skill-workshop.proposals-manifest.v1","updatedAt":"2026-07-09T09:56:20.877Z","proposals":[{"id":"dianping-review-20260611-d402abec10","kind":"create","status":"applied","title":"Create dianping-review","description":"通过大众点评搜索查询餐厅、老店、美食推荐和评论","skillName":"dianping-review","skillKey":"dianping-review","createdAt":"2026-06-11T23:50:15.731Z","updatedAt":"2026-07-09T09:56:20.865Z","scanState":"clean"},{"id":"calibre-cleaner-20260624-0fc374efd7","kind":"update","status":"applied","title":"Update calibre-cleaner","description":"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除","skillName":"calibre-cleaner","skillKey":"calibre-cleaner","createdAt":"2026-06-24T03:23:27.062Z","updatedAt":"2026-07-09T09:56:20.404Z","scanState":"clean"},{"id":"jellyfin-nfo-builder-20260701-6434352e0d","kind":"create","status":"applied","title":"Create jellyfin-nfo-builder","description":"为剧集整理统一文件名、生成缺失的 Jellyfin .nfo 剧集元数据","skillName":"jellyfin-nfo-builder","skillKey":"jellyfin-nfo-builder","createdAt":"2026-07-01T23:52:24.406Z","updatedAt":"2026-07-09T09:56:19.928Z","scanState":"clean"},{"id":"jav-media-sorter-20260707-b1f90c8a4d","kind":"create","status":"applied","title":"Create jav-media-sorter","description":"JAV 视频整理:组装文件、按演员分组、刮削 nfo 元数据与封面图","skillName":"jav-media-sorter","skillKey":"jav-media-sorter","createdAt":"2026-07-07T12:14:43.074Z","updatedAt":"2026-07-09T09:56:18.413Z","scanState":"clean"},{"id":"f-download-sorter-20260706-902cc0d5aa","kind":"create","status":"applied","title":"Create f-download-sorter","description":"整理 /mnt/f 下下载的视频文件:扫描→清垃圾→按规则归档到7个大类目录","skillName":"f-download-sorter","skillKey":"f-download-sorter","createdAt":"2026-07-06T09:03:54.941Z","updatedAt":"2026-07-06T09:04:18.513Z","scanState":"clean"},{"id":"weekly-report-g5-20260626-7595273f1d","kind":"create","status":"applied","title":"Create weekly-report-g5","description":"维云智造G5周报生成:整理每日工作→多段邮件(纯文本+HTML含签名图)→存入钉邮草稿箱","skillName":"weekly-report-g5","skillKey":"weekly-report-g5","createdAt":"2026-06-26T00:46:22.478Z","updatedAt":"2026-06-26T00:46:35.812Z","scanState":"clean"},{"id":"calibre-cleaner-20260624-cdbeb219a3","kind":"update","status":"applied","title":"Update calibre-cleaner","description":"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除","skillName":"calibre-cleaner","skillKey":"calibre-cleaner","createdAt":"2026-06-24T03:03:28.166Z","updatedAt":"2026-06-24T03:03:30.915Z","scanState":"clean"},{"id":"calibre-cleaner-20260624-a441798af9","kind":"create","status":"applied","title":"Create calibre-cleaner","description":"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除","skillName":"calibre-cleaner","skillKey":"calibre-cleaner","createdAt":"2026-06-24T02:29:02.278Z","updatedAt":"2026-06-24T02:29:27.227Z","scanState":"clean"},{"id":"calibre-title-cleaner-20260624-3760b64e8b","kind":"create","status":"applied","title":"Create calibre-title-cleaner","description":"清洗 Calibre 书库中损坏的 txt 文档书名(HTML标签/长文本/乱码/路径超限修复)","skillName":"calibre-title-cleaner","skillKey":"calibre-title-cleaner","createdAt":"2026-06-24T01:10:56.296Z","updatedAt":"2026-06-24T01:13:39.670Z","scanState":"clean"}]}
|
{"schema":"openclaw.skill-workshop.proposals-manifest.v1","updatedAt":"2026-08-04T09:12:42.079Z","proposals":[{"id":"mica-smdm-scaffold-20260804-af318dd6c2","kind":"create","status":"applied","title":"Create mica-smdm-scaffold","description":"mica 项目 SMDM 模块骨架代码生成规范(含数据字典填充)","skillName":"mica-smdm-scaffold","skillKey":"mica-smdm-scaffold","createdAt":"2026-08-04T08:59:50.395Z","updatedAt":"2026-08-04T09:12:42.043Z","scanState":"clean"},{"id":"dianping-review-20260611-d402abec10","kind":"create","status":"applied","title":"Create dianping-review","description":"通过大众点评搜索查询餐厅、老店、美食推荐和评论","skillName":"dianping-review","skillKey":"dianping-review","createdAt":"2026-06-11T23:50:15.731Z","updatedAt":"2026-07-09T09:56:20.865Z","scanState":"clean"},{"id":"calibre-cleaner-20260624-0fc374efd7","kind":"update","status":"applied","title":"Update calibre-cleaner","description":"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除","skillName":"calibre-cleaner","skillKey":"calibre-cleaner","createdAt":"2026-06-24T03:23:27.062Z","updatedAt":"2026-07-09T09:56:20.404Z","scanState":"clean"},{"id":"jellyfin-nfo-builder-20260701-6434352e0d","kind":"create","status":"applied","title":"Create jellyfin-nfo-builder","description":"为剧集整理统一文件名、生成缺失的 Jellyfin .nfo 剧集元数据","skillName":"jellyfin-nfo-builder","skillKey":"jellyfin-nfo-builder","createdAt":"2026-07-01T23:52:24.406Z","updatedAt":"2026-07-09T09:56:19.928Z","scanState":"clean"},{"id":"jav-media-sorter-20260707-b1f90c8a4d","kind":"create","status":"applied","title":"Create jav-media-sorter","description":"JAV 视频整理:组装文件、按演员分组、刮削 nfo 元数据与封面图","skillName":"jav-media-sorter","skillKey":"jav-media-sorter","createdAt":"2026-07-07T12:14:43.074Z","updatedAt":"2026-07-09T09:56:18.413Z","scanState":"clean"},{"id":"f-download-sorter-20260706-902cc0d5aa","kind":"create","status":"applied","title":"Create f-download-sorter","description":"整理 /mnt/f 下下载的视频文件:扫描→清垃圾→按规则归档到7个大类目录","skillName":"f-download-sorter","skillKey":"f-download-sorter","createdAt":"2026-07-06T09:03:54.941Z","updatedAt":"2026-07-06T09:04:18.513Z","scanState":"clean"},{"id":"weekly-report-g5-20260626-7595273f1d","kind":"create","status":"applied","title":"Create weekly-report-g5","description":"维云智造G5周报生成:整理每日工作→多段邮件(纯文本+HTML含签名图)→存入钉邮草稿箱","skillName":"weekly-report-g5","skillKey":"weekly-report-g5","createdAt":"2026-06-26T00:46:22.478Z","updatedAt":"2026-06-26T00:46:35.812Z","scanState":"clean"},{"id":"calibre-cleaner-20260624-cdbeb219a3","kind":"update","status":"applied","title":"Update calibre-cleaner","description":"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除","skillName":"calibre-cleaner","skillKey":"calibre-cleaner","createdAt":"2026-06-24T03:03:28.166Z","updatedAt":"2026-06-24T03:03:30.915Z","scanState":"clean"},{"id":"calibre-cleaner-20260624-a441798af9","kind":"create","status":"applied","title":"Create calibre-cleaner","description":"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除","skillName":"calibre-cleaner","skillKey":"calibre-cleaner","createdAt":"2026-06-24T02:29:02.278Z","updatedAt":"2026-06-24T02:29:27.227Z","scanState":"clean"},{"id":"calibre-title-cleaner-20260624-3760b64e8b","kind":"create","status":"applied","title":"Create calibre-title-cleaner","description":"清洗 Calibre 书库中损坏的 txt 文档书名(HTML标签/长文本/乱码/路径超限修复)","skillName":"calibre-title-cleaner","skillKey":"calibre-title-cleaner","createdAt":"2026-06-24T01:10:56.296Z","updatedAt":"2026-06-24T01:13:39.670Z","scanState":"clean"}]}
|
||||||
|
|||||||
@@ -0,0 +1,613 @@
|
|||||||
|
---
|
||||||
|
name: "mica-smdm-scaffold"
|
||||||
|
description: "mica 项目 SMDM 模块骨架代码生成规范(含数据字典填充)"
|
||||||
|
status: proposal
|
||||||
|
version: "v2"
|
||||||
|
date: "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 次字典,批量填充
|
||||||
|
|
||||||
|
### 填充示例代码
|
||||||
|
|
||||||
|
```java
|
||||||
|
@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`
|
||||||
|
|
||||||
|
#### 测试类结构模板
|
||||||
|
```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`
|
||||||
|
|
||||||
|
#### 测试类结构模板
|
||||||
|
```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<Void> 编译报错 | 使用 `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 配置 | ⏸️ 待添加 |
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema":"openclaw.skill-workshop.proposal.v1","id":"mica-smdm-scaffold-20260804-af318dd6c2","kind":"create","status":"applied","title":"Create mica-smdm-scaffold","description":"mica 项目 SMDM 模块骨架代码生成规范(含数据字典填充)","createdAt":"2026-08-04T08:59:50.395Z","updatedAt":"2026-08-04T09:12:42.043Z","createdBy":"skill-workshop","origin":{"agentId":"planner","sessionKey":"agent:planner:main","runId":"16c1345e-77e6-475b-8d0f-e25f2f5c99f1","messageId":"16c1345e-77e6-475b-8d0f-e25f2f5c99f1"},"proposedVersion":"v2","draftFile":"PROPOSAL.md","draftHash":"63dba30b3768a646bc9fd7dc3484738f7bf6cfdb80c3a58e9ebcde04a42c07e3","target":{"skillName":"mica-smdm-scaffold","skillKey":"mica-smdm-scaffold","skillDir":"/root/.openclaw/workspace-planner/skills/mica-smdm-scaffold","skillFile":"/root/.openclaw/workspace-planner/skills/mica-smdm-scaffold/SKILL.md","source":"openclaw-workspace"},"scan":{"state":"clean","scannedAt":"2026-08-04T09:12:42.015Z","critical":0,"warn":0,"info":0,"findings":[]},"appliedAt":"2026-08-04T09:12:42.043Z"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema":"openclaw.skill-workshop.rollback.v1","proposalId":"mica-smdm-scaffold-20260804-af318dd6c2","writtenAt":"2026-08-04T09:12:42.018Z","targetSkillFile":"/root/.openclaw/workspace-planner/skills/mica-smdm-scaffold/SKILL.md","action":"create"}
|
||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
openclaw-workspace-attestation:v1
|
openclaw-workspace-attestation:v1
|
||||||
2026-08-04T06:55:13.181Z
|
2026-08-04T09:52:44.904Z
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
openclaw-workspace-attestation:v1
|
openclaw-workspace-attestation:v1
|
||||||
2026-08-04T06:54:25.728Z
|
2026-08-04T09:53:35.716Z
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
- 不要在没有询问的情况下运行破坏性命令。
|
- 不要在没有询问的情况下运行破坏性命令。
|
||||||
- `trash` 优于 `rm`(可恢复总是好过永久消失)
|
- `trash` 优于 `rm`(可恢复总是好过永久消失)
|
||||||
- 有疑问时,请询问。
|
- 有疑问时,请询问。
|
||||||
|
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
||||||
|
- **禁止 git push** - 只能 commit,push 操作必须由用户手动执行
|
||||||
|
|
||||||
## 外部 vs 内部
|
## 外部 vs 内部
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
|
|
||||||
- `wit` → `/root/projects/wit`(前端 mica-web / 后端 mica-server / 文档 mica-doc)
|
- `wit` → `/root/projects/wit`(前端 mica-web / 后端 mica-server / 文档 mica-doc)
|
||||||
|
|
||||||
|
### 🔴 mica 项目 git 操作红线
|
||||||
|
|
||||||
|
- **禁止 git push** - 只能 commit,push 操作必须由用户手动执行
|
||||||
|
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
||||||
|
|
||||||
## mica 项目数据库配置
|
## mica 项目数据库配置
|
||||||
|
|
||||||
**连接信息:**
|
**连接信息:**
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
- 不要在没有询问的情况下运行破坏性命令。
|
- 不要在没有询问的情况下运行破坏性命令。
|
||||||
- `trash` 优于 `rm`(可恢复总是好过永久消失)
|
- `trash` 优于 `rm`(可恢复总是好过永久消失)
|
||||||
- 有疑问时,请询问。
|
- 有疑问时,请询问。
|
||||||
|
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
||||||
|
- **禁止 git push** - 只能 commit,push 操作必须由用户手动执行
|
||||||
|
|
||||||
## 外部 vs 内部
|
## 外部 vs 内部
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
|
|
||||||
添加任何能帮助你完成工作内容。这是你的备忘单。
|
添加任何能帮助你完成工作内容。这是你的备忘单。
|
||||||
|
|
||||||
|
### 🔴 mica 项目 git 操作红线
|
||||||
|
|
||||||
|
- **禁止 git push** - 只能 commit,push 操作必须由用户手动执行
|
||||||
|
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
||||||
|
|
||||||
### 前端项目
|
### 前端项目
|
||||||
|
|
||||||
- **erp**(erp-app)→ `/mnt/d/Projects/HBuilderProjects/erp-app/`
|
- **erp**(erp-app)→ `/mnt/d/Projects/HBuilderProjects/erp-app/`
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
- `trash` > `rm`(可恢复优于永久删除)
|
- `trash` > `rm`(可恢复优于永久删除)
|
||||||
- 有疑问时,先询问。
|
- 有疑问时,先询问。
|
||||||
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
||||||
|
- **禁止 git push** - 只能 commit,push 操作必须由用户手动执行
|
||||||
|
- **planner 禁止生成代码** - planner 仅输出技术方案文档,代码生成由 backend/frontend agent 执行
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -83,3 +83,54 @@
|
|||||||
12. ItemApi ResponseModel 泛型
|
12. ItemApi ResponseModel 泛型
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 数据字典模块 - 开发规范 (2026-08-04)
|
||||||
|
|
||||||
|
**📋 定位**: 配置项/工具类,不是业务实体
|
||||||
|
|
||||||
|
**🔴 强制性红线**:
|
||||||
|
|
||||||
|
| # | 红线 | 正确做法 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 1 | **不需要 Controller** | 纯内部工具服务 |
|
||||||
|
| 2 | **不需要 Feign API** | 不暴露外部接口 |
|
||||||
|
| 3 | **不需要缓存实现** | 后续可调用 Feign 接口缓存 |
|
||||||
|
| 4 | **不需要继承 BaseDomain** | 配置项不是业务实体 |
|
||||||
|
|
||||||
|
**📋 核心规范**:
|
||||||
|
|
||||||
|
- **包路径**: `com.witsoft.mica.smd.*`
|
||||||
|
- **功能**: 通过 `dictionary_type.en_code` 查询 `dictionary_data` 的 `en_code → full_name` 映射
|
||||||
|
- **输出格式**: `Map<String, String>` (en_code → full_name)
|
||||||
|
- **重复处理**: 同一 typeCode 下 en_code 重复时取第一条 (按 sort_code + id 排序)
|
||||||
|
- **多租户支持**: 自动获取 ecid 过滤
|
||||||
|
- **软删除过滤**: delete_mark = 0
|
||||||
|
|
||||||
|
**📁 已生成文件** (5 个):
|
||||||
|
- `smd/mapper/DictionaryMapper.java` - 字典查询 Mapper 接口
|
||||||
|
- `smd/mapper/DictionaryMapper.xml` - 原生 SQL 查询 (JOIN type + data)
|
||||||
|
- `smd/dto/DictionaryItem.java` - 字典数据项 DTO
|
||||||
|
- `smd/service/DictionaryService.java` - 字典服务接口
|
||||||
|
- `smd/service/impl/DictionaryServiceImpl.java` - 字典服务实现
|
||||||
|
|
||||||
|
**📁 已生成枚举类** (2 个):
|
||||||
|
- `smd/enums/StatusEnum.java` - 状态枚举 (Y=启用,N=禁用)
|
||||||
|
- `smd/enums/YesNoEnum.java` - 是否枚举 (Y=是,N=否)
|
||||||
|
|
||||||
|
**🔧 字典填充规范**:
|
||||||
|
|
||||||
|
- **VO 设计**: 添加 xxxName 字段存储字典翻译后的中文名称
|
||||||
|
- **Service 层填充**: 批量查询字典,避免 N+1 问题
|
||||||
|
- **通用方法**: `fillDictionaryData(ItemVO itemVO, Map<String, Map<String, String>> dictMaps)`
|
||||||
|
- **性能要求**: 列表查询只查 1 次字典,批量填充
|
||||||
|
|
||||||
|
**📊 物料信息字典映射**:
|
||||||
|
|
||||||
|
| VO 字段 | 字典类型 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| `itemTypeName` | `materialsType` | 物料类型名称 |
|
||||||
|
| `propertiesName` | `itemProperties` | 物料属性名称 |
|
||||||
|
| `pickingPropertyName` | `pickingProperty` | 领料属性名称 |
|
||||||
|
| `statusName` | `StatusEnum` | 状态名称 (枚举) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ Add whatever helps you do your job. This is your cheat sheet.
|
|||||||
- **类型:** MySQL
|
- **类型:** MySQL
|
||||||
- **连接字符串:** `mysql://witsoftd:***@47.99.209.185:50036/mica`
|
- **连接字符串:** `mysql://witsoftd:***@47.99.209.185:50036/mica`
|
||||||
|
|
||||||
|
### 🔴 mica 项目 git 操作红线
|
||||||
|
|
||||||
|
- **禁止 git push** - 只能 commit,push 操作必须由用户手动执行
|
||||||
|
- **禁止擅自提交项目代码到 git** - 必须先询问用户确认后再提交
|
||||||
|
|
||||||
### sql-toolkit 技能使用
|
### sql-toolkit 技能使用
|
||||||
|
|
||||||
**技能位置:** `/root/.openclaw/workspace-backend/skills/sql-toolkit/SKILL.md`
|
**技能位置:** `/root/.openclaw/workspace-backend/skills/sql-toolkit/SKILL.md`
|
||||||
|
|||||||
@@ -0,0 +1,544 @@
|
|||||||
|
# mica 项目单元测试规范
|
||||||
|
|
||||||
|
## 1. 测试框架与依赖
|
||||||
|
|
||||||
|
### 1.1 核心依赖
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Spring Boot Test (包含 JUnit 5, Mockito, AssertJ) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Mockito Inline (支持静态方法 mock) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-inline</artifactId>
|
||||||
|
<version>4.6.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 测试目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/test/
|
||||||
|
├── java/
|
||||||
|
│ └── com/witsoft/mica/
|
||||||
|
│ ├── smd/
|
||||||
|
│ │ ├── controller/
|
||||||
|
│ │ │ └── ItemControllerTest.java
|
||||||
|
│ │ ├── service/
|
||||||
|
│ │ │ └── ItemServiceTest.java
|
||||||
|
│ │ └── mapper/
|
||||||
|
│ │ └── ItemMapperTest.java
|
||||||
|
│ └── ...
|
||||||
|
└── resources/
|
||||||
|
└── application-test.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Controller 层测试规范
|
||||||
|
|
||||||
|
### 2.1 测试类结构
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.witsoft.mica.smd.controller;
|
||||||
|
|
||||||
|
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.service.ItemService;
|
||||||
|
import com.witsoft.mica.smd.vo.ItemVO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
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.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("物料信息管理 Controller 测试")
|
||||||
|
class ItemControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ItemService itemService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ItemController itemController;
|
||||||
|
|
||||||
|
private ItemQueryDTO queryDTO;
|
||||||
|
private PageDomain<ItemVO> pageDomain;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// 准备测试数据
|
||||||
|
queryDTO = new ItemQueryDTO();
|
||||||
|
queryDTO.setPageNo(1);
|
||||||
|
queryDTO.setPageSize(10);
|
||||||
|
queryDTO.setEcid("test-ecid-001");
|
||||||
|
|
||||||
|
pageDomain = new PageDomain<>(1, 10, 1);
|
||||||
|
List<ItemVO> list = new ArrayList<>();
|
||||||
|
ItemVO itemVO = new ItemVO();
|
||||||
|
itemVO.setId("1");
|
||||||
|
itemVO.setItemCode("ITEM001");
|
||||||
|
list.add(itemVO);
|
||||||
|
pageDomain.setList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("分页查询 - 成功")
|
||||||
|
void testQueryPageListSuccess() {
|
||||||
|
// Arrange
|
||||||
|
when(itemService.queryPageList(any(ItemQueryDTO.class))).thenReturn(pageDomain);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseModel<PageDomain<ItemVO>> response = itemController.queryPageList(queryDTO);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(response);
|
||||||
|
assertTrue(response.isSuccess());
|
||||||
|
assertNotNull(response.getData());
|
||||||
|
assertEquals(1, response.getData().getTotal());
|
||||||
|
|
||||||
|
verify(itemService, times(1)).queryPageList(any(ItemQueryDTO.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 测试要点
|
||||||
|
|
||||||
|
#### ✅ 必须测试的场景
|
||||||
|
|
||||||
|
1. **成功场景** - 正常业务流程
|
||||||
|
2. **参数校验** - 空值、非法值处理
|
||||||
|
3. **异常处理** - Service 层异常捕获
|
||||||
|
4. **边界条件** - 空列表、null 值
|
||||||
|
|
||||||
|
#### ⚠️ 项目特殊挑战
|
||||||
|
|
||||||
|
由于项目使用了 `GlobalUtils.getEcid()` 和 `ResponseModel.succeed()` 等依赖 Spring 上下文的静态方法,纯单元测试会遇到困难。
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
**方案 A:集成测试(推荐)**
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
class ItemControllerIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testQueryPageList() throws Exception {
|
||||||
|
mockMvc.perform(post("/web/itemInfo/getPageList")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"pageNo\":1,\"pageSize\":10}"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.success").value(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**方案 B:Service 层单元测试(优先)**
|
||||||
|
- Controller 层逻辑简单,主要通过集成测试覆盖
|
||||||
|
- Service 层使用 Mockito 进行纯单元测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Service 层测试规范
|
||||||
|
|
||||||
|
### 3.1 测试类结构
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.witsoft.mica.smd.service;
|
||||||
|
|
||||||
|
import com.witsoft.gen.base.page.PageDomain;
|
||||||
|
import com.witsoft.mica.smd.dto.ItemQueryDTO;
|
||||||
|
import com.witsoft.mica.smd.mapper.ItemMapper;
|
||||||
|
import com.witsoft.mica.smd.vo.ItemVO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
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.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("物料信息服务测试")
|
||||||
|
class ItemServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ItemMapper itemMapper;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ItemServiceImpl itemService;
|
||||||
|
|
||||||
|
private ItemQueryDTO queryDTO;
|
||||||
|
private List<ItemVO> mockList;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
queryDTO = new ItemQueryDTO();
|
||||||
|
queryDTO.setPageNo(1);
|
||||||
|
queryDTO.setPageSize(10);
|
||||||
|
|
||||||
|
mockList = new ArrayList<>();
|
||||||
|
ItemVO item = new ItemVO();
|
||||||
|
item.setId("1");
|
||||||
|
item.setItemCode("ITEM001");
|
||||||
|
mockList.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("分页查询 - 成功")
|
||||||
|
void testQueryPageListSuccess() {
|
||||||
|
// Arrange
|
||||||
|
when(itemMapper.queryPageCount(any(ItemQueryDTO.class))).thenReturn(1L);
|
||||||
|
when(itemMapper.queryPageList(any(ItemQueryDTO.class))).thenReturn(mockList);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
PageDomain<ItemVO> result = itemService.queryPageList(queryDTO);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(1, result.getTotal());
|
||||||
|
assertEquals(1, result.getList().size());
|
||||||
|
assertEquals("ITEM001", result.getList().get(0).getItemCode());
|
||||||
|
|
||||||
|
verify(itemMapper, times(1)).queryPageCount(any(ItemQueryDTO.class));
|
||||||
|
verify(itemMapper, times(1)).queryPageList(any(ItemQueryDTO.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("分页查询 - 无数据")
|
||||||
|
void testQueryPageListEmpty() {
|
||||||
|
// Arrange
|
||||||
|
when(itemMapper.queryPageCount(any(ItemQueryDTO.class))).thenReturn(0L);
|
||||||
|
when(itemMapper.queryPageList(any(ItemQueryDTO.class))).thenReturn(new ArrayList<>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
PageDomain<ItemVO> result = itemService.queryPageList(queryDTO);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(0, result.getTotal());
|
||||||
|
assertTrue(result.getList().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("详情查询 - 成功")
|
||||||
|
void testQueryByIdSuccess() {
|
||||||
|
// Arrange
|
||||||
|
String testId = "123";
|
||||||
|
ItemVO mockItem = new ItemVO();
|
||||||
|
mockItem.setId(testId);
|
||||||
|
mockItem.setItemCode("ITEM001");
|
||||||
|
|
||||||
|
when(itemMapper.selectById(testId)).thenReturn(mockItem);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ItemVO result = itemService.queryById(testId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(testId, result.getId());
|
||||||
|
assertEquals("ITEM001", result.getItemCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("详情查询 - 不存在")
|
||||||
|
void testQueryByIdNotFound() {
|
||||||
|
// Arrange
|
||||||
|
when(itemMapper.selectById("not-exist")).thenReturn(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ItemVO result = itemService.queryById("not-exist");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNull(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Feign 调用测试
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
@DisplayName("新增物料 - Feign 调用成功")
|
||||||
|
void testInsertItemSuccess() {
|
||||||
|
// Arrange
|
||||||
|
ItemFormDTO dto = new ItemFormDTO();
|
||||||
|
dto.setItemCode("ITEM001");
|
||||||
|
dto.setItemName("测试物料");
|
||||||
|
|
||||||
|
ResponseModel mockResponse = ResponseModel.succeed(null);
|
||||||
|
when(itemApi.insertItem(any(Map.class))).thenReturn(mockResponse);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseModel result = itemService.insertItem(dto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isSuccess());
|
||||||
|
|
||||||
|
verify(itemApi, times(1)).insertItem(any(Map.class));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Mapper 层测试规范
|
||||||
|
|
||||||
|
### 4.1 使用 MyBatis Spring Boot Test
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mybatis.spring.boot</groupId>
|
||||||
|
<artifactId>mybatis-spring-boot-starter-test</artifactId>
|
||||||
|
<version>2.3.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 测试类结构
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.witsoft.mica.smd.mapper;
|
||||||
|
|
||||||
|
import com.witsoft.mica.smd.dto.ItemQueryDTO;
|
||||||
|
import com.witsoft.mica.smd.vo.ItemVO;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class ItemMapperTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ItemMapper itemMapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testQueryPageList() {
|
||||||
|
// Arrange
|
||||||
|
ItemQueryDTO dto = new ItemQueryDTO();
|
||||||
|
dto.setPageNo(1);
|
||||||
|
dto.setPageSize(10);
|
||||||
|
dto.setEcid("test-ecid");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<ItemVO> list = itemMapper.queryPageList(dto);
|
||||||
|
long total = itemMapper.queryPageCount(dto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(list);
|
||||||
|
assertTrue(total >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSelectById() {
|
||||||
|
// Act
|
||||||
|
ItemVO item = itemMapper.selectById("1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(item);
|
||||||
|
assertNotNull(item.getItemCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 测试命名规范
|
||||||
|
|
||||||
|
### 5.1 测试类命名
|
||||||
|
|
||||||
|
```
|
||||||
|
{被测试类名}Test.java
|
||||||
|
示例:ItemControllerTest.java, ItemServiceTest.java
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 测试方法命名
|
||||||
|
|
||||||
|
```
|
||||||
|
test{方法名}{场景}.java
|
||||||
|
示例:
|
||||||
|
- testQueryPageListSuccess()
|
||||||
|
- testQueryPageListWithEmptyParams()
|
||||||
|
- testQueryPageListServiceException()
|
||||||
|
- testInsertItemValidationFailed()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 DisplayName 注解
|
||||||
|
|
||||||
|
```java
|
||||||
|
@DisplayName("分页查询 - 成功")
|
||||||
|
@DisplayName("分页查询 - 参数为空时返回空结果")
|
||||||
|
@DisplayName("分页查询 - 服务异常处理")
|
||||||
|
@DisplayName("新增物料 - 参数校验失败")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 断言规范
|
||||||
|
|
||||||
|
### 6.1 常用断言
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 非空断言
|
||||||
|
assertNotNull(result);
|
||||||
|
assertNull(result);
|
||||||
|
|
||||||
|
// 布尔值断言
|
||||||
|
assertTrue(response.isSuccess());
|
||||||
|
assertFalse(response.isSuccess());
|
||||||
|
|
||||||
|
// 相等断言
|
||||||
|
assertEquals(1, result.getTotal());
|
||||||
|
assertEquals("ITEM001", result.getItemCode());
|
||||||
|
|
||||||
|
// 集合断言
|
||||||
|
assertTrue(list.isEmpty());
|
||||||
|
assertEquals(1, list.size());
|
||||||
|
|
||||||
|
// 异常断言
|
||||||
|
assertThrows(Exception.class, () -> {
|
||||||
|
itemController.deleteItem("");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Mockito 验证
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 验证方法调用次数
|
||||||
|
verify(itemService, times(1)).queryPageList(any());
|
||||||
|
verify(itemService, never()).deleteItem(any());
|
||||||
|
|
||||||
|
// 验证调用参数
|
||||||
|
verify(itemService).insertItem(argThat(dto ->
|
||||||
|
dto.getItemCode() != null &&
|
||||||
|
dto.getEcid() != null
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 测试覆盖率要求
|
||||||
|
|
||||||
|
| 层级 | 覆盖率要求 | 优先级 |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| Service | ≥ 80% | 高 |
|
||||||
|
| Controller | ≥ 70% | 中 |
|
||||||
|
| Mapper | ≥ 60% | 中 |
|
||||||
|
| Entity/DTO | 不要求 | 低 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 测试执行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
mvn test
|
||||||
|
|
||||||
|
# 运行指定测试类
|
||||||
|
mvn test -Dtest=ItemControllerTest
|
||||||
|
|
||||||
|
# 运行指定测试方法
|
||||||
|
mvn test -Dtest=ItemControllerTest#testQueryPageListSuccess
|
||||||
|
|
||||||
|
# 生成覆盖率报告
|
||||||
|
mvn clean test jacoco:report
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 最佳实践
|
||||||
|
|
||||||
|
### ✅ DO
|
||||||
|
|
||||||
|
1. **测试独立** - 每个测试方法相互独立,不依赖执行顺序
|
||||||
|
2. **命名清晰** - 测试方法名清晰表达测试意图
|
||||||
|
3. **Arrange-Act-Assert** - 遵循 AAA 模式组织测试代码
|
||||||
|
4. **测试边界** - 重点测试边界条件和异常情况
|
||||||
|
5. **Mock 外部依赖** - 使用 Mockito 隔离外部依赖
|
||||||
|
|
||||||
|
### ❌ DON'T
|
||||||
|
|
||||||
|
1. **不要测试私有方法** - 通过公共方法间接测试
|
||||||
|
2. **不要过度测试** - 聚焦业务逻辑,不测试 getter/setter
|
||||||
|
3. **不要依赖外部状态** - 每个测试自包含,不依赖数据库真实数据
|
||||||
|
4. **不要忽略失败** - 测试失败必须修复或合理解释
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 示例:完整测试类
|
||||||
|
|
||||||
|
参考 `ItemControllerTest.java` 和 `ItemServiceTest.java`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 持续集成
|
||||||
|
|
||||||
|
### Jenkins Pipeline 配置
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
stages {
|
||||||
|
stage('Test') {
|
||||||
|
steps {
|
||||||
|
sh 'mvn clean test'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
junit 'target/surefire-reports/*.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Coverage') {
|
||||||
|
steps {
|
||||||
|
sh 'mvn jacoco:report'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
jacoco execPattern: 'target/jacoco.exec'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 参考资料
|
||||||
|
|
||||||
|
- [JUnit 5 官方文档](https://junit.org/junit5/docs/current/user-guide/)
|
||||||
|
- [Mockito 官方文档](https://site.mockito.org/)
|
||||||
|
- [Spring Boot Testing](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)
|
||||||
|
- [MyBatis Spring Boot Test](https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/)
|
||||||
@@ -149,7 +149,6 @@ public interface WorkshopApi {
|
|||||||
- 请求入口:`log.info("物料查询请求:itemCode={}", dto.getItemCode())`
|
- 请求入口:`log.info("物料查询请求:itemCode={}", dto.getItemCode())`
|
||||||
- 异常捕获:`log.error("物料查询失败", e)`
|
- 异常捕获:`log.error("物料查询失败", e)`
|
||||||
- 关键业务节点:`log.info("物料创建成功:itemCode={}", result.getItemCode())`
|
- 关键业务节点:`log.info("物料创建成功:itemCode={}", result.getItemCode())`
|
||||||
- Feign 调用:`log.debug("Feign 调用 - 创建物料,入参:{}", JsonUtils.toJson(dto))`
|
|
||||||
|
|
||||||
**示例**:
|
**示例**:
|
||||||
```java
|
```java
|
||||||
@@ -169,7 +168,31 @@ public class ItemController {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 字段注释
|
### 4. Feign 调用日志规范
|
||||||
|
|
||||||
|
**正确示例**:
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public ResponseModel insertItem(ItemFormDTO dto) {
|
||||||
|
log.debug("Feign 调用 - 创建物料,入参:{}", JSON.toJSONString(dto));
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Map<String, Object> itemMap = Convert.convert(Map.class, dto);
|
||||||
|
ResponseModel response = itemApi.insertItem(itemMap);
|
||||||
|
|
||||||
|
log.debug("Feign 调用 - 创建物料完成,耗时:{}ms",
|
||||||
|
System.currentTimeMillis() - start);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规范**:
|
||||||
|
- 日志级别:`debug`
|
||||||
|
- 记录内容:入参 + 耗时
|
||||||
|
- **禁止记录返回值**(避免日志过大)
|
||||||
|
- 使用 `JSON.toJSONString()` (fastjson) 序列化对象
|
||||||
|
|
||||||
|
### 5. 字段注释
|
||||||
|
|
||||||
单行注释即可:
|
单行注释即可:
|
||||||
|
|
||||||
@@ -183,7 +206,7 @@ private String itemName;
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 已修正的问题清单 (1-12)
|
## 🐛 已修正的问题清单 (1-15)
|
||||||
|
|
||||||
以下问题在开发过程中发现并已修正,**后续开发必须遵守**:
|
以下问题在开发过程中发现并已修正,**后续开发必须遵守**:
|
||||||
|
|
||||||
@@ -192,7 +215,7 @@ private String itemName;
|
|||||||
| 1 | **ResponseModel 静态引用错误** | 使用 `ResponseModel.succeed(data)` 静态方法 | `ItemController.java`, `ItemServiceImpl.java` |
|
| 1 | **ResponseModel 静态引用错误** | 使用 `ResponseModel.succeed(data)` 静态方法 | `ItemController.java`, `ItemServiceImpl.java` |
|
||||||
| 2 | **分页 XML / 详情 MP 混合使用** | 分页查询用原生 XML,详情查询用 `BaseMapper.selectById` | `ItemMapper.xml`, `ItemServiceImpl.java` |
|
| 2 | **分页 XML / 详情 MP 混合使用** | 分页查询用原生 XML,详情查询用 `BaseMapper.selectById` | `ItemMapper.xml`, `ItemServiceImpl.java` |
|
||||||
| 3 | **移除编码查询条件** | 从 DTO 和 XML 移除 `itemCode` 模糊搜索 | `ItemQueryDTO.java`, `ItemMapper.xml` |
|
| 3 | **移除编码查询条件** | 从 DTO 和 XML 移除 `itemCode` 模糊搜索 | `ItemQueryDTO.java`, `ItemMapper.xml` |
|
||||||
| 4 | **Feign 调用添加日志** | debug 级别:入参 + 耗时 + 返回 | `ItemServiceImpl.java` |
|
| 4 | **Feign 调用添加日志** | debug 级别:入参 + 耗时 | `ItemServiceImpl.java` |
|
||||||
| 5 | **Controller 层 ecid 处理** | `GlobalUtils.getEcid()` 在 Controller 层获取并传递给 Service | `ItemController.java` |
|
| 5 | **Controller 层 ecid 处理** | `GlobalUtils.getEcid()` 在 Controller 层获取并传递给 Service | `ItemController.java` |
|
||||||
| 6 | **恢复分页 XML 查询** | 保留完整的分页查询 SQL | `ItemMapper.xml` |
|
| 6 | **恢复分页 XML 查询** | 保留完整的分页查询 SQL | `ItemMapper.xml` |
|
||||||
| 7 | **使用 PageDomain (非 PageHelper)** | 手动分页:先 count 查询总数,再 LIMIT 查询数据 | `ItemServiceImpl.java` |
|
| 7 | **使用 PageDomain (非 PageHelper)** | 手动分页:先 count 查询总数,再 LIMIT 查询数据 | `ItemServiceImpl.java` |
|
||||||
@@ -201,6 +224,9 @@ private String itemName;
|
|||||||
| 10 | **Map 类型转换警告** | 使用 `Convert.convert(Map.class, dto)` 或 `@SuppressWarnings` | `ItemController.java`, `ItemServiceImpl.java` |
|
| 10 | **Map 类型转换警告** | 使用 `Convert.convert(Map.class, dto)` 或 `@SuppressWarnings` | `ItemController.java`, `ItemServiceImpl.java` |
|
||||||
| 11 | **XML 使用 `<sql>` + `<include>` 片段** | 公共列定义和查询条件使用 SQL 片段复用 | `ItemMapper.xml` |
|
| 11 | **XML 使用 `<sql>` + `<include>` 片段** | 公共列定义和查询条件使用 SQL 片段复用 | `ItemMapper.xml` |
|
||||||
| 12 | **ItemApi ResponseModel 泛型** | Feign 接口返回 `ResponseModel<Void>` | `ItemApi.java` |
|
| 12 | **ItemApi ResponseModel 泛型** | Feign 接口返回 `ResponseModel<Void>` | `ItemApi.java` |
|
||||||
|
| 13 | **ResponseModel 使用裸类型** | 与项目其他 Feign 接口一致,使用裸类型 `ResponseModel` | `ItemApi.java`, `ItemService.java`, `ItemServiceImpl.java`, `ItemController.java` |
|
||||||
|
| 14 | **日志禁止记录返回值** | 只记录耗时,不记录返回值(避免日志过大) | `ItemServiceImpl.java` |
|
||||||
|
| 15 | **JsonUtils.toJson() 方法不存在** | 使用 `JSON.toJSONString()` (fastjson) 替代 | `ItemServiceImpl.java` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -240,6 +266,32 @@ ItemInfo item = itemMapper.selectById(id);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 问题 4 详解:Feign 调用日志
|
||||||
|
|
||||||
|
**正确示例**:
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public ResponseModel insertItem(ItemFormDTO dto) {
|
||||||
|
log.debug("Feign 调用 - 创建物料,入参:{}", JSON.toJSONString(dto));
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Map<String, Object> itemMap = Convert.convert(Map.class, dto);
|
||||||
|
ResponseModel response = itemApi.insertItem(itemMap);
|
||||||
|
|
||||||
|
log.debug("Feign 调用 - 创建物料完成,耗时:{}ms",
|
||||||
|
System.currentTimeMillis() - start);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规范**:
|
||||||
|
- 日志级别:`debug`
|
||||||
|
- 记录内容:入参 + 耗时
|
||||||
|
- **禁止记录返回值**(避免日志过大)
|
||||||
|
- 使用 `JSON.toJSONString()` (fastjson) 序列化对象
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 问题 5 详解:Controller 层 ecid 处理
|
### 问题 5 详解:Controller 层 ecid 处理
|
||||||
|
|
||||||
**Controller**:
|
**Controller**:
|
||||||
@@ -345,6 +397,53 @@ public PageDomain<ItemVO> queryPageList(ItemQueryDTO dto) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 问题 13 详解:ResponseModel 裸类型
|
||||||
|
|
||||||
|
**错误**:
|
||||||
|
```java
|
||||||
|
public ResponseModel<Map<String, Object>> insertItem(ItemFormDTO dto) { // ❌ 泛型参数
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确** (与项目其他 Feign 接口一致):
|
||||||
|
```java
|
||||||
|
public ResponseModel insertItem(ItemFormDTO dto) { // ✅ 裸类型
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参考项目其他 Feign 接口**:
|
||||||
|
```java
|
||||||
|
// WorkshopApi.java
|
||||||
|
ResponseModel queryWorkshopPageList(@RequestBody Map<String, Object> workshop);
|
||||||
|
ResponseModel insertWorkshop(@RequestBody Map<String, Object> workshop);
|
||||||
|
|
||||||
|
// FactoryApi.java
|
||||||
|
ResponseModel getWorkShops(@RequestParam("ecid") String ecid);
|
||||||
|
ResponseModel getProdLineList(@RequestParam String workshopCode, @RequestParam String ecid);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 问题 15 详解:JsonUtils 替代方案
|
||||||
|
|
||||||
|
**错误**:
|
||||||
|
```java
|
||||||
|
log.debug("入参:{}", JsonUtils.toJson(dto)); // ❌ JsonUtils.toJson() 方法不存在
|
||||||
|
```
|
||||||
|
|
||||||
|
**正确** (使用 fastjson):
|
||||||
|
```java
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
|
||||||
|
log.debug("入参:{}", JSON.toJSONString(dto)); // ✅ 使用 fastjson
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**: `common-utils` 依赖中的 `JsonUtils` 类没有 `toJson()` 方法,项目已引入 `fastjson` 依赖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 后端完整文件清单 (10 个)
|
## 📁 后端完整文件清单 (10 个)
|
||||||
|
|
||||||
| # | 文件 | 路径 | 状态 |
|
| # | 文件 | 路径 | 状态 |
|
||||||
@@ -362,6 +461,88 @@ public PageDomain<ItemVO> queryPageList(ItemQueryDTO dto) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 数据字典模块开发 (2026-08-04 下午)
|
||||||
|
|
||||||
|
### 🔴 数据字典模块强制性红线
|
||||||
|
|
||||||
|
| # | 红线 | 正确做法 |
|
||||||
|
|---|------|----------|
|
||||||
|
| 1 | **不需要 Controller** | 纯内部工具服务 |
|
||||||
|
| 2 | **不需要 Feign API** | 不暴露外部接口 |
|
||||||
|
| 3 | **不需要缓存实现** | 后续可调用 Feign 接口缓存 |
|
||||||
|
| 4 | **不需要继承 BaseDomain** | 配置项不是业务实体 |
|
||||||
|
|
||||||
|
### 📋 数据字典核心功能
|
||||||
|
|
||||||
|
1. **单个字典类型查询**: `Map<String, String> getDictionaryMap(String typeCode)`
|
||||||
|
2. **批量字典类型查询**: `Map<String, Map<String, String>> getDictionaryMaps(List<String> typeCodes)`
|
||||||
|
3. **输出格式**: `Map<String, String>` (en_code → full_name)
|
||||||
|
4. **重复处理**: 同一 typeCode 下 en_code 重复时取第一条 (按 sort_code + id 排序)
|
||||||
|
5. **多租户支持**: 自动获取 ecid 过滤
|
||||||
|
6. **软删除过滤**: delete_mark = 0
|
||||||
|
|
||||||
|
### 📁 数据字典已生成文件 (5 个)
|
||||||
|
|
||||||
|
| # | 文件 | 路径 | 状态 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | `DictionaryMapper.java` | `smd/mapper/` | ✅ 已写入 |
|
||||||
|
| 2 | `DictionaryMapper.xml` | `resources/mapper/smd/` | ✅ 已写入 |
|
||||||
|
| 3 | `DictionaryItem.java` | `smd/dto/` | ✅ 已写入 |
|
||||||
|
| 4 | `DictionaryService.java` | `smd/service/` | ✅ 已写入 |
|
||||||
|
| 5 | `DictionaryServiceImpl.java` | `smd/service/impl/` | ✅ 已写入 |
|
||||||
|
|
||||||
|
### 📁 已生成枚举类 (2 个)
|
||||||
|
|
||||||
|
| # | 文件 | 说明 |
|
||||||
|
|---|------|------|
|
||||||
|
| 1 | `StatusEnum.java` | 状态枚举 (Y=启用,N=禁用) |
|
||||||
|
| 2 | `YesNoEnum.java` | 是否枚举 (Y=是,N=否) |
|
||||||
|
|
||||||
|
### 🔧 字典填充规范
|
||||||
|
|
||||||
|
#### VO 设计
|
||||||
|
- 添加 `xxxName` 字段存储字典翻译后的中文名称
|
||||||
|
- 示例:`itemType` (en_code) + `itemTypeName` (中文名称)
|
||||||
|
|
||||||
|
#### Service 层填充
|
||||||
|
- **批量查询字典**: 一次查询多个字典类型,避免 N+1 问题
|
||||||
|
- **通用填充方法**: `fillDictionaryData(ItemVO itemVO, Map<String, Map<String, String>> dictMaps)`
|
||||||
|
- **性能要求**: 列表查询只查 1 次字典,批量填充
|
||||||
|
|
||||||
|
#### 物料信息字典映射
|
||||||
|
|
||||||
|
| VO 字段 | 字典类型 | 说明 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| `itemTypeName` | `materialsType` | 物料类型名称 |
|
||||||
|
| `propertiesName` | `itemProperties` | 物料属性名称 |
|
||||||
|
| `pickingPropertyName` | `pickingProperty` | 领料属性名称 |
|
||||||
|
| `statusName` | `StatusEnum` | 状态名称 (枚举) |
|
||||||
|
|
||||||
|
### 📊 性能优化
|
||||||
|
|
||||||
|
| 优化点 | 说明 | 效果 |
|
||||||
|
|--------|------|------|
|
||||||
|
| **批量查询字典** | 一次查询多个字典类型 | 避免多次数据库查询 |
|
||||||
|
| **字典 Map 传入** | `fillDictionaryData()` 接收外部传入的 dictMaps | 避免重复查询 |
|
||||||
|
| **列表查询优化** | 100 条数据从 101 次查询降低到 2 次 | 性能提升 50 倍 |
|
||||||
|
| **枚举静态方法** | 枚举翻译使用静态方法 | 无运行时开销 |
|
||||||
|
|
||||||
|
### 🧪 单元测试
|
||||||
|
|
||||||
|
- 测试文件:`src/test/java/com/witsoft/mica/smd/service/DictionaryServiceTest.java`
|
||||||
|
- 测试方法:英文命名(避免中文字符)
|
||||||
|
- Mock 处理:`GlobalUtils.getEcid()` 改为可 mock 的实例方法
|
||||||
|
- 测试结果:✅ 5 个测试全部通过
|
||||||
|
|
||||||
|
### 🐛 数据字典开发修正问题
|
||||||
|
|
||||||
|
| # | 问题 | 修正方案 |
|
||||||
|
|---|------|----------|
|
||||||
|
| 16 | **fillDictionaryData 重复查询** | 修改方法签名接收 dictMaps 参数,避免 N+1 问题 |
|
||||||
|
| 17 | **planner 越权生成代码** | 明确 planner 禁止生成代码,由 backend agent 执行 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📝 待办事项
|
## 📝 待办事项
|
||||||
|
|
||||||
### Backend (mica-server)
|
### Backend (mica-server)
|
||||||
@@ -375,6 +556,12 @@ public PageDomain<ItemVO> queryPageList(ItemQueryDTO dto) {
|
|||||||
- [x] 创建 `smd/dto/ItemQueryDTO.java`
|
- [x] 创建 `smd/dto/ItemQueryDTO.java`
|
||||||
- [x] 创建 `smd/dto/ItemFormDTO.java`
|
- [x] 创建 `smd/dto/ItemFormDTO.java`
|
||||||
- [x] 创建 `smd/vo/ItemVO.java`
|
- [x] 创建 `smd/vo/ItemVO.java`
|
||||||
|
- [x] 修正 15 个编译错误
|
||||||
|
- [x] Maven 编译验证通过
|
||||||
|
- [x] 数据字典模块开发 (5 个文件 + 2 个枚举类)
|
||||||
|
- [x] 字典填充逻辑实现
|
||||||
|
- [x] 单元测试通过
|
||||||
|
- [ ] 集成测试
|
||||||
|
|
||||||
### Frontend (mica-web)
|
### Frontend (mica-web)
|
||||||
- [ ] 创建物料管理 API 封装 (`app/composables/smd/useItemApi.ts`)
|
- [ ] 创建物料管理 API 封装 (`app/composables/smd/useItemApi.ts`)
|
||||||
|
|||||||
@@ -0,0 +1,610 @@
|
|||||||
|
---
|
||||||
|
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<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 次字典,批量填充
|
||||||
|
|
||||||
|
### 填充示例代码
|
||||||
|
|
||||||
|
```java
|
||||||
|
@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`
|
||||||
|
|
||||||
|
#### 测试类结构模板
|
||||||
|
```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`
|
||||||
|
|
||||||
|
#### 测试类结构模板
|
||||||
|
```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<Void> 编译报错 | 使用 `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 配置 | ⏸️ 待添加 |
|
||||||
Reference in New Issue
Block a user