Files
PartsInquiry/doc/vip_development.md
2025-09-27 22:57:59 +08:00

332 lines
23 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.

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