3
This commit is contained in:
@@ -267,7 +267,7 @@
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| name | VARCHAR(120) | NOT NULL | | 供全文检索 |
|
||||
| category_id | BIGINT UNSIGNED | YES | | |
|
||||
| unit_id | BIGINT UNSIGNED | NOT NULL | | |
|
||||
| unit_id | (已移除) | | | |
|
||||
| template_id | BIGINT UNSIGNED | YES | | 关联的模板 |
|
||||
| brand | VARCHAR(64) | YES | | |
|
||||
| model | VARCHAR(64) | YES | | |
|
||||
@@ -291,8 +291,8 @@
|
||||
- safe_min/safe_max: 安全库存上下限
|
||||
- search_text: 聚合检索字段(触发器维护)
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_unit` (`unit_id`) - KEY: `idx_products_template` (`template_id`) - KEY: `idx_products_dedupe` (`dedupe_key`) - KEY: `idx_products_shop_blacklist` (`shop_id`,`is_blacklisted`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`) - UNIQUE: `ux_products_template_name_model` (`template_id`,`name`,`model`)
|
||||
**Foreign Keys**: - `fk_products_shop`: `shop_id` → `shops(id)` - `fk_products_user`: `user_id` → `users(id)` - `fk_products_category`: `category_id` → `product_categories(id)` - `fk_products_unit`: `unit_id` → `product_units(id)` - `fk_products_template`: `template_id` → `part_templates(id)` - `fk_products_globalsku`: `global_skus(id)`
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_template` (`template_id`) - KEY: `idx_products_dedupe` (`dedupe_key`) - KEY: `idx_products_shop_blacklist` (`shop_id`,`is_blacklisted`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`) - UNIQUE: `ux_products_template_name_model` (`template_id`,`name`,`model`)
|
||||
**Foreign Keys**: - `fk_products_shop`: `shop_id` → `shops(id)` - `fk_products_user`: `user_id` → `users(id)` - `fk_products_category`: `category_id` → `product_categories(id)` - `fk_products_template`: `template_id` → `part_templates(id)` - `fk_products_globalsku`: `global_skus(id)`
|
||||
|
||||
### part_submissions(配件提交与审核)
|
||||
| Column Name | Data Type | Nullable | Default | Comment |
|
||||
@@ -377,8 +377,9 @@
|
||||
| created_by_admin_id | BIGINT UNSIGNED | YES | | 创建管理员 |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
| deleted_at | DATETIME | YES | | 软删标记 |
|
||||
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_pt_category` (`category_id`) - KEY: `idx_pt_status` (`status`) - KEY: `idx_pt_admin` (`created_by_admin_id`)
|
||||
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_pt_category` (`category_id`) - KEY: `idx_pt_status` (`status`) - KEY: `idx_pt_admin` (`created_by_admin_id`) - KEY: `idx_part_templates_deleted_at` (`deleted_at`)
|
||||
**Foreign Keys**: - `fk_pt_category`: `category_id` → `product_categories(id)` - `fk_pt_admin`: `created_by_admin_id` → `admins(id)`
|
||||
|
||||
### part_template_params(模板参数字段)
|
||||
|
||||
@@ -728,6 +728,30 @@ paths:
|
||||
responses:
|
||||
'200': { description: 成功 }
|
||||
|
||||
/api/normal-admin/application/status:
|
||||
get:
|
||||
summary: 普通管理员-本人申请状态查询(✅ Fully Implemented)
|
||||
description: 返回当前用户最近一次申请状态与是否已具备普通管理员权限。
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-User-Id
|
||||
required: true
|
||||
schema: { type: integer, format: int64 }
|
||||
description: 当前登录用户ID
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
isNormalAdmin: { type: boolean }
|
||||
applicationStatus: { type: string, enum: [none, pending, approved, rejected, revoked] }
|
||||
lastAction: { type: string, nullable: true }
|
||||
lastActionAt: { type: string, format: date-time, nullable: true }
|
||||
lastRemark: { type: string, nullable: true }
|
||||
|
||||
/api/admin/normal-admin/applications:
|
||||
get:
|
||||
summary: 平台-普通管理员申请列表(❌ Partially Implemented)
|
||||
@@ -1149,12 +1173,18 @@ paths:
|
||||
/api/products:
|
||||
get:
|
||||
summary: 商品搜索(✅ Fully Implemented)
|
||||
description: 支持 kw/page/size/categoryId;返回 {list:[]} 以兼容前端。
|
||||
description: 支持 kw/page/size/categoryId/templateId 以及模板参数过滤。模板参数以 param_ 前缀传入,如 param_颜色=黑、param_内径=10;后端对 JSON attributes 进行 LIKE 匹配(字符串化),多个参数为 AND 关系。返回 {list:[]} 以兼容前端。
|
||||
parameters:
|
||||
- in: query
|
||||
name: kw
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: categoryId
|
||||
schema: { type: integer, format: int64 }
|
||||
- in: query
|
||||
name: templateId
|
||||
schema: { type: integer, format: int64 }
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
@@ -1165,6 +1195,11 @@ paths:
|
||||
schema:
|
||||
type: integer
|
||||
default: 50
|
||||
- in: query
|
||||
name: param_*
|
||||
schema:
|
||||
type: string
|
||||
description: 模板参数过滤,星号代表任意模板参数键;示例 param_颜色=黑
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
@@ -1204,11 +1239,16 @@ paths:
|
||||
/api/products/{id}:
|
||||
get:
|
||||
summary: 商品详情(✅ Fully Implemented)
|
||||
description: 默认对软删记录返回 404;仅当 includeDeleted=true 时返回已软删详情(仅管理端使用)。
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
- in: query
|
||||
name: includeDeleted
|
||||
required: false
|
||||
schema: { type: boolean, default: false }
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
@@ -2232,7 +2272,6 @@ paths:
|
||||
spec: { type: string, nullable: true }
|
||||
origin: { type: string, nullable: true }
|
||||
barcode: { type: string, nullable: true }
|
||||
unitId: { type: integer, format: int64, nullable: true }
|
||||
categoryId: { type: integer, format: int64, nullable: true }
|
||||
parameters: { type: object, additionalProperties: true, nullable: true }
|
||||
images:
|
||||
@@ -2311,7 +2350,6 @@ paths:
|
||||
spec: { type: string, nullable: true }
|
||||
origin: { type: string, nullable: true }
|
||||
barcode: { type: string, nullable: true }
|
||||
unitId: { type: integer, format: int64, nullable: true }
|
||||
categoryId: { type: integer, format: int64, nullable: true }
|
||||
parameters: { type: object, additionalProperties: true, nullable: true }
|
||||
images:
|
||||
@@ -2401,7 +2439,6 @@ paths:
|
||||
name: { type: string, nullable: true }
|
||||
brand: { type: string, nullable: true }
|
||||
spec: { type: string, nullable: true }
|
||||
unitId: { type: integer, format: int64, nullable: true }
|
||||
categoryId: { type: integer, format: int64, nullable: true }
|
||||
parameters: { type: object, additionalProperties: true, nullable: true }
|
||||
images:
|
||||
@@ -2482,7 +2519,7 @@ paths:
|
||||
|
||||
/api/admin/part-templates:
|
||||
get:
|
||||
summary: 管理端-模板列表(❌ Partially Implemented)
|
||||
summary: 管理端-模板列表(✅ Fully Implemented)
|
||||
responses: { '200': { description: 成功 } }
|
||||
post:
|
||||
summary: 管理端-创建模板(❌ Partially Implemented)
|
||||
@@ -2514,11 +2551,27 @@ paths:
|
||||
responses: { '200': { description: 成功 } }
|
||||
/api/admin/part-templates/{id}:
|
||||
get:
|
||||
summary: 管理端-模板详情(❌ Partially Implemented)
|
||||
summary: 管理端-模板详情(✅ Fully Implemented)
|
||||
parameters: [ { in: path, name: id, required: true, schema: { type: integer, format: int64 } } ]
|
||||
responses: { '200': { description: 成功 } }
|
||||
put:
|
||||
summary: 管理端-更新模板(❌ Partially Implemented)
|
||||
delete:
|
||||
summary: 管理端-删除模板(软删除,✅ Fully Implemented)
|
||||
description: |-
|
||||
默认行为:软删除(隐藏)——仅将 `part_templates.status` 置为 0,前台列表默认不再显示。
|
||||
强制模式:`force=true` 时,执行永久删除:删除参数定义并清理关联商品(软删)与提交(软删),最后删除模板记录。
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer, format: int64 }
|
||||
- in: query
|
||||
name: force
|
||||
required: false
|
||||
schema: { type: boolean }
|
||||
responses:
|
||||
'200': { description: 成功 }
|
||||
parameters: [ { in: path, name: id, required: true, schema: { type: integer, format: int64 } } ]
|
||||
requestBody:
|
||||
required: true
|
||||
|
||||
127
doc/模板参数可模糊查询_功能需求文档.md
Normal file
127
doc/模板参数可模糊查询_功能需求文档.md
Normal file
@@ -0,0 +1,127 @@
|
||||
## 模板参数可模糊查询(±容差)功能需求文档
|
||||
|
||||
### 1. 背景与目标
|
||||
当前用户端「按模板参数查询」要求参数值与数据库完全相同才能命中,实际使用中数值类参数(如内径、外径、长度等)存在测量/录入微小误差,严格等值导致命中率偏低。新增能力:在管理端创建模板时,为每个参数提供「可模糊查询」选项;开启后,用户搜索该参数时按数值区间匹配(±容差);未开启的参数继续精确等值。
|
||||
|
||||
### 2. 业务范围
|
||||
- 场景:用户端/管理端的商品列表查询(含「按模板参数查询」模式)。
|
||||
- 对象:模板参数定义(仅限数值型参数生效)。
|
||||
- 不影响:名称/品牌/型号/规格关键字搜索逻辑;非数值类型参数的等值匹配逻辑。
|
||||
|
||||
### 3. 术语与约束
|
||||
- 模板参数类型:string/number/boolean/enum/date。
|
||||
- 模糊查询仅对 type=number 生效;其他类型不展示该选项或忽略配置。
|
||||
- 容差(tolerance):对搜索入参 v,匹配区间为 \[v - tolerance, v + tolerance](闭区间)。默认容差为 1(见配置项),可在参数层级单独覆盖。
|
||||
- 组合关系:多参数为 AND 关系;每个参数根据其「可模糊查询」与容差独立计算。
|
||||
|
||||
### 4. 交互与流程
|
||||
- 管理端-模板配置:
|
||||
- 新建/编辑模板参数时,新增选项:
|
||||
- 可模糊查询(开关,仅当类型为 number 显示)
|
||||
- 容差值(number,>0,显示单位提示,同 `unit` 字段;当开关开启时必填,否则置空)
|
||||
- 校验:
|
||||
- type≠number 时禁止开启;
|
||||
- 容差必须为正数,支持小数;
|
||||
- 可保存为“使用平台默认容差”,当字段留空时后端落默认(见配置)。
|
||||
- 用户端/管理端-按模板参数查询:
|
||||
- 入参与现状一致:仍以 `templateId` + 多个 `param_*` 传参;
|
||||
- 行为变化:
|
||||
- 对应参数若开启可模糊查询:按区间 \[v - tol, v + tol] 比较;
|
||||
- 否则:仍为精确等值比较。
|
||||
|
||||
### 5. 数据模型变更(待实施)
|
||||
- 表:`part_template_params`
|
||||
- 新增列:
|
||||
- `fuzzy_searchable` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许模糊查询(仅数值型)'
|
||||
- `fuzzy_tolerance` DECIMAL(18,6) NULL COMMENT '容差;NULL 表示使用平台默认容差'
|
||||
- 说明:
|
||||
- 仅当 `type='number' AND fuzzy_searchable=1` 时才使用容差;
|
||||
- 初始迁移将全部历史记录置为 `fuzzy_searchable=0, fuzzy_tolerance=NULL`,保持现有行为不变。
|
||||
|
||||
### 6. 配置项(后端)
|
||||
- `search.fuzzy.enabled`(bool,默认 true):是否启用模糊查询全局开关;
|
||||
- `search.fuzzy.defaultTolerance`(decimal,默认 1.0):当参数未设置 `fuzzy_tolerance` 时使用;
|
||||
- 读取途径:Spring 配置(application.properties/yaml)或环境变量。禁止在代码中硬编码数字 1。
|
||||
- 仅全局配置,不支持租户级(`system_parameters`)覆盖;无需设置小数精度上限/最大容差限制。
|
||||
|
||||
### 7. 接口协议与兼容性
|
||||
- 查询接口:`GET /api/products`(已存在)
|
||||
- 入参保持不变:`templateId`、`param_*`。
|
||||
- 语义扩展(无须 `templateId` 也启用模糊):后端将基于商品行的 `template_id` 与参数定义逐行判定某个 `param_*` 是否启用 ±容差;若该参数在对应模板中未开启模糊或非数值型,则对该条件执行等值匹配。
|
||||
- 模板接口:`POST /api/admin/part-templates`、`PUT /api/admin/part-templates/{id}`(已存在)
|
||||
- 参数定义对象新增字段:
|
||||
- `fuzzySearchable`(boolean)
|
||||
- `fuzzyTolerance`(number,nullable)
|
||||
- 若前端暂未改造,后端默认按 `fuzzySearchable=false` 处理,兼容旧请求体。
|
||||
|
||||
(根据「接口规范生效条件」,待功能开发完成后更新 `doc/openapi.yaml` 中对应 schema 与描述,并在 summary/description 标注实现状态)
|
||||
|
||||
### 8. 后端实现要点(建议方案)
|
||||
当前实现(精确匹配),示意:
|
||||
```sql
|
||||
-- 现状(等值):
|
||||
AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, '$.内径')) = '10'
|
||||
```
|
||||
|
||||
推荐实现:
|
||||
- 行级判定方案(支持无 `templateId` 也启用模糊):
|
||||
- 对每个传入的 `param_<key>=v`:
|
||||
- 以 `EXISTS` 子查询或 `JOIN part_template_params ptp ON ptp.template_id=p.template_id AND ptp.field_key='<key>'` 获取参数定义;
|
||||
- 若 `ptp.type='number' AND ptp.fuzzy_searchable=1`:对 `v` 解析为数值,计算 `tol = COALESCE(ptp.fuzzy_tolerance, :defaultTolerance)`;
|
||||
- 下限截断:`lower = GREATEST(0, v - tol)`;
|
||||
- 条件:
|
||||
```sql
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, '$.<key>')) AS DECIMAL(18,6)) BETWEEN :lower AND :upper
|
||||
```
|
||||
- 否则:执行等值匹配:
|
||||
```sql
|
||||
JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, '$.<key>')) = :val
|
||||
```
|
||||
- 快路径(可选):当请求携带 `templateId` 时,可先一次性加载该模板参数定义映射到内存,按映射决定每个条件构造,以减少 `JOIN/EXISTS` 次数。
|
||||
- 容差取值:优先 `ptp.fuzzy_tolerance`,否则全局 `search.fuzzy.defaultTolerance`。
|
||||
|
||||
性能建议:
|
||||
- 初期:允许全表扫描 + JSON_EXTRACT;观察真实 QPS 与延迟;
|
||||
- 进阶(可选):对热点参数引入“生成列 + 索引”(Generated Column),例如:
|
||||
- 在 `products` 增加 `attr_<key> DECIMAL(18,6) GENERATED ALWAYS AS (CAST(JSON_UNQUOTE(JSON_EXTRACT(attributes_json, '$.<key>')) AS DECIMAL(18,6))) STORED` 并建索引,以支持范围查询;
|
||||
- 仅对访问量高的少数参数启用,避免列爆炸。
|
||||
|
||||
### 9. 管理端实现要点(UI/校验)
|
||||
- `admin/src/views/parts/Templates.vue`:
|
||||
- 参数编辑行新增:
|
||||
- 开关:可模糊查询(仅 type=number 显示)
|
||||
- 数值输入:容差(显示单位,>0,支持小数;留空表示使用平台默认)
|
||||
- 保存/加载兼容:与后端新增字段映射,历史数据默认显示为关闭态。
|
||||
- 校验:当参数开启模糊时,对应值在 UI 侧仅允许数字输入;单位提示与 `unit` 一致。
|
||||
|
||||
### 10. 验收标准(Test Cases)
|
||||
- 单参数-模糊:模板字段 `内径`(number,fuzzy=true,tolerance=1);商品 A/B/C 分别取值 9/10/11;搜索 `param_内径=10` 命中 A/B/C。
|
||||
- 单参数-精确:同上但 fuzzy=false;搜索 `param_内径=10` 仅命中 B。
|
||||
- 多参数组合:`内径`(fuzzy=true, tol=0.5)、`长度`(fuzzy=false);搜索 `param_内径=10`、`param_长度=20` 仅命中满足区间与等值的交集。
|
||||
- 无 templateId:也启用模糊;后端逐行按 `p.template_id` 与参数定义判定是否应用容差。
|
||||
- 容差来源:当 `fuzzy_tolerance=NULL` 时,生效平台默认容差;覆盖值生效优先级高于默认。
|
||||
- 非数值参数:即使请求携带 `param_颜色=黑`,也严格等值。
|
||||
- 下限截断:当 `v - tol < 0` 时,以 `0` 作为下限;不支持负数参数匹配。
|
||||
- 非法输入:当参数在模板中开启模糊但请求值非数字时,返回 400(Bad Request)。
|
||||
|
||||
### 11. 兼容与回退
|
||||
- 不改动现有请求入参与返回体,历史客户端无需升级亦可按原精确逻辑使用;
|
||||
- 新能力由模板参数配置显式开启,可随时在模板中关闭;
|
||||
- 如需全局关闭,可通过 `search.fuzzy.enabled=false` 临时禁用(后端配置)。
|
||||
|
||||
### 12. 风险与注意事项
|
||||
- 数据质量:历史 `attributes_json` 中数值可能以字符串存储;需统一以 `CAST(JSON_UNQUOTE(...))` 解析。
|
||||
- 单位与容差:UI 需提示单位;容差与单位一一对应,避免“毫米 vs 厘米”误解。
|
||||
- 性能:范围查询较等值更难走索引;必要时引入“生成列+索引”优化热点字段。
|
||||
- 负数与边界:不支持负数参数;区间采用闭区间,且下限截断为 `0`。
|
||||
|
||||
### 13. 实施清单(参考)
|
||||
1) 数据库:为 `part_template_params` 增列 `fuzzy_searchable`、`fuzzy_tolerance`;(变更需通过 MysqlMCP,成功后同步更新 `doc/database_documentation.md` 与 `backend/db/db.sql`)
|
||||
2) 配置:新增 `search.fuzzy.*` 配置项并给出默认值(全局生效,无租户级覆盖);
|
||||
3) 管理端:模板参数编辑 UI 新增开关与容差输入;
|
||||
4) 后端:按 8 节改造查询 SQL 构建逻辑(无 `templateId` 也启用模糊,行级按模板判定);
|
||||
5) 文档:在功能开发完成后更新 `doc/openapi.yaml` 中模板参数 schema 与 `GET /api/products` 的查询规则说明,并标注实现状态;
|
||||
6) 发布:前后端同步上线;无需灰度与回滚开关;
|
||||
7) 验收:按 10 节用例覆盖单测/集成测试与手工回归。
|
||||
|
||||
|
||||
175
doc/货品删除功能开发文档.md
Normal file
175
doc/货品删除功能开发文档.md
Normal 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`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user