Files
openclaw-config/workspace-backend/memory/2026-05-27.md
T

423 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`