9.5 KiB
货品删除功能开发文档(软删方案)
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(上线脚本草案)
-- 仅对生产环境执行一次;如已存在请跳过对应步骤
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(新增软删标记)
ALTER TABLE part_templates
ADD COLUMN deleted_at DATETIME NULL,
ADD INDEX idx_part_templates_deleted_at (deleted_at);
5. 接口设计(OpenAPI 约定)
说明:按规范,等后端开始开发即补充到 /doc/openapi.yaml 并标注实现状态;本方案不新增任何“恢复”接口。
- 软删商品(行为不变,明确语义)
- Method/Path:
DELETE /api/products/{id} - 语义:软删,将
deleted_at=NOW()。 - 返回:
200 {} - 鉴权:需要
X-Shop-Id/X-User-Id或 Token,且仅允许同店铺数据。
- 商品详情(行为调整)
- Method/Path:
GET /api/products/{id} - 默认:若
deleted_at IS NOT NULL返回404。 - 可选:
includeDeleted=true时允许读取已软删详情(仅管理端使用)。
- 恢复接口
- 不同意新增以下恢复接口:
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 级联软删伪代码
// 分类软删
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. 发布与回滚
- 发布顺序:
- 执行数据库 DDL(生成列与索引)。
- 上线后端(调整 detail 行为,移除/不提供恢复逻辑)。
- 上线前端(不提供回收站/恢复入口)。
- 回滚:
- 后端回滚到旧版本;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)