This commit is contained in:
2025-09-27 22:57:59 +08:00
parent 8a458ff0a4
commit ed26244cdb
12585 changed files with 1914308 additions and 3474 deletions

View File

@@ -1,59 +1,28 @@
# 管理端开发文档
## 管理端开发说明(当前实现)
本文档用于同步当前管理端admin/)前端与数据库的开发状态与下一步计划。
### 1. 模块概览
- **VIP 系统**`/vip/system` 管理统一售价与充值记录,`/vip/list` 维护店铺会员状态。价格管理页面引用 `/api/admin/vip/system/price`,充值列表读取 `/api/admin/vip/system/recharges`
- **公告管理**`/notice/list` 支持公告检索、创建、编辑、发布与下线,对应接口 `/api/admin/notices` 及其子路径。
- **咨询回复**`/consult` 基于 `/api/admin/consults` 完成列表、单次回复、标记已解决。
- **用户/配件/供应商**`/users``/parts``/supplier` 依次调用 `/api/admin/users``/api/admin/parts``/api/suppliers` 进行检索与维护。
- **附件与图片**:图片上传统一走 `/api/attachments`,列表页展示时需通过 `withBaseUrl()` 处理相对路径。
## 一、前端admin/
- 技术栈Vue3 + Vite + Element Plus
- 主题:深色尊贵风格,数据展示面板/卡片/表格采用深蓝背景Tag 颜色整体降亮
- 路由与页面:
- `/vip-review`VIP审核默认页卡片式一卡一申请
- `/vip`VIP管理是否VIP、到期时间、启用/停用)
- `/users`:用户管理(拉黑/恢复,状态文案“正常/黑名单”)
- `/parts`:用户配件管理(拉黑/恢复)
- `/consult`:咨询回复(回复/标记解决,标记后行内即时显示“已解决”)
- Mock 数据:
- 开启:`VITE_USE_MOCK=true`;使用 `public/mock/*.json`
- 覆盖:`admin_vips.json``admin_vips_reviews.json``admin_users.json``admin_parts.json``admin_consults.json`
- 状态:
- 页面与交互:已完成
- 主题色与降亮:已完成
- Mock已完成
### 2. 认证与上下文
- 后端默认启用 `AdminAuthInterceptor`,优先校验 Bearer Token未登录情况下回退到请求头 `X-Admin-Id``X-User-Id`
- 管理端前端目前仍使用本地存储写入 `USER_ID`/`ADMIN_ID`(详见 `admin/src/api/http.ts`);接入登录页时需改为在登录成功后写入 Token 并移除默认 ID
- 所有请求必须附带 `X-Shop-Id`(默认 1可在本地存储或环境变量覆盖以匹配租户范围。
## 二、数据库DB
- 已有与复用
- `users.status`1=正常、0=黑名单(供用户管理拉黑/恢复)
- `global_skus`公共SKU库供未来配件审核通过后发布使用
- 新增表
- `vip_users`管理VIPis_vip/status/expire_at/reviewer_id/reviewed_at/remark
- `consults``consult_replies`:咨询与回复
- 字段与索引改动
- `products.is_blacklisted TINYINT(1) NOT NULL DEFAULT 0`(黑名单标记)
- 索引:`idx_products_shop_blacklist (shop_id, is_blacklisted)`
- 文档:`doc/database_documentation.md` 已同步
### 3. 接口要点
- **VIP 列表**`GET /api/admin/vips` 已落地;`POST /api/admin/vips` 需要传入 `shopId``userId` 才能成功创建。
- **VIP 价格**`GET/PUT /api/admin/vip/system/price` 会清空 `vip_price` 再写入单条记录,不允许并行修改;可考虑后续改为 `UPDATE` 语句。
- **VIP 充值**`GET /api/admin/vip/system/recharges` 支持关键字(姓名/手机号)过滤,默认按创建时间倒序。
- **公告管理**:创建与更新均支持标签、置顶、时间窗,未填写时间默认为即时生效/长期有效。
- **附件上传**:上传成功返回 `url` 与元信息,若需要落库请在业务表维持引用;多次上传同一文件会复用记录(按 hash 去重)。
## 三、API 规范OpenAPI
- 已补充
- 管理端VIP`/api/admin/vips` 列表/创建/更新/审核通过/驳回(标注“❌ Partially Implemented”
- 待补充
- 管理端用户列表kw、更新status 拉黑/恢复)
- 管理端用户配件列表kw/status、黑名单/恢复(更新 `products.is_blacklisted`
- 管理端咨询列表status/kw、回复、标记解决
## 四、落地与联调建议
- 后端接口:按上述待补充接口实现,完成后将 OpenAPI 的相应条目更新为“✅ Fully Implemented”。
- 前台/小程序:默认过滤 `products.is_blacklisted=0`,防止黑名单配件外显。
- 安全收口:后端增加 JWT 校验,从 Token 注入 `shopId/userId`,避免完全信任请求头。
## 五、环境与运行
- 开发:
```powershell
cd admin; npm i; $env:VITE_APP_API_BASE_URL="http://127.0.0.1:8080"; $env:VITE_APP_SHOP_ID="1"; npm run dev
```
- Mock
```powershell
cd admin; npm i; $env:VITE_USE_MOCK="true"; npm run dev
```
- 构建/预览:
```powershell
npm run build; npm run preview
```
### 4. 条码识别接入
- 管理端不提供扫码入口,仅用户端调用 `/api/barcode/scan`
- 后端代理服务会将图片转发至 Python TXMFastAPI并返回首个匹配条码需保持 Java 与 Python 两侧的 `PY_BARCODE_MAX_UPLOAD_MB` 一致。
- 部署时应通过环境变量配置:
- `PY_BARCODE_HOST`/`PY_BARCODE_PORT`Python 服务地址,默认 `127.0.0.1:8000`
- `PY_BARCODE_MAX_UPLOAD_MB`:上传大小限制,默认 8MB。
- 小程序端调用扫码接口需将后端域名加入“request 合法域名”并启用 HTTPS。

View File

@@ -1,59 +1,26 @@
# 管理端需求说明(配件管家)
# 管理端需求文档
本文件描述管理端前端与后端的需求要点,遵循“默认同意,管理端可拉黑/恢复”的运营策略;所有可配置值放置在配置或数据库,不允许硬编码。
## 1. 页面结构
- **VIP 系统**:含价格配置、充值记录与会员列表三块;价格修改需立即生效并同步到前端 `VIP_PRICE` 显示;列表支持手机号模糊检索。
- **公告管理**:支持公告的新增、编辑、发布、下线、置顶,字段包括标题、内容、标签、有效期、生效状态。
- **咨询回复**:列出店铺咨询,管理员可单次回复并标记已解决。
- **用户管理**:展示用户基本信息,支持编辑、启停(黑名单)。
- **用户配件管理**:适配用户提交的配件数据,允许管理员编辑品牌/型号/规格与图片链接。
- **供应商管理**:列表检索、创建、编辑供应商信息,含欠款字段展示。
- **主数据字典**:主单位、主类别维护入口,仅平台管理员可用。
## 1. 运营策略
- 用户与用户配件(投稿/商品)默认允许展示与使用
- 管理端具备拉黑与恢复能力:
- 拉黑后:对应对象在前台/小程序/客户侧不可见或被限制
- 恢复后:恢复正常可用状态。
## 2. 认证约束
- 所有接口通过 `AdminAuthInterceptor` 鉴权:优先 Bearer Token其次 `X-Admin-Id`/`X-User-Id`
- 请求必须携带 `X-Shop-Id`,缺省取 1多租户数据严格隔离。
- 登录功能正在开发中,当前临时通过本地存储写入管理员 ID
## 2. 数据模型
### 2.1 用户黑名单
- 复用 `users.status` 字段:`1=正常``0=黑名单`
- 管理端动作:
- 拉黑用户 → `PUT /api/admin/users/{id}` body `{ status: 0 }`
- 恢复用户 → `PUT /api/admin/users/{id}` body `{ status: 1 }`
## 3. 功能约束
- 禁止硬编码配置值,价格、库存等需从后端接口读取。
- 上传图片统一调用 `/api/attachments`,返回的 `url` 需落库或直接引用
- 所有列表接口支持分页(默认 `page=1&size=20`);前端需预留分页扩展点。
- 删除功能未启用,均以启停或逻辑状态位代替。
### 2.2 用户配件黑名单
- 建议在承载表中增加状态位供管理端控制:
- 若使用 `products` 承载:新增 `products.is_blacklisted TINYINT(1) NOT NULL DEFAULT 0`(黑名单标记)
- 若使用专表(如未来 `part_submissions`/`user_parts`):同样增加 `is_blacklisted`
- 索引建议:`KEY idx_products_shop_blacklist (shop_id, is_blacklisted)`,便于后台筛选。
- 管理端动作:
- 拉黑配件 → `PUT /api/admin/parts/{id}/blacklist`
- 恢复配件 → `PUT /api/admin/parts/{id}/restore`
## 3. 前端admin/
- 路由:
- 用户管理:列表提供“拉黑/恢复”按钮,状态显示“正常/黑名单”。
- 用户配件管理:列表提供“拉黑/恢复”按钮,状态显示“正常/黑名单”。
- Mock
- 开启 `VITE_USE_MOCK=true` 使用 `public/mock/*.json`
## 4. 后端接口OpenAPI 标注)
- 用户:
- `GET /api/admin/users?kw=` → 列表(分页)
- `PUT /api/admin/users/{id}` → 修改 `status`/`name`/`phone`/`role`
- 用户配件:
- `GET /api/admin/parts?kw=&status=` → 列表(分页,支持按 `is_blacklisted` 过滤)
- `PUT /api/admin/parts/{id}/blacklist` → 标记黑名单
- `PUT /api/admin/parts/{id}/restore` → 取消黑名单
- 咨询服务:
- `GET /api/admin/consults?status=&kw=` → 列表
- `POST /api/admin/consults/{id}/reply` → 回复
- `PUT /api/admin/consults/{id}/resolve` → 标记解决(前端需即时将该行状态改为 resolved
说明OpenAPI 需更新到 `doc/openapi.yaml` 并在 summary 中标注“❌ Partially Implemented”。
## 5. 前台/小程序侧行为
- 用户被拉黑:登录受限或仅保留最低权限(由业务规则定义);其数据不可见或只读。
- 配件被拉黑:前台与小程序配件列表/搜索/选择均需基于 `is_blacklisted=0` 过滤。
## 6. 审计与可观测性
- 建议记录操作日志(操作者、对象、操作、时间),便于追责与回溯。
- 建议在管理端提供简单的筛选/导出功能。
## 7. 迁移计划
- 若使用 `products`:新增字段 `is_blacklisted` 并补齐索引;前台/小程序查询添加过滤条件。
- 若采用专表方案:保持同名字段与相同行为,编写同步脚本将历史数据初始化为非黑名单。
## 4. 未完成功能
- 管理端登录页与权限粒度控制尚未上线。
- 配件审核流仅完成基础编辑,驳回/通过流程待接入
- 公告板目前缺少多语言与富文本支持

View File

@@ -1,8 +1,12 @@
## partsinquiry 数据库文档
更新日期2025-09-23已插入演示数据新增演示店B/演示店C与其默认账户、两名用户
更新日期2025-09-27对齐远程线上库结构提醒`backend/db/db.sql` 尚未覆盖 VIP 相关表,请勿直接依赖本地脚本
说明:本文件根据远程库 mysql.tonaspace.com 中 `partsinquiry` 的实际结构生成,字段/索引/外键信息以线上为准。
说明:本文件根据远程库 mysql.tonaspace.com 中 `partsinquiry` 的实际结构生成,字段/索引/外键信息以线上为准。如需执行结构变更,请通过 MysqlMCP并在成功后更新此文档和 `backend/db/db.sql`。差异概览:
- ✅ 线上已存在 `vip_users``vip_price``vip_recharges``attachments` 表;`backend/db/db.sql` 仍缺少对应建表语句。
- ✅ 线上 `admins` 表存储平台管理员,管理端接口通过 `AdminAuthInterceptor` 校验。
-`global_skus` 及配件审核体系仅部分表有数据,审批流程仍在试运行阶段。
- ✅ 新增“模板化配件参数”相关结构:`part_templates``part_template_params`;为 `products``part_submissions` 增加 `template_id``dedupe_key`,并建立唯一与辅助索引。
### shops
| Column Name | Data Type | Nullable | Default | Comment |
@@ -27,23 +31,39 @@
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 用户ID |
| shop_id | BIGINT UNSIGNED | NOT NULL | | 所属店铺 |
| phone | VARCHAR(32) | YES | | 手机号 |
| email | VARCHAR(128) | YES | | 邮箱 |
| name | VARCHAR(64) | NOT NULL | | 姓名 |
| role | VARCHAR(32) | NOT NULL | staff | 角色owner/staff/finance/... |
| password_hash | VARCHAR(255) | YES | | 密码哈希(若采用短信登录可为空) |
| status | TINYINT UNSIGNED | NOT NULL | 1 | 状态1启用 0停用 |
| is_owner | TINYINT(1) | NOT NULL | 0 | 是否店主 |
| is_platform_admin | TINYINT(1) | NOT NULL | 0 | 是否平台管理员 |
(字段已调整:移除 `is_platform_admin`,平台管理员改为独立表 `admins`
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_users_shop` (`shop_id`) - UNIQUE: `uk_users_phone` (`phone`) - UNIQUE: `ux_users_shop_phone` (`shop_id`,`phone`)
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_users_shop` (`shop_id`) - UNIQUE: `uk_users_phone` (`phone`) - UNIQUE: `ux_users_shop_phone` (`shop_id`,`phone`) - UNIQUE: `ux_users_email` (`email`)
字段说明:
- shop_id: 归属店铺
- role: 角色标识字符串
- is_owner: 是否店主标记
- is_platform_admin: 是否平台管理员(平台级权限)
- 平台管理员:请参见 `admins`
### admins
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 管理员ID |
| username | VARCHAR(64) | NOT NULL | | 登录名/展示名 |
| phone | VARCHAR(32) | YES | | 手机号 |
| password_hash | VARCHAR(255) | YES | | 密码哈希 |
| status | TINYINT UNSIGNED | NOT NULL | 1 | 状态1启用 0停用 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
说明:平台管理员不隶属店铺,不具备店铺资源(店铺/配件)归属,仅用于管理端功能。
**Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_admins_username` (`username`) - UNIQUE: `ux_admins_phone` (`phone`)
- 其余同名含义
### user_identities
@@ -109,6 +129,38 @@
说明:该表为全局配置表,仅包含一条记录用于表示当前 VIP 单月价格。
### vip_recharges
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 充值记录ID |
| shop_id | BIGINT UNSIGNED | NOT NULL | | 店铺ID |
| user_id | BIGINT UNSIGNED | NOT NULL | | 用户ID |
| price | DECIMAL(10,2) | NOT NULL | | 本次充值价格(元) |
| duration_days | INT | NOT NULL | | 本次续期天数 |
| expire_from | DATETIME | YES | | 生效前到期时间(可空) |
| expire_to | DATETIME | NOT NULL | | 生效后到期时间 |
| channel | VARCHAR(32) | NOT NULL | oneclick | 渠道oneclick/…) |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_vr_shop` (`shop_id`) - KEY: `idx_vr_user` (`user_id`)
**Foreign Keys**: - `fk_vr_shop`: `shop_id``shops(id)` - `fk_vr_user`: `user_id``users(id)`
### normal_admin_audits普通管理员申请/审批审计日志)
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | 店铺 |
| user_id | BIGINT UNSIGNED | NOT NULL | | 用户 |
| action | ENUM('apply','approve','reject','revoke','expire') | NOT NULL | | 操作类型 |
| remark | VARCHAR(255) | YES | | 备注 |
| operator_admin_id | BIGINT UNSIGNED | YES | | 平台管理员IDapply时可空 |
| previous_role | VARCHAR(32) | YES | | 变更前角色 |
| new_role | VARCHAR(32) | YES | | 变更后角色 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 创建时间 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_naudit_shop_time` (`shop_id`,`created_at`) - KEY: `idx_naudit_user_time` (`user_id`,`created_at`)
**Foreign Keys**: - `fk_naudit_shop`: `shop_id``shops(id)` - `fk_naudit_user`: `user_id``users(id)` - `fk_naudit_admin`: `operator_admin_id``admins(id)`
### wechat_sessions
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
@@ -216,11 +268,13 @@
| name | VARCHAR(120) | NOT NULL | | 供全文检索 |
| category_id | BIGINT UNSIGNED | YES | | |
| unit_id | BIGINT UNSIGNED | NOT NULL | | |
| template_id | BIGINT UNSIGNED | YES | | 关联的模板 |
| brand | VARCHAR(64) | YES | | |
| model | VARCHAR(64) | YES | | |
| spec | VARCHAR(128) | YES | | |
| origin | VARCHAR(64) | YES | | |
| barcode | VARCHAR(32) | YES | | |
| dedupe_key | VARCHAR(512) | YES | | 去重键(规范化后计算) |
| alias | VARCHAR(120) | YES | | |
| is_blacklisted | TINYINT(1) | NOT NULL | 0 | 黑名单标记(管理端可控) |
| description | TEXT | YES | | |
@@ -237,8 +291,43 @@
- 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_shop_blacklist` (`shop_id`,`is_blacklisted`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`)
**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_globalsku`: `global_sku_id` `global_skus(id)`
**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)`
### part_submissions配件提交与审核
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 提交ID |
| shop_id | BIGINT UNSIGNED | NOT NULL | | 店铺 |
| user_id | BIGINT UNSIGNED | NOT NULL | | 提交用户 |
| name | VARCHAR(255) | YES | | 配件名称 |
| external_code | VARCHAR(255) | YES | | 外部编码 |
| model_unique | VARCHAR(255) | NOT NULL | | 规范化型号(唯一校验) |
| brand | VARCHAR(64) | YES | | 品牌 |
| spec | VARCHAR(128) | YES | | 规格 |
| unit_id | BIGINT UNSIGNED | YES | | 单位 |
| category_id | BIGINT UNSIGNED | YES | | 分类 |
| template_id | BIGINT UNSIGNED | YES | | 模板 |
| attributes | JSON | YES | | 参数JSON |
| images | JSON | YES | | 图片URL数组JSON |
| size | VARCHAR(64) | YES | | 兼容历史字段 |
| aperture | VARCHAR(64) | YES | | 兼容历史字段 |
| compatible | VARCHAR(255) | YES | | 兼容机型文本 |
| barcode | VARCHAR(32) | YES | | 条码 |
| dedupe_key | VARCHAR(512) | YES | | 去重键 |
| remark | TEXT | YES | | 备注 |
| status | ENUM('pending','approved','rejected') | NOT NULL | | 审核状态 |
| reviewer_id | BIGINT UNSIGNED | YES | | 审核人 |
| product_id | BIGINT UNSIGNED | YES | | 关联生成的商品ID |
| global_sku_id | BIGINT UNSIGNED | YES | | 关联全局SKU |
| reviewed_at | DATETIME | YES | | 审核时间 |
| review_remark | VARCHAR(255) | 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_ps_template` (`template_id`) - KEY: `idx_ps_dedupe` (`dedupe_key`) - UNIQUE: `ux_ps_template_name_model` (`template_id`,`name`,`model_unique`)
**Foreign Keys**: - `fk_ps_template`: `template_id``part_templates(id)`
### product_aliases
| Column Name | Data Type | Nullable | Default | Comment |
@@ -277,6 +366,41 @@
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_product_images_product` (`product_id`) - UNIQUE: `ux_product_image_hash` (`product_id`,`hash`)
**Foreign Keys**: - `fk_pimg_shop`: `shop_id``shops(id)` - `fk_pimg_user`: `user_id``users(id)` - `fk_pimg_product`: `product_id``products(id)` ON DELETE CASCADE
### part_templates配件模板
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 模板ID |
| category_id | BIGINT UNSIGNED | NOT NULL | | 绑定分类 |
| name | VARCHAR(120) | NOT NULL | | 配件名 |
| model_rule | VARCHAR(255) | YES | | 型号规则(说明/正则,可空) |
| status | TINYINT UNSIGNED | NOT NULL | 1 | 1启用 0停用 |
| created_by_admin_id | BIGINT UNSIGNED | YES | | 创建管理员 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_pt_category` (`category_id`) - KEY: `idx_pt_status` (`status`) - KEY: `idx_pt_admin` (`created_by_admin_id`)
**Foreign Keys**: - `fk_pt_category`: `category_id``product_categories(id)` - `fk_pt_admin`: `created_by_admin_id``admins(id)`
### part_template_params模板参数字段
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| template_id | BIGINT UNSIGNED | NOT NULL | | 所属模板 |
| field_key | VARCHAR(64) | NOT NULL | | 参数键(字母/下划线) |
| field_label | VARCHAR(120) | NOT NULL | | 参数名(展示) |
| type | ENUM('string','number','boolean','enum','date') | NOT NULL | | 参数类型 |
| required | TINYINT(1) | NOT NULL | 0 | 是否必填 |
| unit | VARCHAR(32) | YES | | 单位(文本) |
| enum_options | JSON | YES | | 枚举项type=enum |
| searchable | TINYINT(1) | NOT NULL | 0 | 参与检索 |
| dedupe_participate | TINYINT(1) | NOT NULL | 0 | 参与去重键 |
| sort_order | INT | NOT NULL | 0 | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ptp_template` (`template_id`) - KEY: `idx_ptp_sort` (`template_id`,`sort_order`) - UNIQUE: `ux_ptp_field_key` (`template_id`,`field_key`)
**Foreign Keys**: - `fk_ptp_template`: `template_id``part_templates(id)` ON DELETE CASCADE
### product_prices
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
@@ -321,7 +445,6 @@
| phone | VARCHAR(32) | YES | | 座机 |
| address | VARCHAR(255) | YES | | 送货地址 |
| mobile | VARCHAR(32) | YES | | 手机 |
| level | VARCHAR(32) | YES | | 客户等级标签 |
| contact_name | VARCHAR(64) | YES | | 联系人 |
| price_level | ENUM('零售价','批发价','大单报价') | NOT NULL | 零售价 | 默认售价列(中文存储) |
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
@@ -332,7 +455,6 @@
| deleted_at | DATETIME | YES | | |
字段说明customers
- level: 等级标签
- price_level: 默认售价列(中文存储:零售价/批发价/大单报价)
- ar_opening: 期初应收
@@ -420,10 +542,13 @@
| quantity | DECIMAL(18,3) | NOT NULL | | |
| unit_price | DECIMAL(18,2) | NOT NULL | | |
| discount_rate | DECIMAL(5,2) | NOT NULL | 0.00 | 折扣百分比0-100 |
| cost_price | DECIMAL(18,2) | NOT NULL | 0.00 | 记录生成单据时的成本单价 |
| cost_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 成本金额 = 数量×成本单价 |
| amount | DECIMAL(18,2) | NOT NULL | | |
字段说明sales_order_items
- quantity/unit_price/discount_rate/amount: 数量/单价/折扣%/行金额
- cost_price/cost_amount: 记录生成单据时的成本(用于利润分析)
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_soi_order` (`order_id`) - KEY: `idx_soi_product` (`product_id`)
**Foreign Keys**: - `fk_soi_order`: `order_id``sales_orders(id)` ON DELETE CASCADE - `fk_soi_product`: `product_id``products(id)`
@@ -463,7 +588,7 @@
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_poi_order` (`order_id`) - KEY: `idx_poi_product` (`product_id`)
**Foreign Keys**: - `fk_poi_order`: `order_id``purchase_orders(id)` ON DELETE CASCADE - `fk_poi_product`: `product_id``products(id)`
image.png
### payments
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
@@ -477,16 +602,107 @@ image.png
| amount | DECIMAL(18,2) | NOT NULL | | |
| pay_time | DATETIME | NOT NULL | | |
| remark | VARCHAR(255) | YES | | |
| category | VARCHAR(64) | YES | | 分类 key主要用于其他收支 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
字段说明payments
- biz_type/biz_id: 业务来源及关联主键
- direction: in 收款 / out 付款
- account_id: 使用的结算账户
- category: 分类 key主要用于其他收支用于台账明细展示与统计
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_payments_shop_time` (`shop_id`,`pay_time`) - KEY: `idx_payments_biz` (`biz_type`,`biz_id`)
**Foreign Keys**: - `fk_payments_shop`: `shop_id``shops(id)` - `fk_payments_user`: `user_id``users(id)` - `fk_payments_account`: `account_id``accounts(id)`
### inventory_movements
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| source_type | VARCHAR(32) | NOT NULL | | 来源sale/purchase/sale_return/purchase_return/adjust/audit |
| source_id | BIGINT UNSIGNED | YES | | 来源表ID可空 |
| qty_delta | DECIMAL(18,3) | NOT NULL | | 数量增减,出库为负,入库为正 |
| amount_delta | DECIMAL(18,2) | YES | | 金额增减(可空) |
| cost_price | DECIMAL(18,2) | YES | | 业务发生时的成本单价(可空) |
| cost_amount | DECIMAL(18,2) | YES | | 成本金额(可空) |
| reason | VARCHAR(64) | YES | | 原因/类别 |
| tx_time | DATETIME | NOT NULL | | 业务时间 |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_im_shop_time` (`shop_id`,`tx_time`) - KEY: `idx_im_product` (`product_id`)
### sales_return_orders
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| customer_id | BIGINT UNSIGNED | YES | | |
| order_no | VARCHAR(32) | NOT NULL | | |
| order_time | DATETIME | NOT NULL | | |
| status | ENUM('approved','void') | NOT NULL | approved | |
| amount | DECIMAL(18,2) | NOT NULL | 0.00 | 退货金额合计 |
| paid_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 已退/已收合计 |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_sr_order_no` (`shop_id`,`order_no`) - KEY: `idx_sr_shop_time` (`shop_id`,`order_time`)
**Foreign Keys**: - `fk_sr_customer`: `customer_id``customers(id)` - `fk_sr_user`: `user_id``users(id)`
### sales_return_order_items
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| order_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| quantity | DECIMAL(18,3) | NOT NULL | | |
| unit_price | DECIMAL(18,2) | NOT NULL | | |
| discount_rate | DECIMAL(5,2) | NOT NULL | 0.00 | 折扣百分比0-100 |
| cost_price | DECIMAL(18,2) | NOT NULL | 0.00 | 退货时对应的成本单价 |
| cost_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 成本金额 = 数量×成本单价 |
| amount | DECIMAL(18,2) | NOT NULL | | 行金额 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_sroi_order` (`order_id`) - KEY: `idx_sroi_product` (`product_id`)
**Foreign Keys**: - `fk_sroi_order`: `order_id``sales_return_orders(id)` - `fk_sroi_product`: `product_id``products(id)`
### purchase_return_orders
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| supplier_id | BIGINT UNSIGNED | YES | | |
| order_no | VARCHAR(32) | NOT NULL | | |
| order_time | DATETIME | NOT NULL | | |
| status | ENUM('approved','void') | NOT NULL | approved | |
| amount | DECIMAL(18,2) | NOT NULL | 0.00 | 退货金额合计 |
| paid_amount | DECIMAL(18,2) | NOT NULL | 0.00 | 已付合计(退款/扣减) |
| remark | VARCHAR(255) | YES | | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | |
**Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_pr_order_no` (`shop_id`,`order_no`) - KEY: `idx_pr_shop_time` (`shop_id`,`order_time`)
**Foreign Keys**: - `fk_pr_supplier`: `supplier_id``suppliers(id)` - `fk_pr_user`: `user_id``users(id)`
### purchase_return_order_items
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| order_id | BIGINT UNSIGNED | NOT NULL | | |
| product_id | BIGINT UNSIGNED | NOT NULL | | |
| quantity | DECIMAL(18,3) | NOT NULL | | |
| unit_price | DECIMAL(18,2) | NOT NULL | | |
| amount | DECIMAL(18,2) | NOT NULL | | 行金额 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_proi_order` (`order_id`) - KEY: `idx_proi_product` (`product_id`)
**Foreign Keys**: - `fk_proi_order`: `order_id``purchase_return_orders(id)` - `fk_proi_product`: `product_id``products(id)`
### other_transactions
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
@@ -509,4 +725,74 @@ image.png
- type/category: 收入/支出与分类
- counterparty_type/id: 往来单位(可空)
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ot_shop_time`
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ot_shop_time` (`shop_id`,`tx_time`)
### consults
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 咨询ID |
| shop_id | BIGINT UNSIGNED | NOT NULL | | 所属店铺 |
| user_id | BIGINT UNSIGNED | NOT NULL | | 提问用户 |
| topic | VARCHAR(120) | NO | | 主题(可空字符串) |
| message | TEXT | NO | | 咨询内容 |
| status | ENUM('open','resolved','closed') | NO | open | 状态:未解决/已解决/关闭 |
| created_at | TIMESTAMP | NO | CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NO | CURRENT_TIMESTAMP | 更新时间 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_consult_shop_status` (`shop_id`,`status`) - KEY: `fk_consult_user` (`user_id`)
**Foreign Keys**: - `fk_consult_shop`: `shop_id``shops(id)` - `fk_consult_user`: `user_id``users(id)`
### consult_replies
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | 回复ID |
| consult_id | BIGINT UNSIGNED | NOT NULL | | 所属咨询 |
| user_id | BIGINT UNSIGNED | NOT NULL | | 回复人(管理员) |
| content | TEXT | NOT NULL | | 回复内容 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | 回复时间 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_cr_consult` (`consult_id`) - KEY: `fk_cr_user` (`user_id`)
**Foreign Keys**: - `fk_cr_consult`: `consult_id``consults(id)` - `fk_cr_user`: `user_id``users(id)`
```
**触发器**:
- `trg_consult_replies_ai`: AFTER INSERT ON `consult_replies` → 更新 `consults.status='resolved'` 且 `updated_at=NOW()`
```
### notices
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| title | VARCHAR(120) | NOT NULL | | 标题 |
| content | VARCHAR(500) | NOT NULL | | 内容(跑马灯/简讯) |
| tag | VARCHAR(32) | YES | | 标签(如“活动”) |
| is_pinned | TINYINT(1) | NOT NULL | 0 | 是否置顶 |
| starts_at | DATETIME | YES | | 生效开始时间(为空立即生效) |
| ends_at | DATETIME | YES | | 生效结束时间(为空长期) |
| status | ENUM('draft','published','offline') | NOT NULL | published | 状态 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | 逻辑删除 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_notices_time` (`starts_at`,`ends_at`)
字段说明:
- 生效窗口:仅当 `status='published'` 且当前时间处于 `[starts_at, ends_at]` 区间(空表示不限制)时,前台 `/api/notices` 会返回;排序 `is_pinned DESC, created_at DESC`
- 数据范围:平台全局公告(与租户无关)。如需“店铺公告”,需在该表增加 `shop_id` 并调整接口逻辑。
### email_codes
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| email | VARCHAR(128) | NOT NULL | | 邮箱 |
| scene | VARCHAR(32) | NOT NULL | | 场景login/register/…) |
| code_hash | VARCHAR(64) | NOT NULL | | 验证码哈希SHA-256 |
| salt | VARCHAR(64) | NOT NULL | | 加盐字符串 |
| expire_at | DATETIME | NOT NULL | | 过期时间 |
| status | TINYINT UNSIGNED | NOT NULL | 0 | 0=unused,1=used,2=expired |
| fail_count | INT | NOT NULL | 0 | 错误次数 |
| ip | VARCHAR(64) | YES | | 发送IP |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_email_scene_created` (`email`,`scene`,`created_at`) - KEY: `idx_email_expire` (`expire_at`)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,187 @@
# 货品功能扩展需求文档(草案)
## 1. 当前实现概览
### 1.1 用户端uni-app
- `pages/product/list.vue`:支持关键字与按类别筛选、下拉分页、查看详情并跳转编辑;展示“平台推荐/我的提交”标签。
- `pages/product/form.vue`:可创建/编辑货品,字段涵盖名称、条码、品牌、型号、规格、产地、单位、类别、安全库存、四列售价、初始库存、图片与备注;提供图片识码能力;图片上传走 `/api/attachments`
- **`pages/product/submit.vue`**:新增配件提交入口,支持型号唯一校验、多图上传、参数 JSON、备注、安全库存等字段提交成功跳转“我的提交”。
- **`pages/product/submissions.vue``pages/product/submission-detail.vue`**:新增提交列表与详情页,支持状态筛选、滚动分页、驳回重新提交、图片/参数展示。
- 字典:调用 `/api/product-units``/api/product-categories` 读取全局单位/类别,并缓存到本地。
### 1.2 管理端Vue3 + Element Plus
- **`admin/src/views/parts/Submissions.vue`**:已上线配件审核模块,提供筛选、详情、编辑、通过/驳回、Excel 导出能力。
- 其它模块VIP 系统、公告管理、咨询回复、用户管理、配件管理、供应商管理、主数据字典。
- 待迭代:登录页、角色权限细分、操作日志可视化。
### 1.3 后端Spring Boot
- `ProductSubmissionController` + `ProductSubmissionService`:完成用户提交、用户查看、管理员审核/驳回、导出全链路;审批通过时同步 `products``product_images``source_submission_id` 等字段。
- OpenAPI 已新增 `/api/products/submissions*``/api/admin/parts/submissions*` 路径,并标记为 ✅ Fully Implemented。
- 数据模型:`products``product_prices``inventories``product_images` 已投产;`part_submissions` 已扩展 JSON、状态、审核信息等字段。
- 图片存储:统一走 `attachments`,提交与商品共享资源。
## 2. 差距分析
| 需求点 | 现状 | 差距 |
| --- | --- | --- |
| 用户提交配件待审核 | 用户端新增提交/列表/详情页面,与后台接口打通 | 持续跟踪型号唯一范围(店铺或全局);补充并发校验测试 |
| 审核管理 | 管理端已上线审核列表、详情编辑、通过/驳回、导出 | 后续可扩展批量操作、导出更大数据量的异步方案 |
| 数据查询增强 | 后端支持多条件、返回 total前端列表分页同步 | 可继续优化查询性能(索引/缓存)与前端展示字段 |
| 图片管理 | 提交端与审核端均支持多图显示、预览、排序 | 可根据需求补充图片备注、AI 识别等增强能力 |
| 上架逻辑 | 审核通过自动写入 `products` 并关联 `source_submission_id``global_sku_id` | 待确认产品图片、参数同步策略是否满足后续拓展(如平台库) |
| 导出 | 管理端支持按筛选条件导出 Excel2000 条以内同步导出) | 后续可评估异步导出/下载中心、导出模板自定义 |
## 3. 设计方案
> 项目采用方案 A基于 `part_submissions` + `products` 双轨流程),方案 B 已放弃,不再纳入考虑。
### 方案 A基于现有 `part_submissions` + `products` 双轨流程(已确认)
1. **用户提交**
- 新增用户端页面 `pages/product/submit.vue`字段型号model唯一必填、配件名称name、配件参数parameters自由文本/JSON 字段)、图片(多图,最多 9 张)、备注。
- 调用 `POST /api/products/submissions`,写入 `part_submissions``shop_id``user_id``model_unique``status=pending``images JSON``extra_attrs``created_at`
- 验证规则:
- `model_unique` 同一店铺 + 全局唯一(需确认范围)。
- 图片通过 `attachments` 保存,限制尺寸/数量。
- 上传成功后展示提交状态(待审核)。
2. **管理员审核**
- 新增管理端视图 `admin/src/views/parts/Submissions.vue`
- 列表字段:型号、名称、提交人、提交时间、当前状态、备注、图片缩略图。
- 过滤项:状态(待审/已审/驳回)、关键字(支持型号/名称/提交人)、提交时间区间、店铺。
- 操作:
- 查看详情(弹窗):可编辑名称、型号、参数、图片(增删排序)、备注。
- 审核按钮:批准/驳回,必须填写备注(驳回)。
- 批量导出:选中或按筛选条件导出 Excel。
- 审核通过逻辑:
- 检测是否存在相同型号产品:
- 若没有:创建 `products`source_submission_id 引用)、初始化库存 0、价格默认 0关联图片。
- 若已有:补充图片、参数(可覆盖或合并),并记录 submission→product 对应关系。
- 更新 `part_submissions.status=approved``reviewer_id``reviewed_at``product_id`
- 将产品显示给提交用户(若产品属于平台库,可关联 `global_sku_id`)。
- 审核驳回:状态=reject记录 `remark``reviewer_id``reviewed_at`
3. **数据查询**
- `GET /api/products` 增强:支持 `brand``model``status`(是否审核通过)、`createdStart/End` 等参数;返回 `{ list, total }`
- `GET /api/admin/parts/submissions` 支持分页+多条件,默认按创建时间倒序。
- Excel 导出:`GET /api/admin/parts/submissions/export`,返回文件下载,支持当前过滤条件。
4. **图片管理**
- 提交阶段:图片存储 `attachments`ownerType=submission路径按 hash 去重。
- 审核详情弹窗内使用 `el-image` + preview 功能查看大图;允许删除/新增(调用 `/api/attachments` 上传)。
- 审核通过时同步图片至 `product_images`(可重用 attachment URL无需复制物理文件根据排序保存 `sort_order`
5. **上架逻辑**
- 平台维护 `global_skus`(可选):
- 审核通过界面提供“关联平台配件”下拉,或创建新平台配件,同时写入 `global_skus`
- `products.global_sku_id` 记录来源,供其他店铺引用。
- 用户提交的产品通过审核后自动加入其店铺的 `products`,并可在商品列表中展示“平台推荐/自有”区分。
### 4.1 数据模型变更细节
#### part_submissions新增/调整)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | VARCHAR(120) | 配件名称(用户填写,可用于审核) |
| `parameters` | JSON | 结构化参数,如规格、尺寸、材质等 |
| `images` | JSON | 图片 URL 列表,提交阶段存储 |
| `status` | ENUM('pending','approved','rejected') | 当前审核状态 |
| `remark` | VARCHAR(255) | 审核备注(驳回原因等) |
| `reviewer_id` | BIGINT | 审核人 ID |
| `reviewed_at` | DATETIME | 审核时间 |
| `product_id` | BIGINT | 审核通过后关联的产品 ID |
| `global_sku_id` | BIGINT | 可选,关联平台配件 ID |
| `source_shop_id` | BIGINT | 原提交店铺(若与 shop_id 区分需求明确可保留) |
> 若原表已有旧字段(如 `compatible`、`size` 等),可迁移到 `parameters` JSON 中或保留以兼容老数据。
#### products新增字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `source_submission_id` | BIGINT | 来源提交记录 ID |
| `global_sku_id` | BIGINT | 平台配件 ID可选 |
| `platform_status` | ENUM('platform','custom') | 产品来源标识(可选) |
需要为 `model_unique``shop_id` 添加唯一索引,保障型号不可重复提交(规则确定后实施)。
### 4.2 接口契约摘要
| 接口 | 方法 | 说明 | 核心入参 | 返回 |
| --- | --- | --- | --- | --- |
| `/api/products/submissions` | POST | 用户提交配件 | `{ name, model, parameters?, images[], remark? }` | `{ id, status }` |
| `/api/products/submissions` | GET | 用户查看提交记录 | `status?, page, size` | `{ list, total }` |
| `/api/admin/parts/submissions` | GET | 管理端列表 | `status?, kw?, shopId?, reviewerId?, startAt?, endAt?, page, size` | `{ list, total }` |
| `/api/admin/parts/submissions/{id}` | GET | 查看详情 | - | 详细信息(含图片) |
| `/api/admin/parts/submissions/{id}` | PUT | 编辑字段(名称/参数/图片) | `{ name?, parameters?, images?, remark? }` | `{ ok: true }` |
| `/api/admin/parts/submissions/{id}/approve` | POST | 审核通过 | `{ remark?, assignGlobalSkuId?, createGlobalSku? }` | `{ ok: true, productId }` |
| `/api/admin/parts/submissions/{id}/reject` | POST | 审核驳回 | `{ remark }` | `{ ok: true }` |
| `/api/admin/parts/submissions/export` | GET | Excel 导出 | 同列表查询参数 | 二进制文件 |
| `/api/products` | GET | 增强查询 | `kw, brand, model, status, categoryId, startAt, endAt, page, size` | `{ list, total }` |
OpenAPI 需同步更新:新增路径、请求体/响应体、状态标注。
### 4.3 前端/后端任务拆分
**后端**
1. 数据库迁移脚本(新增字段、索引、默认值)。
2. 提交接口实现:存储提交记录、处理图片、型号唯一校验。
3. 审核接口实现:
- 获取详情、编辑提交记录。
- 批准流程(创建/更新产品、同步图片、记录审计)。
- 驳回流程。
4. 列表查询与导出(注意权限:仅管理员)。
5. 产品查询增强(支持多条件和 total
6. 单元测试/集成测试:审核通过/驳回、重复型号、导出大数据量。
**管理端前端**
1. 新建 `Submissions.vue` 页面:
- 列表 + 搜索表单 + 分页。
- Excel 导出按钮。
2. 审核详情弹窗组件:
- 多图预览/排序Element Plus `el-image` + `el-upload` 或复用现有 `ImageUploader` 组件)。
- 字段编辑与保存。
- 审核通过/驳回按钮及确认弹窗。
3. 上架结果联动:审批通过后刷新列表,提示成功。
**用户端前端**
1. 新增 `pages/product/submit.vue`
- 表单输入 + 多图上传 + 校验(型号唯一、本地格式校验)。
- 提交成功提示与跳转。
2. 新增“我的提交”页面或在现有列表中添加状态指示。
3. 货品列表/详情展示新字段(如“平台配件”标签、审核状态)。
## 5. 开发计划建议
1. **第一阶段:后端改造**
- 数据库变更(通过 MysqlMCP更新 `part_submissions``products``global_skus` 相关字段与索引。
- 新增接口实现(提交、审核、导出、多条件查询)。
- 扩展 `ProductService` 支持 total、更多筛选。
- 调整 OpenAPI标注实现状态。
2. **第二阶段:管理端前端**
- 新建审核模块页面,集成组合查询 + 分页 + 导出。
- 审核详情对话框支持多图预览/编辑、字段修改、审核按钮。
3. **第三阶段:用户端前端**
- 新增“提交配件”入口(从货品列表或我的模块进入)。
- 提交表单 + 多图上传(复用 ImageUploader
- 提交记录列表(待审/已通过/驳回)与状态提示。
- 审核通过后自动同步到货品列表(带标签)。
4. **第四阶段:测试与文档**
- 定义审批流程测试用例:提交->审批->列表刷新->导出。
- 验证图片上传、重复型号、并发审批场景。
- 更新需求文档、开发文档、用户操作指引。
## 6. 验收标准
- 用户端可提交配件:型号唯一校验、上传多图成功、提交后状态为“待审核”。
- 管理端待审核列表能根据筛选条件展示结果,并支持详情查看(含大图预览、编辑字段)。
- 管理端可对单条记录审核通过/驳回:
- 通过:在 `products` 中生成或更新商品;提交用户在商品列表中可见;状态变更为“已通过”。
- 驳回:状态变更为“已驳回”,记录审核备注。
- 导出功能可按筛选条件导出 Excel至少包含 2000 条数据测试无超时;生成文件名避免冲突(时间戳+操作人)。
- 数据查询支持组合条件,返回总数;前后端分页一致。
- 图片管理:审核详情中可预览大图、删除、重新上传;重复文件自动识别 hash避免冲突。
- 上线后数据库结构、OpenAPI、开发文档同步更新无遗漏。
## 7. 风险与注意事项
- 型号唯一性需统一标准化(去空格、大小写)并考虑跨店铺冲突策略。
- 审核通过时同步到 `products`,需明确库存与价格的初始值(建议默认 0提醒用户自行编辑
- Excel 导出需加限流或异步处理,防止长时间请求阻塞。
- 图片引用避免重复占用存储;可直接引用同一 URL而非复制文件。
- 若需消息通知(如审核结束提醒),需要后续补充通知机制。
- 需要回顾 `global_skus``products` 的关系,明确平台配件库与用户私有货品的边界,以免数据混乱。
---
> 本文档结合现状与新增需求整理。请确认方案后,可进一步拆解任务并排期执行。

View File

@@ -3,26 +3,30 @@
#### 1.0 项目概述
本项目旨在开发一款面向小微商户的移动端进销存管理应用,命名为“配件查询”。该应用核心功能是帮助用户高效管理商品、库存、销售、采购、客户、供应商及财务收支,并通过数据报表提供经营状况分析,助力商户实现数字化经营。
参考的小程序“智慧记进销存”,但是多了一个配件查询功能,以下所罗列的内容大多也参考至该小程序,如有歧义可优先参照这个小程序,拿不准优先问。
当前交付内容移动端已实现基础的登录、VIP 开通、用户/客户/供应商管理、下单与报表入口;管理端完成会员、公告、咨询、配件、字典维护模块。
#### 2.0 功能模块需求
**2.1 首页 (Dashboard)**
* **2.1.1 核心数据概览:** 首页需直观展示当日、当月的核心经营数据。
> 已实现基础销量/库存统计,利润汇总需依赖真实成本数据完善。
* 今日销售额
* 本月销售额
* 本月利润
* 库存商品数量
**2.1.2 广告位:** 在首页区域提供一个展示广告的区域。
> 待设计,当前保留静态占位图。
* **2.1.3 快捷功能入口:** 提供一个快捷功能区域,方便用户快速访问常用功能。
* 默认应包含客户管理、销售开单、账户管理、供应商管理、进货开单、其他支出、VIP会员、报表统计等。
* 现状:移动端 Tab 首页已集成常用入口,后续需根据配置扩展。
* **2.1.4 在线客服:** 提供一个悬浮的“咨询”或“在线客服”入口,方便用户随时获取帮助。
* 当前通过“我的-咨询”入口触达,悬浮入口尚未实现。
**2.2 货品管理模块**
* **2.2.1 货品列表与库存:**
@@ -92,9 +96,12 @@
**2.6 “我的”(用户中心)模块**
* **2.6.1 用户信息:** 显示用户头像、店铺名称、注册手机号及“老板”身份标识。
> 个人信息读取 `/api/user/me` 已上线。
* **2.6.2 会员与订单:**
* 提供**VIP会员**入口,展示会员特权。
> `/pages/my/vip` 已接入 `/api/vip/status`,开通调用 `/api/vip/pay`。
* 提供**我的订单**入口,可能用于查看应用内服务订单。
> 入口预留,服务订单接口尚未上线。
* **2.6.3 基础管理:**
* **供应商管理**
* **客户管理**
@@ -113,6 +120,7 @@
#### 3.0 全局性需求
* **3.1 导航:** 采用底部Tab栏导航包含“首页”、“货品”、“开单”、“明细”、“我的”五个主要模块。
> uni-app 端已实现,管理端采用侧栏导航。
* **3.2 统一的UI/UX:** 应用整体风格简洁、清晰,操作流程符合移动端使用习惯。
* **3.3 空状态页面:** 在列表、报表等数据为空的页面,需提供友好的空状态提示图和引导性文字。
* **3.4 数据同步:** 应用数据应在云端同步,保证用户更换设备或多设备使用时数据一致性。
@@ -129,11 +137,12 @@
* 需满足大规模SKU下的性能目标可通过系统参数配置匹配策略。
* **3.8 客户端平台:** 提供移动App与小程序小程序不支持商品条形码扫描功能。
> HBuilderX 打包的 App 版本与微信小程序版本已具备扫描差异处理;条码识别仅用户端调用。
* **3.9 多列销售价格:** 销售价格分四列,即同一种商品有四个销售价格
### 配件查询
1. **数据查询功能**
- 多参数组合查询(分类、尺寸、型号等)
- 模糊匹配关键字
@@ -154,14 +163,14 @@
- 点击图片可放大查看
- 管理员可管理所有图片
- 自动处理文件名冲突
## 全局说明(必看)
由于这个文档写的还不是很完善,目前有存疑的部分先行参考小程序小程序“智慧记进销存”(功能和按钮可以参考,界面样式除外),管理端文档目前待定。
客户要求的是做双端应用app端+小程序端),需要考虑兼容性相关问题。
本程序和“智慧记进销存”大多一致,主要的区别在于客户有配件查询要求,即在产品页面中要额外加一个配件查询按钮或入口,且要求一个产品要有四个销售价格(先按零售价 分销价 批发价 大客户价),且要求能自定义添加各种规格(尺寸,孔径等)。
有疑惑的部分一定要及时沟通(如未提及的页面和功能需要确认的时候)

331
doc/vip_development.md Normal file
View File

@@ -0,0 +1,331 @@
### VIP 功能开发文档
#### 1. 范围与目标
- 目标为“配件查询”应用提供面向店铺租户的会员体系VIP支持会员开通、审核、生效与到期管理并为后续增值能力如数据永久存储、高级报表、优先审核等提供统一的权限判定入口。
- 范围:后端服务与管理端优先落地;移动端展示会员状态与开通入口,后续再接入支付与自动续期。
#### 2. 现状与差距
- 前端 App`frontend/pages/my/vip.vue` 当前为静态演示页,读取 `VIP_PRICE_PER_MONTH` 展示,未接入后端状态与支付。
- 管理端:已存在页面
- `admin/src/views/vip/VipList.vue`:查询、编辑、启停。
- `admin/src/views/vip/VipReview.vue`:列出待启用(审核)并提供通过/驳回。
- 后端:`AdminVipController` 提供基础接口:列表、创建、更新、审核通过/驳回(部分实现)。依赖表 `vip_users`
- OpenAPI`/doc/openapi.yaml` 已定义管理端 VIP 接口,标注为「❌ Partially Implemented」。
- 数据库:本地 `backend/db/db.sql` 尚未创建 `vip_users` 表;而远程数据库文档 `/doc/database_documentation.md` 已包含该表结构。这是“脚本未对齐线上库”的差距,需要弥合(见实施计划)。
#### 3. 业务与权限
- 归属维度VIP 为“用户级”。每个用户仅归属一个店铺(无店员概念)。
- 状态定义:
- `isVip`是否为会员标记1/0
- `status`启用状态1=生效0=未启用/停用)。
- `expireAt`:到期时间(为空代表永久或未设置)。
#### 4. 数据模型设计(多方案)
- 方案A单表 `vip_users`
- 字段:`id, shop_id, user_id, is_vip, status, expire_at, remark, reviewer_id, reviewed_at, created_at, updated_at`
- 优点:简单直观,满足当前开通/续期/停用/审核的核心需求。
- 局限后续若需要等级gold/platinum、付费订单、优惠券、权益包等需要再扩展新表。
- 方案B三表拆分推荐用于中长期
- `vip_users`(会员资格)、`vip_levels`(等级/权益配置)、`vip_orders`(开通/续费订单记录)。
- 优点:扩展性好,可平滑引入支付渠道、对账、活动定价。
- 初期实施成本更高。可采用“增量演进”先落地方案A同步预留 `vip_orders` 的接口草案与库表蓝图,待支付接入时启用。
当前建议首期采用方案A满足“开通-审核-生效-到期”的MVP同时在配置中预留价格与时长便于后续衔接 `vip_orders`
#### 5. 配置与价格策略(严禁硬编码)
- 前端价格:已通过 `frontend/common/config.js``VIP_PRICE_PER_MONTH` 从环境变量/本地存储读取,默认 15 元/月。
- 后端价格与时长建议:通过 `system_parameters` 表维护,键建议:
- `vip.price`(单位:元/月number
- `vip.durationDays`(开通/续费时长,默认 30
- 可选:`vip.trialDays`(试用天数,默认 0
- 后端读取优先级:系统参数 > 环境变量(如 `VIP_PRICE`,`VIP_DURATION_DAYS`> 安全默认值;并通过管理端配置面板维护参数(后续迭代)。
管理员端价格设置(新增):
- 平台统一价,存储于表 `vip_price`(仅一条记录)。
- 在管理端“VIP管理”新增“价格设置”入口读取/修改 `vip_price.price`
- 修改保存后立即生效;前端价格展示以后端返回为准(避免前后端不一致)。
#### 6. 接口契约(与 openapi.yaml 对齐)
- 管理端:
- GET `/api/admin/vips`(列表)
- POST `/api/admin/vips`(新增)
- PUT `/api/admin/vips/{id}`(更新 isVip/status/expireAt/remark
- 审核流程已取消approve/reject 已移除);统一由用户端一键开通或管理员直接设置。
- App建议新增未在 openapi.yaml 中登记,待一方开工后再登记):
- GET `/api/vip/status`:返回当前用户 VIP 状态与到期时间。
- POST `/api/vip/pay`:一键支付并立即开通(临时方案:点击即视为支付成功,直接写入/更新 `vip_users`,置 `status=1,isVip=1,expireAt=now+durationDays`;后续接入真实支付将替换该接口实现)。
- 说明:上述 App 接口仅为“建议方案”,在前端或后端开始实现后再将其写入 `/doc/openapi.yaml` 并标注实现状态,避免“仅设计未实现”的文档污染。
示例返回(状态查询):
```json
{
"isVip": true,
"status": 1,
"expireAt": "2025-12-31T23:59:59Z"
}
```
#### 7. 前后端对接要点
- Header管理端接口需要 `X-User-Id`(用于写入审核人);多租户场景下优先根据用户解析 `shop_id`,或显式传递 `X-Shop-Id`
- 列表过滤:`phone` 模糊匹配 `users.phone``status` 精确匹配 `vip_users.status`
- 审核流:通过/驳回接口必须记录 `reviewer_id``reviewed_at`,便于审计。
- App 端状态展示:进入“我的-会员”页时调用 `GET /api/vip/status`,根据返回值展示“已激活/有效期至/立即开通”。
##### 7.1 数据可见性与留存策略(关键新增)
- 规则:
- VIP 用户:可查看“所有历史数据”(不做时间裁剪)。
- 普通用户:仅显示“最近两个月内”的数据,早于两个月的数据不显示。
- 配置项(严禁硬编码):
- `vip.dataRetentionDaysForNonVip`(默认 60单位写入 `system_parameters`;也可通过环境变量 `VIP_NONVIP_RETENTION_DAYS` 覆盖。
- 当该值小于等于 0 时视为“非VIP也不做裁剪”便于灰度/应急)。
- 后端落地建议:
- 在列表/查询型接口(如 `/api/orders`, `/api/purchase-orders`, `/api/payments`, `/api/inventories/logs`, `/api/other-transactions`统一增加“非VIP数据时间窗”约束
- 通过当前用户的 VIP 状态与 `expireAt` 判断是否 VIP非VIP则在 SQL WHERE 中增加 `tx_time/order_time >= NOW() - INTERVAL retentionDays DAY` 等条件。
- 采用服务层统一拦截/拼接过滤,避免各控制器重复实现。
- 在导出/报表接口同样应用该约束,确保口径一致。
- 前端落地建议:
- 当非VIP进行跨越两个月以上的日期筛选时给出轻提示“普通用户仅显示近两个月数据开通VIP可查看全部历史”。
- 页面不做本地硬裁剪,一切以后端约束为准,前端仅用于用户沟通与引导开通。
#### 8. 业务流程Graphviz
```dot
digraph G {
rankdir=LR;
node [shape=box, style=rounded];
start [label="用户进入VIP页"];
pay [label="一键支付开通\n(POST /api/vip/pay)"];
active [label="生效: status=1, isVip=1\n设置expireAt=now+durationDays"];
expire [label="到期: 定时任务或登录判定\n自动置 isVip=0 / 提示续期"];
start -> pay -> active -> expire;
}
```
#### 9. 实施计划MVP
1) 数据库脚本对齐(仅通过 MysqlMCP 执行)
- 若目标库缺失 `vip_users`:按 `/doc/database_documentation.md` 定义创建,并补充必要索引与外键。
- 备注:执行后同步更新 `/doc/database_documentation.md` 中的更新时间与差异说明(保持与线上一致)。
2) 后端接口完备
- 补齐 `AdminVipController` 的参数校验、店铺隔离、分页 total 统计(可选)。
- 新增 App 侧接口:`GET /api/vip/status``POST /api/vip/pay`(点击即开通临时方案)。当开始实现后,将路径登记至 `/doc/openapi.yaml` 并标注状态。
- 配置读取:接入 `system_parameters``vip.price`/`vip.durationDays`
3) 管理端接入
- `VipList.vue``VipReview.vue` 接口字段对齐;在创建/编辑时透传 `shopId`(或后端根据用户解析)。
- 在“VIP管理”中新增“价格设置”对话框读取/修改 `vip.price`(走系统参数接口或新增专用接口)。
4) App 接入
- `pages/my/vip.vue` 调用 `GET /api/vip/status`;“立即开通”改为调用 `POST /api/vip/pay`(临时:点击即开通)。
5) 定时/登录时到期检查
- 每日定时任务或用户登录时,对已到期记录置 `isVip=0` 并提示续期(可由查询接口实时判断并回写)。
#### 10. 测试用例(核心)
- 列表:无过滤/按手机号/按状态返回正确集合;分页稳定。
- 新增:缺少 `shopId`/`userId` 报错;完整入参成功写入,`status=0` 待启用。
- 更新:仅更新传入字段,`updated_at` 变更。
- 审核:通过后 `status=1` 且记录 `reviewer_id/reviewed_at`;驳回后 `status=0`
- 到期:`expireAt < now()` 时,状态查询返回 `isVip=false`(或提示续期),符合设计。
- 数据可见性:
- 非VIP订单/支付/库存流水/其他收支等查询接口,返回记录的最早时间不早于“当前时间-60天”默认值当调整 `vip.dataRetentionDaysForNonVip` 后行为随之变化。
- VIP在同样条件下可查询到两个月前的历史记录。
- 前端在选择超窗的日期范围时出现轻提示,不影响接口正常返回。
- 一键支付开通:
- 点击开通后接口返回成功,`vip_users` 写入/更新,`status=1,isVip=1,expireAt=now+durationDays`
- 再次点击在有效期内应提示“已是VIP/延长有效期”,并将 `expireAt` 顺延 `durationDays`(若设计为续期)。
- 管理端价格设置:
- 读取当前价格成功显示;修改后保存成功,重新读取为新值;前端展示同步更新(以接口返回为准)。
#### 11. 非功能性要求
- 审计:记录审核人与审核时间。
- 性能:常用过滤列建索引(`shop_id,status`)。
- 安全:所有接口在租户维度强制鉴权与隔离;仅店主/平台管理员可审核。
#### 12. 验收标准
- 管理端可增改查 VIP支持启停与审核通过/驳回。
- App 可正确显示会员状态与有效期,支持提交开通申请。
- 配置可通过参数调整价格与时长,无任何硬编码常量。
- OpenAPI 中 VIP 路径状态标注准确,接口字段与实现一致。
- 数据可见性非VIP仅显示近两个月数据VIP显示全部历史可通过系统参数动态调整非VIP留存天数。
- 一键支付点击立即开通可直接成为VIP并设置到期时间有效期内再次开通实现续期如有设计
- 价格设置管理员可在“VIP管理”中读取/修改VIP价格保存后前端价格展示与后端一致。
#### 13. 普通管理员Admin-Lite与申请机制新增需求
- 背景:在现有平台管理员(`admins`之外新增“普通管理员Normal Admin供用户端的 VIP 用户申请成为普通管理员后登录一个精简的普通管理端admin-lite
- 目标:
- 普通管理端仅包含“配件审核”与“我的”两个功能。
- 普通管理员使用“用户端账号(邮箱 + 密码)”登录普通管理端。
- 申请资格:仅限 VIP且建议要求处于有效期内
##### 13.1 业务规则
- 申请前置:用户必须为 VIP 用户(建议校验 `vip_users.status=1``expire_at>now()`)。
- 审批策略(多方案):
- 方案A推荐平台审核提交申请→平台管理员审核→通过后赋予“普通管理员”权限。
- 方案B 自动通过:成为 VIP 后点击“申请”即自动获取权限(适合早期冷启动)。
- 方案C 店主审核:若后续引入“店主/员工”角色体系,可由店主审批本店普通管理员。
- 有效性约束(推荐):普通管理员权限与 VIP 状态绑定;当 VIP 过期则自动失去普通管理员权限(或进入“受限”状态,仅保留查看)。
- 范围隔离:普通管理员仅可访问其所属店铺 `shop_id` 范围内的资源(含配件提交与审核)。
##### 13.2 数据模型(多方案)
- 方案A复用 `users.role`,并新增审计表(快速落地)
- `users.role` 可写入 `normal_admin` 作为标识;新增 `normal_admin_audits` 记录申请、审批与撤销流水。
- 优点:改动小,落地快。
- 风险:`role` 单值扩展性差,后续多角色并存不便。
- 方案B新增 `user_roles`(推荐中长期)
- 结构:`id, user_id, role, status, created_at, updated_at`,可并存多个角色;配套 `user_role_audits` 留痕。
- 优点:扩展性强,便于细粒度权限。
- 成本:需要通用角色判定与拦截器改造。
- 方案C新增 `normal_admins`
- 结构:`id, shop_id, user_id, status, expire_at(可选), remark, reviewer_id, reviewed_at, created_at, updated_at`
- 优点:语义清晰;可直接与 VIP 绑定。
- 成本:专用表+专用逻辑,通用性略弱。
当前建议MVP 采用方案A或C二选一优先保证上线速度后续演进到方案B以支撑更多角色与权限。
配置项(严禁硬编码,写入 `system_parameters` 或全局参数表):
- `normalAdmin.autoApprove`:是否自动通过申请(默认 false
- `normalAdmin.requiredVipActive`:是否要求 VIP 有效期内(默认 true
##### 13.3 接口契约(开始实现任一侧后同步登记至 `/doc/openapi.yaml` 并标注状态)
- 用户端App/小程序):
- POST `/api/normal-admin/apply`:提交申请。需要登录且满足 VIP 条件;根据 `normalAdmin.autoApprove` 决定是否即时生效。
- GET `/api/normal-admin/application/status`:查询本人申请状态与生效情况。
- 平台管理端:
- GET `/api/admin/normal-admin/applications`:申请列表(支持状态/关键词/时间范围)。
- POST `/api/admin/normal-admin/applications/{id}/approve`:审批通过。
- POST `/api/admin/normal-admin/applications/{id}/reject`:审批驳回(需备注)。
- 普通管理端鉴权(见 13.4
- 登录沿用用户侧认证:`POST /api/auth/email/login` 或现有登录接口,后端签发 Token 时加入 `scope`/`role` 标识(如 `normal_admin`)。
示例返回(申请状态):
```json
{ "status": "approved", "isVip": true, "vipExpireAt": "2025-12-31T23:59:59Z" }
```
##### 13.4 鉴权与拦截Admin-Lite
- 新增拦截器 `NormalAdminAuthInterceptor`(或等效过滤器):
- 校验登录 Token 为用户侧 Token读取 `role/scope``normal_admin`
- 若启用 `normalAdmin.requiredVipActive`:同时校验 `vip_users` 有效。
- 强制多租户隔离:将 `shop_id` 固化为当前用户所属店铺,杜绝越权访问。
- 路由命名建议:`/api/normal-admin/**` 与平台 `/api/admin/**` 区隔;服务层可复用同一业务逻辑。
- 兼容方案:若短期沿用现有 `/api/admin/parts/submissions*`,需在后端根据 Token 类型与 `shop_id` 自动收敛到本店数据(不建议长期共用)。
##### 13.5 普通管理端admin-lite前端
- 技术栈Vue3 + Vite + Element Plus与现管理端一致新建 `normal-admin/` 工程或在现有 `admin/` 下以独立入口构建。
- 模块:
- 配件审核:复制 `admin/src/views/parts/Submissions.vue`仅保留必要字段与操作HTTP 基址指向 `/api/normal-admin/...`(或复用后端别名);列表限定为本店数据。
- 我的展示账户信息、VIP 状态、普通管理员状态、退出登录;若未获批且为 VIP显示“申请成为普通管理员”按钮调用 `/api/normal-admin/apply`)。
- 路由示例:
- `/` → 重定向 `/parts/submissions`
- `/parts/submissions` → 审核页
- `/my` → 我的
- 配置(环境变量,不得硬编码):
- `VITE_APP_TITLE=配件审核(普通管理端)`
- `VITE_API_BASE=/api`(与部署网关一致)
- `VITE_ENABLE_EXPORT=true/false`(是否开启导出)
##### 13.6 流程图Graphviz
```dot
digraph G {
rankdir=LR;
node [shape=box, style=rounded];
a[label="成为VIP\n(GET /api/vip/status)"];
b[label="申请普通管理员\n(POST /api/normal-admin/apply)"];
c[label="平台审批/自动通过\n(根据 normalAdmin.autoApprove)"];
d[label="获得 normal_admin 权限\n(Token 含 scope)"];
e[label="登录普通管理端\n(邮箱+密码)"];
f[label="配件审核\n(/api/normal-admin/parts/submissions*)"];
a -> b -> c -> d -> e -> f;
}
```
##### 13.7 验收标准Admin-Lite
- 登录:使用用户邮箱+密码成功登录普通管理端;非普通管理员登录被拒绝并提示申请。
- 资格:非 VIP 点击“申请”提示需先成为 VIPVIP 点击“申请”按配置自动通过或进入待审;审批通过后刷新权限可访问。
- 范围:仅能查看与操作本店 `shop_id` 范围内的配件提交;越权访问被拦截。
- 审核:通过/驳回、备注、图片管理、参数编辑等操作与现管理端一致;操作成功刷新列表。
- 导出(可选):按配置开放导出功能,文件名与权限口径与现实现一致。
- 退化:当 VIP 失效时,若配置为强绑定,应在下一次鉴权时移除 `normal_admin` 访问权限(或降级为只读)。
##### 13.8 实施计划(补充)
1) 后端:
- 新增 `NormalAdminAuthInterceptor``/api/normal-admin/**` 路由;实现申请接口与(可选)平台审批接口;复用配件审核服务,默认按当前用户 `shop_id` 限定范围。
- Token 扩展:在用户侧登录成功时,根据数据库角色写入 `role/scope=normal_admin`
- 配置读取:接入 `normalAdmin.autoApprove``normalAdmin.requiredVipActive`(来自 `system_parameters` 或全局参数表)。
2) 前端admin-lite
- 建立最小可用工程,复制并精简 `Submissions.vue`;新增 `My.vue`;完善登录页与路由守卫(未授权跳转登录)。
- “我的”页集成申请按钮与状态显示;成功后刷新权限并跳转审核页。
3) 文档与 OpenAPI
- 任一侧开工后,将上述接口登记至 `/doc/openapi.yaml` 并标注实现状态(❌/✅)。
##### 13.9 风险与注意事项
- 权限收敛:严禁普通管理员访问跨店铺数据;所有查询/导出在服务层统一追加 `shop_id=当前用户.shop_id` 条件。
- 身份冲突:若同一用户既是平台管理员又是普通管理员,需优先以平台后台入口登录对应端;两个端的 Token/域名/路由需区分。
- 续期与降级:明确 VIP 失效后的权限处理策略与用户提示文案,避免误用。
- 审计留痕:申请/审批/撤销必须入审计表;审核操作记录操作人与时间。
##### 13.10 选型落地方案A
- 选型结论:
- 审批策略采用「方案A平台审核提交申请→平台管理员审核→通过后赋予“普通管理员”权限。
- 数据模型采用「方案A复用 `users.role` + 审计表)」:当前生效权限以 `users.role` 为准,申请/审批/撤销全量留痕于审计表。
- 审计表 DDL提交后通过 MysqlMCP 执行;此处为权威定义,禁止硬编码):
```sql
CREATE TABLE IF NOT EXISTS normal_admin_audits (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
shop_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
action ENUM('apply','approve','reject','revoke','expire') NOT NULL,
remark VARCHAR(255) NULL,
operator_admin_id BIGINT UNSIGNED NULL COMMENT '平台管理员IDapply时可空',
previous_role VARCHAR(32) NULL,
new_role VARCHAR(32) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_naudit_shop_time (shop_id, created_at),
KEY idx_naudit_user_time (user_id, created_at),
CONSTRAINT fk_naudit_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_naudit_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_naudit_admin FOREIGN KEY (operator_admin_id) REFERENCES admins(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='普通管理员申请/审批审计日志';
```
- 状态判定与待办列表(基于审计流,不单独建申请表):
- 当前状态 = 按 `user_id` 取最后一条审计记录的 `action`
- 待审批 = 最后一条为 `apply` 且其后没有 `approve/reject`
- 失效处理 = 定时或登录时检测 VIP 过期,若配置 `normalAdmin.requiredVipActive=true`,写入 `expire` 审计并触发降级(见下)。
- 接口约定细化(开始实现任一侧后再登记至 `/doc/openapi.yaml` 并标注状态):
- 用户申请POST `/api/normal-admin/apply`
- 校验:已登录 + VIP 有效(当 `requiredVipActive=true`)。
- 行为:写入 `normal_admin_audits(action='apply')`;若 `autoApprove=true` 则转调用审批通过流程。
- 审批列表GET `/api/admin/normal-admin/applications`
- 规则:按照审计记录推导“当前为 apply 且未被approve/reject”的记录支持关键词与时间过滤返回 `{ list, total }`
- 审批通过POST `/api/admin/normal-admin/applications/{userId}/approve`
- 行为:读取当前 `users.role` 记为 `previous_role`;更新 `users.role='normal_admin'`;写入审计 `approve`(含 `previous_role/new_role`)。
- 审批驳回POST `/api/admin/normal-admin/applications/{userId}/reject`
- 行为:写入审计 `reject`(需 remark
- 撤销权限POST `/api/admin/normal-admin/users/{userId}/revoke`
- 行为:将 `users.role` 回滚到最近一次 `approve``previous_role`(若不存在则设为 `'staff'` 或根据 `is_owner` 退回 `'owner'`);写入审计 `revoke`
- 鉴权与TokenAdmin-Lite
- 登录沿用用户端登录接口(邮箱/密码),签发 Token 时附带 `role``shopId`Admin-Lite 入口要求 `role='normal_admin'`
- 在拦截器 `NormalAdminAuthInterceptor` 中:
- 校验 `role='normal_admin'`;若 `requiredVipActive=true`,同时校验当前用户 VIP 有效。
- 将请求上下文中的 `shop_id` 固化为当前用户 `shop_id`,所有查询自动拼接该条件。
- 失效与回滚策略:
- 当 VIP 过期(检测点:每日任务/登录/接口访问拦截),若启用强绑定则触发降级:
-`users.role``normal_admin` 回滚至上次 `approve``previous_role`(若无记录:`is_owner=1` 回滚到 `'owner'`,否则 `'staff'`)。
- 写入 `expire` 审计remark 记录触发缘由)。
- 风险与缓解方案A局限
- `users.role` 为单值字段,无法并存多角色;短期仅用于识别 Admin-Lite 访问权限,不影响用户侧功能(用户侧权限建议依赖 `is_owner` 与接口授权)。
- 后续若需多角色并存,按 13.2 的方案B 迁移至 `user_roles`,并在迁移期间保留写入 `users.role` 的兼容逻辑。
- 测试要点补充:
- 申请→审批通过:审计有 `apply``approve` 两条,`users.role` 变为 `normal_admin`Admin-Lite 可登录访问。
- 申请→驳回:审计有 `apply``reject``users.role` 不变Admin-Lite 拒绝访问。
- 撤销:`users.role` 回滚为 `previous_role`,新增 `revoke` 审计;再次访问被拒绝。
- VIP 过期:若强绑定开启,触发降级并写入 `expire`Admin-Lite 访问被拒绝;关闭强绑定时仅告警不降级。
—— 本文档为当前功能实现说明与落地细节,不含历史变更记录;如有与数据模型不匹配之处,以 `/doc/database_documentation.md` 为准并及时校准。

View File

@@ -21,6 +21,49 @@ cd backend
```
> 说明:`application.properties` 已内置远程库默认值(`DB_URL/DB_USER/DB_PASSWORD`)。除非你的终端已设置了同名环境变量并想覆盖,否则无需再配置。
### 启动 Python 条码识别服务(可选)
本项目提供 Python 条码识别服务FastAPI目录 `backend/txm`),可与 Java 同时启动或独立启动。
- 独立启动(推荐先验证):
```powershell
cd .\backend\txm; python -m pip install -r requirements.txt; python -m app.server.main
```
- 随 Java 一并启动(通过环境变量启用;默认关闭):
```powershell
$env:PY_BARCODE_ENABLED="true"; $env:PY_BARCODE_WORKDIR=".\txm"; $env:PY_BARCODE_PYTHON="python"; .\mvnw.cmd spring-boot:run -DskipTests
```
可覆盖的相关配置键(同名环境变量可覆盖,括号为默认值):
```
python.barcode.enabled=${PY_BARCODE_ENABLED:false}
python.barcode.working-dir=${PY_BARCODE_WORKDIR:./txm}
python.barcode.python=${PY_BARCODE_PYTHON:python}
python.barcode.app-module=${PY_BARCODE_APP_MODULE:app.server.main}
python.barcode.use-module-main=${PY_BARCODE_USE_MODULE:true}
python.barcode.host=${PY_BARCODE_HOST:127.0.0.1}
python.barcode.port=${PY_BARCODE_PORT:8000}
python.barcode.health-path=${PY_BARCODE_HEALTH:/openapi.json}
python.barcode.startup-timeout-sec=${PY_BARCODE_TIMEOUT:20}
python.barcode.log-file=${PY_BARCODE_LOG:}
```
Java 侧代理接口:`POST /api/barcode/scan`(表单字段名:`file`)。
返回:
```
{
success: true,
barcodeType: 'EAN13' | 'CODE128' | 'QRCODE' | ...,
barcode: '字符串',
others: [{ type, code }, ...] // 可能为空
}
```
说明:优先 EAN-13否则返回任意码制的第一个结果并同时返回 others。
### 可选:显式指定远程数据库(避免被旧环境变量覆盖)
如需显式声明一次连接信息(建议在怀疑本机已有旧变量时使用):
```powershell
@@ -72,6 +115,25 @@ $env:CORS_ALLOWED_ORIGINS="http://localhost:5173"
cd backend; .\mvnw.cmd clean package -DskipTests; java -jar .\target\demo-0.0.1-SNAPSHOT.jar
```
### SMTP 邮件配置Windows PowerShell
请在启动后端前设置以下环境变量QQ 邮箱):
```powershell
$env:MAIL_HOST="smtp.qq.com"; $env:MAIL_PORT="465"; $env:MAIL_PROTOCOL="smtps"; $env:MAIL_USERNAME="sdssds@163.com"; $env:MAIL_PASSWORD="NQLihrab8vGiAjiE"; $env:MAIL_FROM="sdssds@163.com"; $env:MAIL_SUBJECT_PREFIX="[配件查询]"
```
说明:
- MAIL_USERNAME/MAIL_FROM发件邮箱地址
- MAIL_PASSWORDSMTP 授权码
- 采用 465 + SMTPS + SSL若使用 587请改为 `MAIL_PORT=587` 并设置 `spring.mail.properties.mail.smtp.starttls.enable=true`
### 邮箱验证码接口
- POST `/api/auth/email/send`:请求体 `{ email, scene? }`,成功返回 `{ ok, cooldownSec }`
- POST `/api/auth/email/login`:请求体 `{ email, code }`,成功返回 `{ token, expiresIn, user }`
返回的 JWT 通过 `Authorization: Bearer <token>` 使用,解析后包含 `userId/shopId/email/provider` 等声明。
以上即为在新电脑上启动后端并连接远程数据库的最小步骤。

View File

@@ -0,0 +1,35 @@
## 当前开发进度概览2025-09-27
### 一、后端Spring Boot
- **会员体系**:已实现 `vip_users``vip_price``vip_recharges` CRUD 及接口 `/api/vip/*``/api/admin/vip/*`
- **公告管理**`/api/admin/notices` 完整可用,支持创建/编辑/发布/下线。
- **附件系统**`/api/attachments` 支持本地存储、hash 去重与 URL 校验;用户与管理端均在使用。
- **条码代理**`/api/barcode/scan` 代理 Python TXM 服务,需人工启动 Python 端。
- **鉴权机制**:管理员接口仍依赖 `X-Admin-Id` 兼容头JWT 登录接口已实现但尚未前端集成。
### 二、管理端Vue 3 + Element Plus
- 已上线模块VIP 系统、VIP 列表、公告管理、咨询回复、用户管理、配件管理、主数据字典、**配件审核(新增 Submissions 页面,支持筛选/详情/通过/驳回/导出)**。
- 尚缺模块:登录页、角色权限细分、操作日志可视化。
- 交互问题:表格分页统一使用后端分页参数,个别页面(如 VIP 列表)暂未展示 total需要补齐。
### 三、移动端uni-app
- **主流程**:商品、订单、客户、供应商、报表等基础功能可用。
- **登录/注册**`pages/auth/login.vue` 新增邮箱密码登录、验证码注册、忘记密码三合一页面,调用 `/api/auth/password/login``/api/auth/email/register``/api/auth/email/reset-password`;验证码发送支持 register/reset 场景。
- **配件提交(新增)**`pages/product/submit.vue``pages/product/submissions.vue``pages/product/submission-detail.vue` 已接入配件提交、列表、详情;支持多图上传、参数 JSON、驳回重新提交与后端 `/api/products/submissions*` 系列接口对齐。
- **VIP 页面**`pages/my/vip.vue` 已接入状态查询与一键开通,待接续续费提示与权益引导。
- **咨询入口**:请求与管理端对接正常,尚缺悬浮入口。
- **扫码**:仅 App 端调用条码识别,小程序端提示不支持扫码。
### 四、数据库
- 远程库包含 VIP、附件等新增表本地脚本 `backend/db/db.sql` 尚未同步;执行结构变更需通过 MysqlMCP 并手动更新脚本。
- 需追加的建表脚本:`vip_users``vip_price``vip_recharges``attachments` 等。
### 五、待解决问题
1. **管理员登录上线**:前端需接入 `/api/admin/auth/login` 并替换本地 ADMIN_ID 写死逻辑。
2. **VIP 续期逻辑**`/api/vip/pay` 当前覆盖式设置到期时间,需确认是否改为顺延有效期并记录 `expire_from`
3. **公告富文本**:现为纯文本,若需富文本需评估安全策略。
4. **前端分页补充**:管理端表格统一展示 total 并接入分页控件。
5. **数据库脚本同步**:更新 `backend/db/db.sql` 与 doc 文档保持一致,避免新环境缺表。
6. **条码服务部署**:需编写部署说明,确定 Python TXM 服务是否随 Java 进程自动启动。
7. **登录方式文档同步**OpenAPI 已新增 `/api/auth/email/reset-password`,前端登录页三流程已实现,需对外文档同步展示入口与约束。
8. **配件提交流程验收**:需补充测试用例(提交→审核→导出),并确认型号唯一策略是否支持多店共享或全局唯一。