# 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`
