sync from WSL host via Gitea: align to origin/main
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
# 2026-05-27 会话记录
|
||||
|
||||
## 问题修复
|
||||
|
||||
### 1. MyBatis-Plus 空集合 IN 子句异常
|
||||
|
||||
**问题:** `selectBatchIds` 传入空集合时生成 `IN ()` 非法 SQL,导致 JSQLParser 解析失败。
|
||||
|
||||
**修复位置:**
|
||||
- `InboundServiceImpl.listHistory()` - 供应商、仓库查询
|
||||
- `OutboundServiceImpl.listHistory()` - 客户、仓库查询
|
||||
|
||||
**解决方案:** 使用三元表达式避免空集合查询
|
||||
```java
|
||||
Map<Long, String> supplierNameMap = supplierIds.isEmpty() ? new HashMap<>() :
|
||||
supplierMapper.selectBatchIds(supplierIds).stream()
|
||||
.collect(Collectors.toMap(Supplier::getId, Supplier::getName));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. JavaScript 精度丢失问题
|
||||
|
||||
**问题:** 雪花算法生成的 Long 类型 ID(如 `2059467259520360450`)超出 JS 安全整数范围,前端接收后精度丢失为 `2059467259520360400`。
|
||||
|
||||
**解决方案:** 创建 `JacksonConfig.java`,全局配置 Long 类型序列化为字符串
|
||||
```java
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
@Bean
|
||||
public Module longToStringModule() {
|
||||
return new SimpleModule()
|
||||
.addSerializer(Long.class, ToStringSerializer.instance)
|
||||
.addSerializer(long.class, ToStringSerializer.instance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
- `Long` 类型 → 字符串(`"2059467259520360450"`)
|
||||
- `Integer` 类型 → 保持数字(`code: 200`)
|
||||
|
||||
---
|
||||
|
||||
### 3. creatorName 为 null
|
||||
|
||||
**根因:** `BaseEntity.creator` 字段自动填充时存的是用户昵称(如"管理员"),但查询逻辑错误地用 `creator` 的值去匹配 `sys_user.username` 字段。
|
||||
|
||||
**修复:**
|
||||
1. `MyMetaObjectHandler.java` - 添加默认值兜底:昵称 → 登录名 → "系统"
|
||||
2. `InboundServiceImpl` / `OutboundServiceImpl` - 直接使用 `creator` 字段值作为 `creatorName`
|
||||
|
||||
---
|
||||
|
||||
### 4. 安全库存字段命名
|
||||
|
||||
**问题:** `StockSafeQtyReqDTO.safeQty` 实际含义是最小安全库存,字段名不准确。
|
||||
|
||||
**修复:**
|
||||
- 字段名:`safeQty` → `minSafeQty`
|
||||
- 注释和验证提示同步更新
|
||||
|
||||
---
|
||||
|
||||
### 5. 库存详情接口填充物料信息
|
||||
|
||||
**需求:** `/im/stock/{materialId}` 接口需要返回物料相关信息。
|
||||
|
||||
**实现:**
|
||||
1. 新建 `StockDetailDTO` - 包含库存、物料、仓库信息
|
||||
2. `StockService.getStockDetail()` - 使用 `MaterialFillService` 填充物料信息
|
||||
3. 查询仓库信息填充仓库名称
|
||||
|
||||
---
|
||||
|
||||
## 部署操作
|
||||
|
||||
### 测试环境 Docker 重新部署
|
||||
|
||||
**命令:**
|
||||
```bash
|
||||
cd /home/yangxuan/Projects/IdeaProjects/tiny-erp
|
||||
docker compose stop app
|
||||
docker compose rm -f app
|
||||
docker compose build --no-cache app
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
**状态:** ✅ 部署成功,健康检查通过
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
- [x] 清理 `im_inbound_item` / `im_outbound_item` 脏数据(主表记录不存在的明细)
|
||||
- [x] 考虑添加外键约束防止未来产生脏数据
|
||||
- [x] 创建测试环境部署文档 → `/home/yangxuan/Projects/IdeaProjects/tiny-erp/doc/测试环境部署文档.md`
|
||||
- [x] 强调 Maven 构建前提(`mvn clean install`)
|
||||
# 2026-05-27 会话记录
|
||||
|
||||
## 问题修复
|
||||
|
||||
### 1. MyBatis-Plus 空集合 IN 子句异常
|
||||
|
||||
**问题:** `selectBatchIds` 传入空集合时生成 `IN ()` 非法 SQL,导致 JSQLParser 解析失败。
|
||||
|
||||
**修复位置:**
|
||||
- `InboundServiceImpl.listHistory()` - 供应商、仓库查询
|
||||
- `OutboundServiceImpl.listHistory()` - 客户、仓库查询
|
||||
|
||||
**解决方案:** 使用三元表达式避免空集合查询
|
||||
```java
|
||||
Map<Long, String> supplierNameMap = supplierIds.isEmpty() ? new HashMap<>() :
|
||||
supplierMapper.selectBatchIds(supplierIds).stream()
|
||||
.collect(Collectors.toMap(Supplier::getId, Supplier::getName));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. JavaScript 精度丢失问题
|
||||
|
||||
**问题:** 雪花算法生成的 Long 类型 ID(如 `2059467259520360450`)超出 JS 安全整数范围,前端接收后精度丢失为 `2059467259520360400`。
|
||||
|
||||
**解决方案:** 创建 `JacksonConfig.java`,全局配置 Long 类型序列化为字符串
|
||||
```java
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
@Bean
|
||||
public Module longToStringModule() {
|
||||
return new SimpleModule()
|
||||
.addSerializer(Long.class, ToStringSerializer.instance)
|
||||
.addSerializer(long.class, ToStringSerializer.instance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**效果:**
|
||||
- `Long` 类型 → 字符串(`"2059467259520360450"`)
|
||||
- `Integer` 类型 → 保持数字(`code: 200`)
|
||||
|
||||
---
|
||||
|
||||
### 3. creatorName 为 null
|
||||
|
||||
**根因:** `BaseEntity.creator` 字段自动填充时存的是用户昵称(如"管理员"),但查询逻辑错误地用 `creator` 的值去匹配 `sys_user.username` 字段。
|
||||
|
||||
**修复:**
|
||||
1. `MyMetaObjectHandler.java` - 添加默认值兜底:昵称 → 登录名 → "系统"
|
||||
2. `InboundServiceImpl` / `OutboundServiceImpl` - 直接使用 `creator` 字段值作为 `creatorName`
|
||||
|
||||
---
|
||||
|
||||
### 4. 安全库存字段命名
|
||||
|
||||
**问题:** `StockSafeQtyReqDTO.safeQty` 实际含义是最小安全库存,字段名不准确。
|
||||
|
||||
**修复:**
|
||||
- 字段名:`safeQty` → `minSafeQty`
|
||||
- 注释和验证提示同步更新
|
||||
|
||||
---
|
||||
|
||||
### 5. 库存详情接口填充物料信息
|
||||
|
||||
**需求:** `/im/stock/{materialId}` 接口需要返回物料相关信息。
|
||||
|
||||
**实现:**
|
||||
1. 新建 `StockDetailDTO` - 包含库存、物料、仓库信息
|
||||
2. `StockService.getStockDetail()` - 使用 `MaterialFillService` 填充物料信息
|
||||
3. 查询仓库信息填充仓库名称
|
||||
|
||||
---
|
||||
|
||||
### 6. 安全库存接口统一
|
||||
|
||||
**需求:** 将安全库存相关接口从 `MaterialController` 统一迁移到 `StockController`。
|
||||
|
||||
**修改内容:**
|
||||
- ✅ `StockController` - 新增 3 个接口(设置、批量设置、清除安全库存)
|
||||
- ✅ `StockService` / `StockServiceImpl` - 实现新增方法
|
||||
- ❌ `MaterialController` - 删除 2 个安全库存接口
|
||||
- ❌ `MaterialService` / `MaterialServiceImpl` - 删除相关方法
|
||||
- ❌ `MaterialMapper` / `MaterialMapper.xml` - 删除相关 SQL
|
||||
|
||||
**新 API:**
|
||||
- `PUT /im/stock/safe-qty` - 设置安全库存(单个)
|
||||
- `PUT /im/stock/safe-qty/batch` - 批量设置安全库存
|
||||
- `PUT /im/stock/{materialId}/safe-qty/clear` - 清除安全库存
|
||||
|
||||
---
|
||||
|
||||
### 7. 库存详情接口填充 minSafeQty
|
||||
|
||||
**问题:** `StockServiceImpl.getStockDetail()` 漏掉了 `minSafeQty` 字段填充。
|
||||
|
||||
**修复:** 添加 `dto.setMinSafeQty(material.getMinSafeQty());`
|
||||
|
||||
---
|
||||
|
||||
### 8. /home/welcome 接口数据写死
|
||||
|
||||
**问题:** 用户名、公司名、欢迎语都是硬编码。
|
||||
|
||||
**修复:**
|
||||
- 使用 `UserContext.getNickname()` 获取真实用户名
|
||||
- 使用 `TenantContext.getTenantName()` 获取企业名称
|
||||
- 保留兜底逻辑("用户"、"企业")
|
||||
|
||||
---
|
||||
|
||||
## 数据库变更
|
||||
|
||||
### 增量脚本执行
|
||||
|
||||
**脚本 1:** `scripts/postgresql/fix-fin-add-order-date.sql`
|
||||
- `fin_ar` 表:添加 `order_date` 字段(DATE 类型)
|
||||
- `fin_ap` 表:添加 `order_date` 字段(DATE 类型)
|
||||
- 数据初始化:将 `create_time` 转换为日期赋值给 `order_date`
|
||||
|
||||
**脚本 2:** `scripts/postgresql/fix-write-off-add-updater.sql`
|
||||
- `fin_ar_write_off` 表:添加 `updater`、`update_time` 字段
|
||||
- `fin_ap_write_off` 表:添加 `updater`、`update_time` 字段
|
||||
- 数据初始化:将 `creator`/`create_time` 赋值给 `updater`/`update_time`
|
||||
|
||||
**执行方式:**
|
||||
```bash
|
||||
docker compose exec db psql -U postgres -d tinyerp \
|
||||
-f /docker-entrypoint-initdb.d/migrations/20260527_add_column.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文档更新
|
||||
|
||||
### 1. 部署文档合并
|
||||
|
||||
- ✅ 将 `~/tiny/测试环境部署文档.md` 合并到 `DOCKER.md`
|
||||
- ✅ 新增 4 个部署场景(首次部署、重新部署、只重启、彻底重置)
|
||||
- ✅ 强调 Maven 构建前提(`mvn clean install -DskipTests`)
|
||||
- ✅ 完善故障排查表格
|
||||
- ❌ 删除重复文档 `~/tiny/测试环境部署文档.md`
|
||||
|
||||
### 2. 库存接口文档
|
||||
|
||||
- ✅ 创建 `doc/stock-api.md`
|
||||
- 包含 6 个库存管理接口 + 1 个相关接口
|
||||
- 字段说明、物料类型枚举、注意事项
|
||||
|
||||
---
|
||||
|
||||
## 部署操作
|
||||
|
||||
### 测试环境 Docker 重新部署
|
||||
|
||||
**命令:**
|
||||
```bash
|
||||
cd /home/yangxuan/Projects/IdeaProjects/tiny-erp
|
||||
mvn clean install -DskipTests
|
||||
docker compose stop app
|
||||
docker compose rm -f app
|
||||
docker compose build --no-cache app
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
**状态:** ✅ 部署成功,健康检查通过
|
||||
|
||||
---
|
||||
|
||||
## 经验教训
|
||||
|
||||
1. **Maven 构建前提**:Docker 镜像构建复制本地 `target/` 目录,必须先执行 `mvn clean install`
|
||||
2. **Long 类型精度丢失**:雪花算法 ID 超出 JS 安全范围,全局序列化为字符串
|
||||
3. **空集合 IN 子句**:MyBatis-Plus `selectBatchIds` 不处理空集合,需要手动判断
|
||||
4. **creator 字段含义**:自动填充存的是昵称,不是用户名,查询时要注意匹配字段
|
||||
5. **接口职责分离**:库存相关操作统一在 `StockController`,物料相关在 `MaterialController`
|
||||
|
||||
---
|
||||
|
||||
## 数据库环境(双数据库)
|
||||
|
||||
| 环境 | 类型 | 端口 | 连接方式 | sys_tenant.tenant_name |
|
||||
|------|------|------|----------|------------------------|
|
||||
| **测试库** | Docker 容器 tiny-erp-db (postgres:18.1) | 未暴露 | 仅 Docker 内部访问 (db:5432) | 默认租户 → 已改为**水晶科技有限公司** |
|
||||
| **本地库** | WSL apt 安装的 pgsql 16 | localhost:31983 | IntelliJ / psql 直接连接 | 水晶科技有限公司 |
|
||||
|
||||
- Docker 应用 `tiny-erp-app` 连接的是**测试库**(Docker 内部 `tiny-erp-db`)
|
||||
- `application-local.yml` → `localhost:31983`(本地库),绑定 `server.port=8081`
|
||||
- `application-test.yml` → `db:5432`(测试库),Docker 运行时通过 `SPRING_PROFILES_ACTIVE=test` 激活
|
||||
|
||||
## 待办事项
|
||||
|
||||
- [ ] 更新 Java 实体类(`FinAr`、`FinAp`、`FinArWriteOff`、`FinApWriteOff`)
|
||||
- [ ] 记录数据库变更日志到 `doc/数据库变更日志.md`
|
||||
- [ ] 考虑添加外键约束防止 `im_inbound_item` / `im_outbound_item` 脏数据
|
||||
|
||||
---
|
||||
|
||||
## 新增功能
|
||||
|
||||
### 9. 物料选择器返回上次单价
|
||||
|
||||
**需求:** 采购入库/销售出库时单价必填,物料选择器返回上次单价供用户参考。
|
||||
|
||||
**修改内容:**
|
||||
1. ✅ `StockPageDTO` - 新增 `lastInboundPrice` 和 `lastOutboundPrice` 字段
|
||||
2. ✅ `Material` 实体 - 新增 `lastInboundPrice` 和 `lastOutboundPrice` 字段(数据库已有 `last_inbound_price`、`last_outbound_price`)
|
||||
3. ✅ `MaterialSelectorServiceImpl` - 实现批量查询和填充逻辑
|
||||
- `queryLastInboundPrices()` - 批量查询上次入库单价
|
||||
- `queryLastOutboundPrices()` - 批量查询上次出库单价
|
||||
- `pageInboundMaterial()` / `pageOutboundMaterial()` - 填充单价字段
|
||||
- 常用物料场景也支持单价填充
|
||||
|
||||
**API 响应示例:**
|
||||
```json
|
||||
{
|
||||
"materialId": 4,
|
||||
"materialName": "测试物料 A",
|
||||
"lastInboundPrice": 12.50, // 采购入库参考
|
||||
"lastOutboundPrice": 18.80 // 销售出库参考
|
||||
}
|
||||
```
|
||||
|
||||
**前端使用:**
|
||||
- 采购入库单:显示 `lastInboundPrice` 作为参考,单价必填
|
||||
- 销售出库单:显示 `lastOutboundPrice` 作为参考,单价必填
|
||||
- 后端自动填充逻辑:如果用户未填单价,保存时自动使用上次单价
|
||||
|
||||
**测试验证:** ✅ 66 个集成测试全部通过(物料 20 + 入库 23 + 出库 23)
|
||||
|
||||
---
|
||||
|
||||
### 10. 首页动态数量格式化
|
||||
|
||||
**问题:** 首页动态显示的数量带有末尾多余的 0(如 `10.5000 个`)。
|
||||
|
||||
**修复:** `HomeServiceImpl.getLatestNews()` 中使用 `stripTrailingZeros().toPlainString()` 格式化数量
|
||||
|
||||
```java
|
||||
// 修改前
|
||||
String.format("%s入库%s%s", materialName, item.getQuantity(), unit)
|
||||
// 修改后
|
||||
String.format("%s入库%s%s", materialName, item.getQuantity().stripTrailingZeros().toPlainString(), unit)
|
||||
```
|
||||
|
||||
**效果对比:**
|
||||
| 修改前 | 修改后 |
|
||||
|--------|--------|
|
||||
| `测试物料入库 10.5000 个` | `测试物料入库 10.5 个` |
|
||||
| `测试物料出库 100.0000 个` | `测试物料出库 100 个` |
|
||||
|
||||
---
|
||||
|
||||
## 问题修复
|
||||
|
||||
### 11. operator 字段违反 NOT NULL 约束
|
||||
|
||||
**问题:** 出入库单自动生成应收/应付时,`fin_ar_receipt` 表的 `operator` 字段为 null,导致插入失败。
|
||||
|
||||
**错误日志:**
|
||||
```
|
||||
ERROR: null value in column "operator" of relation "fin_ar_receipt" violates not-null constraint
|
||||
```
|
||||
|
||||
**根因:** `MyMetaObjectHandler` 自动填充处理器没有填充 `operator` 字段,当 `UserContext.getUserName()` 返回 null 时导致插入失败。
|
||||
|
||||
**修复方案:** 在 `MyMetaObjectHandler.insertFill()` 中添加 `operator` 字段的自动填充,但**仅在用户未设置时才填充**:
|
||||
|
||||
```java
|
||||
// 操作人(operator 字段,用于财务收款/付款单)- 仅当未设置时自动填充
|
||||
String operatorName = UserContext.getUserName();
|
||||
if (operatorName == null || operatorName.isEmpty()) {
|
||||
operatorName = "管理员";
|
||||
}
|
||||
// 检查是否已有值,有值则不覆盖
|
||||
if (metaObject.hasGetter("operator") && metaObject.getValue("operator") == null) {
|
||||
this.strictInsertFill(metaObject, "operator", String.class, operatorName);
|
||||
}
|
||||
```
|
||||
|
||||
**行为说明:**
|
||||
- ✅ 用户手动设置 `operator` → 保留用户设置的值
|
||||
- ✅ 用户未设置 `operator` → 自动填充当前登录用户(兜底"管理员")
|
||||
- ✅ 自动生成场景(无用户上下文)→ 自动填充"管理员"
|
||||
|
||||
**影响范围:** 适用于 `fin_ar_receipt`(收款单)和 `fin_ap_payment`(付款单)等需要 `operator` 的表
|
||||
|
||||
---
|
||||
|
||||
## 测试进展
|
||||
|
||||
### 集成测试覆盖率
|
||||
|
||||
| 测试类 | 测试数 | 通过 | 失败 | 覆盖率 |
|
||||
|--------|--------|------|------|--------|
|
||||
| `MaterialControllerTest` | 20 | 20 | 0 | 100% ✅ |
|
||||
| `InboundControllerTest` | 23 | 23 | 0 | 100% ✅ |
|
||||
| `OutboundControllerTest` | 23 | 23 | 0 | 100% ✅ |
|
||||
| **总计** | **66** | **66** | **0** | **100%** ✅ |
|
||||
|
||||
**测试覆盖的功能:**
|
||||
- ✅ 物料 CRUD、分页查询、详情查询
|
||||
- ✅ 采购入库单保存、自动填充单价、更新物料最后入库价
|
||||
- ✅ 销售出库单保存、自动填充单价、更新物料最后出库价
|
||||
- ✅ 编码唯一性校验、必填字段校验、BigDecimal 精度比较
|
||||
|
||||
---
|
||||
|
||||
## 经验教训
|
||||
|
||||
1. **BigDecimal 比较**:测试断言使用 `compareTo()` 而非 `equals()`,避免精度差异(`10.50` vs `10.5000`)
|
||||
2. **自动填充优先级**:用户手动设置的值优先于自动填充,`MyMetaObjectHandler` 需要先检查字段是否已有值
|
||||
3. **批量查询优化**:物料选择器使用 `selectBatchIds` 批量查询上次单价,避免 N+1 查询问题
|
||||
4. **数量格式化**:`BigDecimal` 转字符串时使用 `stripTrailingZeros().toPlainString()` 去除末尾多余的 0,同时避免科学计数法
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
- [ ] 前端对接物料选择器接口,显示上次单价参考
|
||||
- [ ] 验证采购入库/销售出库单价必填的前端校验
|
||||
- [ ] 运行完整测试套件确保无回归
|
||||
- [ ] 记录数据库变更日志到 `doc/数据库变更日志.md`
|
||||
Reference in New Issue
Block a user