This commit is contained in:
2025-09-29 21:38:32 +08:00
parent ed26244cdb
commit 19117de6c8
182 changed files with 11590 additions and 2156 deletions

View File

@@ -0,0 +1,175 @@
## 货品删除功能开发文档(软删方案)
### 1. 背景与目标
- 将“与货品相关”的删除行为统一为软删除,避免历史引用断裂,支持后续恢复与审计。
- 用户仅保留“拉黑/恢复”,订单维持“作废 void”不做删除。
### 2. 范围
- 货品主表:`products`
- 关联信息:`product_images``product_prices``inventories``product_aliases`
- 相关查询接口:商品搜索、详情、导出(如有)
### 2.1 父子级联关系(必须遵守)
- 分类(`product_categories` → 模板(`part_templates` → 商品(`products`
- 规则:
- 删除分类 ⇒ 级联软删该分类下所有模板;再级联软删由这些模板创建的所有商品;并同时软删所有 `category_id=该分类` 的商品(包括未通过模板创建的商品)。
- 删除模板 ⇒ 仅软删该模板下的商品,不影响同分类其它模板的商品。
- 订单不可删除仅允许作废void因此采用“软删”是必要前提避免历史订单断裂。
- 恢复:当前不提供任何恢复入口;如未来开放,恢复不做级联,需逐层独立恢复以避免误恢复。
### 3. 设计要点
- 软删标记:使用 `products.deleted_at DATETIME NULL`(已存在)。被软删即视为“不对外可见”。
- 恢复:当前不提供恢复入口。若未来开放,语义为将 `deleted_at=NULL`
- 查询默认过滤:所有列表/搜索默认附加 `deleted_at IS NULL`(当前搜索已实现)。
- 详情访问:若记录被软删,返回 404或通过 `includeDeleted=true` 显式读取)。
- 关联表处理:软删商品时不物理删除图片/价格/库存/别名(均按商品引用读取,详情被 404 屏蔽即可)。
- 模板软删标记统一:为 `part_templates` 引入 `deleted_at DATETIME NULL` 以统一软删标记;`status` 字段保留为启停用,不代表软删。所有查询需同时过滤 `deleted_at IS NULL AND status=1`(按需)。
- 字典与作用域:分类与单位属于 `shop_id=0` 的全局字典。删除分类会影响所有店铺下此分类的模板与商品;此操作需平台管理员权限并要求二次确认。
- 报表与搜索:默认排除软删记录;不提供“含回收站”开关。
- 数据保留与清理:支持配置项 `SOFT_DELETE_RETENTION_DAYS`(默认永久保留,仅清理无引用对象)。
- 单位删除校验:移除对已废弃 `products.unit_id` 的校验逻辑。
### 4. 数据库与索引
现状:`products` 存在唯一约束 `UNIQUE(shop_id, barcode)`。软删后可能需要“同店铺、同条码”重新建商品。
- 目标:唯一约束仅作用于“活动记录”(未软删)。
- 做法:增加生成列 `is_active` 并重建唯一索引MySQL 8
DDL上线脚本草案
```sql
-- 仅对生产环境执行一次;如已存在请跳过对应步骤
ALTER TABLE products
ADD COLUMN is_active TINYINT AS (CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END) STORED,
ADD INDEX idx_products_deleted_at (deleted_at);
-- 重建唯一索引,使其仅约束未软删记录
DROP INDEX ux_products_shop_barcode ON products; -- 若不存在请忽略
CREATE UNIQUE INDEX ux_products_shop_barcode_live ON products(shop_id, barcode, is_active);
```
风险与说明
- “条码为空”不会受唯一约束影响MySQL 对 NULL 不唯一);符合预期。
- 老数据不受影响;后续删除改为软删即可。
- 若未来需要“永久删除”,可新增仅限平台运维的强删脚本,先清理关联,再物理删除目标商品。
- 如未来开放“恢复”,当恢复商品与现存“活动记录”在 `(shop_id, barcode)` 上冲突时,恢复应返回 `409 Conflict` 并附带冲突商品信息。
模板表 DDL新增软删标记
```sql
ALTER TABLE part_templates
ADD COLUMN deleted_at DATETIME NULL,
ADD INDEX idx_part_templates_deleted_at (deleted_at);
```
### 5. 接口设计OpenAPI 约定)
说明:按规范,等后端开始开发即补充到 `/doc/openapi.yaml` 并标注实现状态;本方案不新增任何“恢复”接口。
1) 软删商品(行为不变,明确语义)
- Method/Path: `DELETE /api/products/{id}`
- 语义:软删,将 `deleted_at=NOW()`
- 返回:`200 {}`
- 鉴权:需要 `X-Shop-Id`/`X-User-Id` 或 Token且仅允许同店铺数据。
2) 商品详情(行为调整)
- Method/Path: `GET /api/products/{id}`
- 默认:若 `deleted_at IS NOT NULL` 返回 `404`
- 可选:`includeDeleted=true` 时允许读取已软删详情(仅管理端使用)。
3) 恢复接口
- 不同意新增以下恢复接口:`PUT /api/admin/dicts/categories/{id}/restore``PUT /api/admin/part-templates/{id}/restore``PUT /api/products/{id}/restore`
### 6. 后端实现说明
- Controller 改动(示意)
- `ProductController.delete(id, shopId)`:保持现有调用,内部执行软删。
- `GET /api/products/{id}`:调用 `productService.findDetail(id)` 前,先判断 `deleted_at`,若非空且未显式 `includeDeleted``404`
- Service 改动(核心)
- 移除/不提供任何恢复相关方法。
- `findDetail(id)`:若被软删且无 `includeDeleted` 参数 → 返回空 Optional。
- 模板表采用 `deleted_at` 表示软删,`status` 表示启停用;查询需同时过滤 `deleted_at IS NULL` 与必要的 `status` 条件。
#### 6.1 级联软删伪代码
```java
// 分类软删
void deleteCategorySoft(Long categoryId) {
// 1) 标记分类 deleted_at
UPDATE product_categories SET deleted_at=NOW() WHERE id=? AND deleted_at IS NULL;
// 2) 级联模板软删(统一使用 deleted_at
UPDATE part_templates SET deleted_at=NOW() WHERE category_id=? AND deleted_at IS NULL;
// 3) 级联商品软删:模板创建的商品 + 直接挂在分类下的商品
UPDATE products SET deleted_at=NOW() WHERE (
template_id IN (SELECT id FROM part_templates WHERE category_id=?)
OR category_id=?
) AND deleted_at IS NULL;
}
// 模板软删(不波及其它模板)
void deleteTemplateSoft(Long templateId) {
// 1) 模板标记为软删
UPDATE part_templates SET deleted_at=NOW() WHERE id=? AND deleted_at IS NULL;
// 2) 级联商品软删(仅该模板下)
UPDATE products SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL;
}
```
### 7. 前端改动
- 列表页:保持不显示软删项(现已过滤)。
- 详情页:若接口返回 404提示“已被删除或无权限”。
- 管理端:不提供“回收站/恢复”入口;删除按钮提示:该操作为软删除,对前台不可见,当前无恢复入口。
### 8. 权限与审计
- 鉴权:沿用现有用户/店铺头部识别;仅同店铺商品可操作。
- 权限边界:
- 普通用户:仅可删除本用户的货品;无权删除模板与分类;无恢复权限。
- 店铺管理员:仅有审核功能;无删除模板/分类与恢复权限。
- 平台管理员:可删除货品、模板、分类;删除全局分类需二次确认;无恢复权限。
- 审计:不记录操作日志(操作者、时间、来源 IP、对象 ID 与名称),以简化开发。
### 9. 测试用例
- 删除后搜索不可见;`GET /api/products/{id}` 返回 404。
- 条码唯一:软删后允许同店铺同条码新建。
- (如未来开放恢复)恢复时如与现有活动记录冲突,返回 409 并附带冲突商品信息。
### 10. 发布与回滚
- 发布顺序:
1) 执行数据库 DDL生成列与索引
2) 上线后端(调整 detail 行为,移除/不提供恢复逻辑)。
3) 上线前端(不提供回收站/恢复入口)。
- 回滚:
- 后端回滚到旧版本DDL 不需要回退(生成列与新索引向前兼容)。
### 11. FAQ / 风险
- 问:软删后图片与价格是否清理?
- 答:不清理,保持数据可恢复;若永久删除再统一清理关联。
- 问:库存与统计是否包含软删商品?
- 答:常规统计应排除软删;如需包含,增加显式参数。
- 问:条码冲突如何处理?
- 答:按“活动记录”唯一;如未来开放恢复,发现冲突则返回 409并指明冲突商品。
- 问:字典(分类/单位)是否为全局维度?删除是否影响所有店铺?
- 答:是,`shop_id=0` 全局字典;删除全局分类会影响所有店铺下该分类的模板与商品,需平台管理员二次确认。
- 问:是否保留“强删”入口?
- 答:保留仅限平台运维的强删入口(默认关闭)。分类/模板强删前需校验无订单关联商品后再执行。
- 问:为何不做物理删除?
- 答:订单/流水等历史记录必须可追溯;物理删除会破坏外键与统计。软删能满足“前台不可见、后台可恢复”的业务诉求。
### 12. 任务拆解(实施)
- 后端:
- [ ] `GET /api/products/{id}` 软删返回 404 / 支持 `includeDeleted`
- [ ] 分类删除级联扩展:同时软删 `category_id=该分类` 的商品(含未走模板创建)
- [ ] 模板表引入 `deleted_at`;查询同时过滤 `deleted_at IS NULL` 与必要的 `status`
- [ ] 移除“单位删除校验检查 products.unit_id”的逻辑
- 数据库:
- [ ]`products` 增加 `is_active` 与唯一索引(见 DDL
- [ ]`part_templates` 增加 `deleted_at` 与索引
- 前端管理端:
- [ ] 删除按钮文案更新(软删除,对前台不可见,当前无恢复入口)
- [ ] 不提供“回收站/恢复”入口
(本文件为技术方案与实施指引,变更上线后请同步 `/doc/openapi.yaml``/doc/database_documentation.md`