176 lines
9.5 KiB
Markdown
176 lines
9.5 KiB
Markdown
## 货品删除功能开发文档(软删方案)
|
||
|
||
### 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`)
|
||
|
||
|