23 KiB
23 KiB
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 已移除);统一由用户端一键开通或管理员直接设置。
- GET
- 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并标注实现状态,避免“仅设计未实现”的文档污染。
- GET
示例返回(状态查询):
{
"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进行跨越两个月以上的日期筛选时,给出轻提示:“普通用户仅显示近两个月数据,开通VIP可查看全部历史”。
- 页面不做本地硬裁剪,一切以后端约束为准,前端仅用于用户沟通与引导开通。
8. 业务流程(Graphviz)
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)
- 数据库脚本对齐(仅通过 MysqlMCP 执行)
- 若目标库缺失
vip_users:按/doc/database_documentation.md定义创建,并补充必要索引与外键。 - 备注:执行后同步更新
/doc/database_documentation.md中的更新时间与差异说明(保持与线上一致)。
- 若目标库缺失
- 后端接口完备
- 补齐
AdminVipController的参数校验、店铺隔离、分页 total 统计(可选)。 - 新增 App 侧接口:
GET /api/vip/status与POST /api/vip/pay(点击即开通临时方案)。当开始实现后,将路径登记至/doc/openapi.yaml并标注状态。 - 配置读取:接入
system_parameters的vip.price/vip.durationDays。
- 补齐
- 管理端接入
VipList.vue与VipReview.vue接口字段对齐;在创建/编辑时透传shopId(或后端根据用户解析)。- 在“VIP管理”中新增“价格设置”对话框:读取/修改
vip.price(走系统参数接口或新增专用接口)。
- App 接入
pages/my/vip.vue调用GET /api/vip/status;“立即开通”改为调用POST /api/vip/pay(临时:点击即开通)。
- 定时/登录时到期检查
- 每日定时任务或用户登录时,对已到期记录置
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:订单/支付/库存流水/其他收支等查询接口,返回记录的最早时间不早于“当前时间-60天”(默认值);当调整
- 一键支付开通:
- 点击开通后接口返回成功,
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:查询本人申请状态与生效情况。
- POST
- 平台管理端:
- GET
/api/admin/normal-admin/applications:申请列表(支持状态/关键词/时间范围)。 - POST
/api/admin/normal-admin/applications/{id}/approve:审批通过。 - POST
/api/admin/normal-admin/applications/{id}/reject:审批驳回(需备注)。
- GET
- 普通管理端鉴权(见 13.4):
- 登录沿用用户侧认证:
POST /api/auth/email/login或现有登录接口,后端签发 Token 时加入scope/role标识(如normal_admin)。
- 登录沿用用户侧认证:
示例返回(申请状态):
{ "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固化为当前用户所属店铺,杜绝越权访问。
- 校验登录 Token 为用户侧 Token;读取
- 路由命名建议:
/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)
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 实施计划(补充)
- 后端:
- 新增
NormalAdminAuthInterceptor与/api/normal-admin/**路由;实现申请接口与(可选)平台审批接口;复用配件审核服务,默认按当前用户shop_id限定范围。 - Token 扩展:在用户侧登录成功时,根据数据库角色写入
role/scope=normal_admin。 - 配置读取:接入
normalAdmin.autoApprove、normalAdmin.requiredVipActive(来自system_parameters或全局参数表)。
- 前端(admin-lite):
- 建立最小可用工程,复制并精简
Submissions.vue;新增My.vue;完善登录页与路由守卫(未授权跳转登录)。 - “我的”页集成申请按钮与状态显示;成功后刷新权限并跳转审核页。
- 文档与 OpenAPI:
- 任一侧开工后,将上述接口登记至
/doc/openapi.yaml并标注实现状态(❌/✅)。
13.9 风险与注意事项
- 权限收敛:严禁普通管理员访问跨店铺数据;所有查询/导出在服务层统一追加
shop_id=当前用户.shop_id条件。 - 身份冲突:若同一用户既是平台管理员又是普通管理员,需优先以平台后台入口登录对应端;两个端的 Token/域名/路由需区分。
- 续期与降级:明确 VIP 失效后的权限处理策略与用户提示文案,避免误用。
- 审计留痕:申请/审批/撤销必须入审计表;审核操作记录操作人与时间。
13.10 选型落地(方案A)
-
选型结论:
- 审批策略采用「方案A(平台审核)」:提交申请→平台管理员审核→通过后赋予“普通管理员”权限。
- 数据模型采用「方案A(复用
users.role+ 审计表)」:当前生效权限以users.role为准,申请/审批/撤销全量留痕于审计表。
-
审计表 DDL(提交后通过 MysqlMCP 执行;此处为权威定义,禁止硬编码):
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则转调用审批通过流程。
- 校验:已登录 + VIP 有效(当
- 审批列表:GET
/api/admin/normal-admin/applications- 规则:按照审计记录推导“当前为 apply 且未被approve/reject”的记录;支持关键词与时间过滤;返回
{ list, total }。
- 规则:按照审计记录推导“当前为 apply 且未被approve/reject”的记录;支持关键词与时间过滤;返回
- 审批通过: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。
- 行为:将
- 用户申请:POST
-
鉴权与Token(Admin-Lite):
- 登录沿用用户端登录接口(邮箱/密码),签发 Token 时附带
role与shopId;Admin-Lite 入口要求role='normal_admin'。 - 在拦截器
NormalAdminAuthInterceptor中:- 校验
role='normal_admin';若requiredVipActive=true,同时校验当前用户 VIP 有效。 - 将请求上下文中的
shop_id固化为当前用户shop_id,所有查询自动拼接该条件。
- 校验
- 登录沿用用户端登录接口(邮箱/密码),签发 Token 时附带
-
失效与回滚策略:
- 当 VIP 过期(检测点:每日任务/登录/接口访问拦截),若启用强绑定则触发降级:
- 将
users.role由normal_admin回滚至上次approve的previous_role(若无记录:is_owner=1回滚到'owner',否则'staff')。 - 写入
expire审计(remark 记录触发缘由)。
- 将
- 当 VIP 过期(检测点:每日任务/登录/接口访问拦截),若启用强绑定则触发降级:
-
风险与缓解(方案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 为准并及时校准。