# 2026-05-27 会话记录 ## 问题修复 ### 1. MyBatis-Plus 空集合 IN 子句异常 **问题:** `selectBatchIds` 传入空集合时生成 `IN ()` 非法 SQL,导致 JSQLParser 解析失败。 **修复位置:** - `InboundServiceImpl.listHistory()` - 供应商、仓库查询 - `OutboundServiceImpl.listHistory()` - 客户、仓库查询 **解决方案:** 使用三元表达式避免空集合查询 ```java Map 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 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`