### 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 点击“申请”提示需先成为 VIP;VIP 点击“申请”按配置自动通过或进入待审;审批通过后刷新权限可访问。 - 范围:仅能查看与操作本店 `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 '平台管理员ID(apply时可空)', 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`。 - 鉴权与Token(Admin-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` 为准并及时校准。