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

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` 为准并及时校准。