sync from WSL host via Gitea: align to origin/main

This commit is contained in:
2026-07-14 16:59:38 +08:00
parent 8865fb5aa7
commit 0b0daf4e66
153 changed files with 24711 additions and 37 deletions
+422
View File
@@ -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`