# 第 4 批接口移植:商品保存链路 + 文件上传 > 时间:2026-10-17 > 范围:商品新增/编辑(含多规格)、上下架、删除、按 ID 取列表、商品基础信息、规格列表、图片/视频上传 ## 一、本批完成的接口 | 接口 | 方法 | 说明 | |---|---|---| | `/store/goods/listByIds` | GET | 按 ID 集取商品(`field(goods_id, ids)` 顺序) | | `/store/goods/basic` | GET/POST | 商品基础信息(含 skuList / goods_images) | | `/store/goods/add` | POST | 新增商品(单规格 / 多规格) | | `/store/goods/edit` | POST | 编辑商品(sku 重建、关联差集) | | `/store/goods/state` | POST | 批量上下架 | | `/store/goods/delete` | POST | 批量软删除 | | `/store/goods.spec/list` | GET | 商品规格属性列表(多级控制器点号) | | `/store/upload/image` | POST | 图片上传(字段名 `iFile`) | | `/store/upload/video` | POST | 视频上传 | store 模块进度:**151 个接口已实现 91 个,剩余 60 个**(`tools/check_progress.py` 可随时盘点)。 ## 二、关键实现细节(对齐 PHP 的行为) ### 1. 单规格 / 多规格的价格与库存 - 单规格:`goods_price_min = goods_price_max = goods_price`,`line_price` 同理,`stock_total = stock_num`,SKU 只写一条且 `goods_sku_id = "0"`。 - 多规格:`stock_total = Σ stock_num`;价格区间取 sku 列表 `array_filter` 后(过滤 0 与空值)的 min/max,全空则 `[0, 0]`。 - 多规格 SKU 落库前做兜底:`goods_price ?: 0.01`、`line_price ?: 0`、`stock_num ?: 0`(与 PHP `increasedFroMulti` 一致)。 ### 2. 规格组 / 规格值复用 对照 PHP `Spec::getNewSpecList` / `SpecValue::getNewValueList`:先按名称(含 `store_id`)查已有记录并复用 ID,不存在才 insert。这样同一规格名在 `yoshop_spec` 里不会重复堆积。 `goods_sku_id` 由规格值 ID 用 `_` 连接生成;`goods_props` 存 `[{group:{name,id}, value:{name,id}}]`。出参时这两个字段会被反解成数组(`GoodsSku` 上加了 `@JsonIgnore` + `@JsonProperty` 的 JSON getter),与 PHP 的获取器行为一致。 ### 3. 关联表更新策略 - 分类 / 图片 / 服务与承诺:新增走「批量插入」,编辑走「差集」(只删多余的、只插新增的),保证关联行 id 稳定。 - 规格关联 / SKU:编辑时「全删再插」(对齐 PHP `GoodsSpecRel::updates`、`GoodsSku::edit`)。 ### 4. 其它易错点 - `alone_grade_equity`:前端键是 `grade_id:10001`,落库前去掉前缀并**保持为对象**(PHP 非连续索引数组 json_encode 后是对象,不是数组);`is_enable_grade=0` 时强制 `is_alone_grade=0`;开启单独折扣但传空时报「请先添加会员等级后再设置会员折扣价」。 - `delivery_type` 库内是 JSON 字符串(`[10]`),实体走 `JacksonTypeHandler`。 - 商品删除是软删除(`is_delete=1`),`Goods` 实体的 `@TableLogic(value="0", delval="1")` 与全局 10/30 配置不同,不能混用。 ### 5. 文件上传 - 字段名 `iFile`、分组走 query `groupId`,与 PHP `Upload::FORM_NAME` 一致。 - 落盘路径 `{storeId}/{yyyyMMdd}/{md5}.{ext}`,写 `yoshop_upload_file`(`storage=local`、`domain` 留空)。 - 出参 `fileInfo` 含 `preview_url` / `external_url`,域名取 `yoshop.base-url`。 - 新增配置:`yoshop.upload-path`(默认 `runtime/uploads`),并把 `/uploads/**` 映射为该目录的静态资源,便于本地联调;线上建议仍由 Nginx 托管。 - 上传大小上限调到 100MB(`spring.servlet.multipart`)。 ## 三、验证结果 ### 1. 写接口端到端(`tools/test_goods_writes.py`,**42 项断言全通过**) 覆盖:新增单规格 → 库内校验(价格/库存/关联/sku)→ 读回校验 → 编辑为多规格(sku 重建、规格入库、价格区间)→ 读回校验 → 下架/上架 → 软删除 → 删除后不可见 → 数据清理。 ### 2. 读接口逐字段对比(`tools/compare_live.py`,**7/7 PASS,0 差异**) `goods/basic`、`goods/listByIds`、`goods.spec/list`、`goods/detail`(单规格 + 2 个多规格商品)全部与线上 PHP 一致。 ### 3. 回归 `tools/compare_batch.py` 28 个既有接口 **28/28 PASS**(`GoodsSku` 的 JSON 字段改造没有破坏详情接口)。 ## 四、⚠️ 环境发现:本地库与线上库不是强一致 测试中确认了一个影响后续方案的事实: - **线上 PHP 写入的数据,本地 Java 能读到**(例:通过 PHP 接口上传的「泰国金枕榴莲」在两边都在); - **Java 写入的数据,线上 PHP 读不到**(新建商品后 PHP `detail` 报「商品信息不存在」、`list?goodsName=` 返回空,等待 9 秒仍如此)。 也就是说 `192.168.1.177/shop` 与线上 PHP 的库之间是**单向同步(PHP → 本地)**,Java 的写操作只落在本地副本。 影响: 1. 写接口无法用「PHP 读回」做交叉验证,本批改为「库内断言 + Java 读回」自洽校验(已覆盖价格、库存、关联、sku、软删除等所有落库结果); 2. **上线前必须把 Java 的数据源指向线上主库**,否则后台改的数据在线上不生效; 3. 第 3 批的写测试(用户/角色/页面)同样写到了本地副本,不影响线上数据,但也验证不到线上。 ## 五、下一步 剩余 61 个接口,按页面可用性排序: 1. **订单事件**:改价、备注、改地址、取消、发货、发货单列表/详情/物流轨迹、退款审核/收货; 2. **设置类 CRUD**:支付设置(options/update/模板 CRUD)、配送模板、运费模板、打印机; 3. **营销**:优惠券 CRUD/领取、充值方案 CRUD、会员等级 CRUD; 4. **其余**:商品评论、客户端设置(h5/wxapp)、统计接口、`controller/initialize`。 另外 **`checkPrivilege`(按角色校验 API 权限)仍未实现**,切流前必须补。