Files
PartsInquiry/doc/货品删除功能开发文档.md
2025-09-29 21:38:32 +08:00

176 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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