Procházet zdrojové kódy

优化代码,添加接口

liuchengsen před 2 měsíci
rodič
revize
5cd107af9d
32 změnil soubory, kde provedl 1771 přidání a 483 odebrání
  1. 2 0
      .gitignore
  2. 0 0
      build-output.txt
  3. 26 0
      deploy/.env.example
  4. 20 0
      deploy/docker-compose.yml
  5. 110 0
      deploy/start-prod.bat
  6. 287 65
      docs/01-产品文档.md
  7. 225 73
      docs/02-架构文档.md
  8. 158 55
      docs/06-验收流程与标准.md
  9. 16 0
      pom.xml
  10. 2 0
      zhijiayun-common/src/main/java/com/xuekairui/common/ErrorCode.java
  11. binární
      zhijiayun-gateway/src/main/resources/apiclient_cert.p12
  12. 32 15
      zhijiayun-gateway/src/main/resources/application-dev.yml
  13. 38 3
      zhijiayun-gateway/src/main/resources/application-prod.yml
  14. 47 0
      zhijiayun-gateway/src/main/resources/db/migration-v2.sql
  15. 12 28
      zhijiayun-gateway/src/main/resources/db/schema.sql
  16. 10 2
      zhijiayun-gateway/src/test/resources/test-schema.sql
  17. 14 0
      zhijiayun-invite/src/main/java/com/xuekairui/invite/controller/InviteController.java
  18. 5 0
      zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/InviteConfigRequest.java
  19. 4 0
      zhijiayun-invite/src/main/java/com/xuekairui/invite/entity/InviteConfig.java
  20. 4 0
      zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteConfigService.java
  21. 126 6
      zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java
  22. 8 0
      zhijiayun-payment/pom.xml
  23. 205 0
      zhijiayun-payment/src/main/java/com/xuekairui/payment/config/PaymentConfig.java
  24. 24 6
      zhijiayun-payment/src/main/java/com/xuekairui/payment/config/PaymentProperties.java
  25. 99 61
      zhijiayun-payment/src/main/java/com/xuekairui/payment/controller/PaymentCallbackController.java
  26. 4 3
      zhijiayun-payment/src/main/java/com/xuekairui/payment/controller/PaymentController.java
  27. 109 63
      zhijiayun-payment/src/main/java/com/xuekairui/payment/service/PaymentOrderService.java
  28. 40 12
      zhijiayun-test/src/main/resources/static/invite-test.html
  29. 2 1
      zhijiayun-user/src/main/java/com/xuekairui/user/config/SecurityConfig.java
  30. 5 7
      zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java
  31. 118 64
      zhijiayun-user/src/main/java/com/xuekairui/user/service/MembershipService.java
  32. 19 19
      zhijiayun-user/src/main/java/com/xuekairui/user/service/UserLevelService.java

+ 2 - 0
.gitignore

@@ -12,6 +12,8 @@ logs/
 ### 敏感配置(严禁提交) ###
 deploy/.env
 .env
+### 支付证书(严禁提交) ###
+certs/
 
 ### IntelliJ IDEA ###
 .idea/modules.xml

+ 0 - 0
build-output.txt


+ 26 - 0
deploy/.env.example

@@ -34,3 +34,29 @@ WECHAT_REDIRECT_URI=https://your-domain.com/api/auth/wechat/callback
 
 # ---- 邀请链接基础地址(生产域名) ----
 INVITE_BASE_URL=https://your-domain.com
+
+# ---- 支付宝当面付(公钥模式优先) ----
+ALIPAY_APP_ID=请填入支付宝应用ID
+ALIPAY_NOTIFY_URL=https://priceapi.kailin.com.cn/api/payment/callback/alipay
+# 公钥模式(优先):填入私钥字符串和支付宝公钥
+ALIPAY_PRIVATE_KEY=请填入应用私钥
+ALIPAY_PUBLIC_KEY=请填入支付宝公钥
+# 证书模式(备选):未配置 ALIPAY_PUBLIC_KEY 时回退到证书模式
+# ALIPAY_PRIVATE_KEY_PATH=certs/prod/alipay/app-private-key.txt
+# ALIPAY_APP_CERT_PATH=certs/prod/alipay/appCertPublicKey.crt
+# ALIPAY_CERT_PATH=certs/prod/alipay/alipayCertPublicKey_RSA2.crt
+# ALIPAY_ROOT_CERT_PATH=certs/prod/alipay/alipayRootCert.crt
+
+# ---- 微信支付(Native扫码) ----
+# 生产证书放入 certs/prod/wechat/ 目录(三份文件:apiclient_key.pem, apiclient_cert.pem, apiclient_cert.p12)
+WECHAT_PAY_APP_ID=请填入微信应用AppID
+WECHAT_PAY_MCH_ID=请填入商户号
+WECHAT_PAY_API_V3_KEY=请填入APIv3密钥
+WECHAT_PAY_PRIVATE_KEY_PATH=certs/prod/wechat/apiclient_key.pem
+WECHAT_PAY_PRIVATE_CERT_PATH=certs/prod/wechat/apiclient_cert.pem
+WECHAT_PAY_KEY_PATH=certs/prod/wechat/apiclient_cert.p12
+WECHAT_PAY_SERIAL_NO=请填入商户证书序列号
+# 微信支付公钥(新商户必须配置)
+WECHAT_PAY_PUBLIC_KEY_PATH=certs/prod/wechat/pub_key.pem
+WECHAT_PAY_PUBLIC_KEY_ID=请填入公钥ID
+WECHAT_PAY_NOTIFY_URL=https://priceapi.kailin.com.cn/api/payment/callback/wechat

+ 20 - 0
deploy/docker-compose.yml

@@ -79,6 +79,7 @@ services:
     volumes:
       - ./zhijiayun-gateway-1.0.0.jar:/app/app.jar:ro
       - app-logs:/var/log/zhijiayun-pharmacy
+      - ./certs:/app/certs:ro
     working_dir: /app
     environment:
       TZ: Asia/Shanghai
@@ -107,6 +108,25 @@ services:
       WECHAT_REDIRECT_URI: ${WECHAT_REDIRECT_URI}
       # 邀请链接
       INVITE_BASE_URL: ${INVITE_BASE_URL:-https://your-domain.com}
+      # 支付宝(证书模式)
+      ALIPAY_APP_ID: ${ALIPAY_APP_ID:-}
+      ALIPAY_NOTIFY_URL: ${ALIPAY_NOTIFY_URL:-https://priceapi.kailin.com.cn/api/payment/callback/alipay}
+      ALIPAY_PRIVATE_KEY_PATH: ${ALIPAY_PRIVATE_KEY_PATH:-}
+      ALIPAY_APP_CERT_PATH: ${ALIPAY_APP_CERT_PATH:-}
+      ALIPAY_CERT_PATH: ${ALIPAY_CERT_PATH:-}
+      ALIPAY_ROOT_CERT_PATH: ${ALIPAY_ROOT_CERT_PATH:-}
+      # 支付宝公钥模式(备选)
+      ALIPAY_PRIVATE_KEY: ${ALIPAY_PRIVATE_KEY:-}
+      ALIPAY_PUBLIC_KEY: ${ALIPAY_PUBLIC_KEY:-}
+      # 微信支付
+      WECHAT_PAY_APP_ID: ${WECHAT_PAY_APP_ID:-}
+      WECHAT_PAY_MCH_ID: ${WECHAT_PAY_MCH_ID:-}
+      WECHAT_PAY_API_V3_KEY: ${WECHAT_PAY_API_V3_KEY:-}
+      WECHAT_PAY_PRIVATE_KEY_PATH: ${WECHAT_PAY_PRIVATE_KEY_PATH:-certs/prod/wechat/apiclient_key.pem}
+      WECHAT_PAY_PRIVATE_CERT_PATH: ${WECHAT_PAY_PRIVATE_CERT_PATH:-certs/prod/wechat/apiclient_cert.pem}
+      WECHAT_PAY_KEY_PATH: ${WECHAT_PAY_KEY_PATH:-certs/prod/wechat/apiclient_cert.p12}
+      WECHAT_PAY_SERIAL_NO: ${WECHAT_PAY_SERIAL_NO:-}
+      WECHAT_PAY_NOTIFY_URL: ${WECHAT_PAY_NOTIFY_URL:-https://priceapi.kailin.com.cn/api/payment/callback/wechat}
     command: >
       java
       -server

+ 110 - 0
deploy/start-prod.bat

@@ -0,0 +1,110 @@
+@echo off
+chcp 65001 > nul
+setlocal enabledelayedexpansion
+
+:: =============================================================
+:: 智价云药店系统 - 生产环境启动脚本
+:: 用法:start-prod.bat [start|stop|build]
+:: 依赖:JDK 17
+:: =============================================================
+
+title 智价云药店 - 生产环境
+
+set APP_NAME=zhijiayun-pharmacy
+set JAR_NAME=zhijiayun-gateway-1.0.0.jar
+set PROJECT_DIR=%~dp0..
+set DEPLOY_DIR=%~dp0
+set JAR_PATH=%DEPLOY_DIR%%JAR_NAME%
+set LOG_DIR=%DEPLOY_DIR%logs
+
+:: JVM 参数(生产推荐配置)
+set JAVA_OPTS=-server -Xms512m -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%LOG_DIR%\heapdump.hprof -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai
+
+set ACTION=%1
+if "%ACTION%"=="" set ACTION=start
+
+if "%ACTION%"=="start"  goto :do_start
+if "%ACTION%"=="stop"   goto :do_stop
+if "%ACTION%"=="build"  goto :do_build
+goto :show_help
+
+:: ============================================================
+:do_build
+echo [INFO] Maven 打包(跳过测试)...
+cd /d "%PROJECT_DIR%"
+call mvn clean package -DskipTests -pl zhijiayun-gateway -am
+if errorlevel 1 (
+    echo [ERROR] 构建失败
+    pause
+    exit /b 1
+)
+echo [INFO] 复制 JAR 到 deploy 目录...
+copy /Y "%PROJECT_DIR%\zhijiayun-gateway\target\%JAR_NAME%" "%DEPLOY_DIR%" > nul
+echo [INFO] 构建完成:%JAR_PATH%
+goto :eof
+
+:: ============================================================
+:do_start
+:: 检查 JAR 是否存在
+if not exist "%JAR_PATH%" (
+    echo [WARN] JAR 包不存在,自动执行构建...
+    call :do_build
+)
+
+:: 检查端口
+netstat -ano | findstr ":8002 " | findstr "LISTENING" > nul 2>&1
+if not errorlevel 1 (
+    echo [WARN] 端口 8002 已被占用
+    netstat -ano | findstr ":8002 " | findstr "LISTENING"
+    pause
+    exit /b 1
+)
+
+if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
+
+echo.
+echo ================================================================
+echo   %APP_NAME% - 生产环境
+echo   JAR: %JAR_PATH%
+echo   日志: %LOG_DIR%
+echo   端口: 8002
+echo   Profile: prod
+echo ================================================================
+echo.
+
+:: 加载 .env 文件(如果存在)
+if exist "%DEPLOY_DIR%.env" (
+    echo [INFO] 加载环境变量: .env
+    for /f "usebackq tokens=1,* delims==" %%A in ("%DEPLOY_DIR%.env") do (
+        set line=%%A
+        if not "!line:~0,1!"=="#" (
+            if not "%%B"=="" set "%%A=%%B"
+        )
+    )
+)
+
+java %JAVA_OPTS% ^
+    -jar "%JAR_PATH%" ^
+    --spring.profiles.active=prod
+goto :eof
+
+:: ============================================================
+:do_stop
+echo [INFO] 查找并停止 %APP_NAME%...
+for /f "tokens=5" %%i in ('netstat -ano ^| findstr ":8002 " ^| findstr "LISTENING"') do (
+    echo [INFO] 终止进程 PID=%%i
+    taskkill /PID %%i /F > nul 2>&1
+)
+echo [INFO] 应用已停止
+goto :eof
+
+:: ============================================================
+:show_help
+echo 用法: start-prod.bat [命令]
+echo.
+echo   start     启动应用(默认)
+echo   stop      停止应用
+echo   build     Maven 打包并复制到 deploy 目录
+echo.
+pause
+goto :eof

+ 287 - 65
docs/01-产品文档.md

@@ -6,7 +6,7 @@
 > **产品形态**:**多端交付策略** - MVP首选Windows独立桌面应用(.exe),后续扩展微信小程序、APP、公众号等多渠道。  
 > **目标用户**:药店采购人员、药师,目标日活20万+。
 >
-> 📅 版本:v1.0-MVP | 📆 更新日期:2026-06-16 | ✅ 状态:开发中
+> 📅 版本:v1.1-MVP | 📆 更新日期:2026-06-25 | ✅ 状态:开发中
 
 ---
 
@@ -35,19 +35,25 @@
 | 🔒 安全可控 | Windows独立桌面应用,自建安全策略 | 不依赖系统浏览器 |
 | ⚡ 高效便捷 | 一键启动,并行查询多平台 | 提升采购效率50%+ |
 | 🎯 精准比价 | 基于用户自有账号查询 | 价格真实可靠 |
-| 👥 裂变增长 | 邀请其他药店注册获得额外配额 | 快速扩大用户规模 |
+| 👥 裂变增长 | 邀请其他药店注册获得会员时长奖励 | 快速扩大用户规模 |
+| 💳 会员变现 | 支持微信/支付宝购买会员套餐 | 商业闭环 |
 
-**🎯 MVP目标**:验证核心业务流程(注册→绑定账号→聚合查询→跳转购买),快速上线获取种子药店用户,收集真实使用反馈。
+**🎯 MVP目标**:验证核心业务流程(注册→绑定账号→聚合查询→跳转购买→购买会员),快速上线获取种子药店用户,收集真实使用反馈。
 
-### 1.2 目标用户
+### 1.2 目标用户与角色
 
-| 角色 | 说明 | 核心需求 |
-|------|------|----------|
-| 药店采购员 | 负责药店日常药品采购 | 快速找到最低价格,生成采购链接 |
-| 执业药师 | 审核药品质量和供应商资质 | 查看药品详细信息、供应商信誉 |
-| 药店老板 | 控制采购成本,优化供应链 | 采购数据分析、成本控制报表 |
-| 运营人员 | 管理邀请配置、发放优惠券 | 提高用户注册率和活跃度 |
-| 管理员 | 系统全局管理 | 用户管理、数据统计、风控监控 |
+| 角色 | 系统角色 | 说明 | 核心需求 |
+|------|----------|------|----------|
+| 药店采购员 | USER | 负责药店日常药品采购 | 快速找到最低价格,生成采购链接 |
+| 执业药师 | USER | 审核药品质量和供应商资质 | 查看药品详细信息、供应商信誉 |
+| 药店老板 | USER | 控制采购成本,优化供应链 | 采购数据分析、成本控制报表 |
+| 运营人员 | ADMIN | 管理邀请配置、发放优惠券、审核执照 | 提高用户注册率和活跃度 |
+| 超级管理员 | SUPER_ADMIN | 系统全局管理、角色赋权 | 用户管理、数据统计、风控监控 |
+
+**三级RBAC权限模型**:
+- **USER**:仅可访问用户侧接口(比价、关注、搜索历史等)
+- **ADMIN**:可访问运营后台接口(用户管理、配置管理、审核等)
+- **SUPER_ADMIN**:拥有全部权限,可管理用户角色赋权
 
 ### 1.3 关键特点
 
@@ -56,6 +62,9 @@
 - **用户绑定账号**:用户输入自己在药师帮/药帮忙/1药城的账号密码(AES-256加密存储)
 - **聚合查询**:并行查询已绑平台,展示比价结果
 - **跳转原平台**:点击"去购买"跳转到原平台完成交易,智价云不提供交易服务
+- **会员体系**:PLUS/PRO/ULTRA三档会员,权益驱动爬虫配额
+- **支付闭环**:支持微信/支付宝扫码支付购买会员套餐
+- **裂变增长**:邀请奖励为会员时长,邀请人获赠1个月普通会员
 - **用户粘性**:通过多渠道触达,提高用户使用频次和留存率
 
 ---
@@ -67,35 +76,68 @@
 | 功能 | 说明 |
 |------|------|
 | 短信验证码登录 | 手机号 + 6位验证码,新用户自动注册 |
+| 密码登录 | 手机号/昵称 + 密码(BCrypt加密),支持设置/修改密码 |
 | 微信扫码登录 | 微信开放平台扫码,首次登录需绑定手机号 |
+| 修改手机号 | 已登录用户通过验证码验证后修改绑定手机号 |
 | Token 管理 | Access Token(24h)+ Refresh Token(7d) |
-| 用户信息 | 昵称、头像、手机号、等级信息、药店名称 |
+| 用户信息 | 昵称、头像、手机号、会员等级、药店名称、地区信息 |
+| 登录来源追踪 | 记录登录设备、登录IP、登录来源(WINDOWS/MINIAPP/ANDROID/IOS/WECHAT) |
+
+### 2.2 会员体系
+
+系统采用**独立会员体系**,会员等级驱动爬虫配额权益:
 
-### 2.2 用户等级体系
+| 会员等级 | 编码 | 每日配额 | 每月配额 | 每年配额 | 最大并发 | 适用场景 |
+|----------|------|----------|----------|----------|----------|----------|
+| 普通会员 | PLUS | 10次 | 300次 | 3650次 | - | 小型药店日常采购 |
+| 高级会员 | PRO | 999次 | 29970次 | 364635次 | 5 | 中型药店高频采购 |
+| 超级会员 | ULTRA | 999次 | 29970次 | 364635次 | 10 | 大型连锁药店批量采购 |
 
-| 等级 | 每日配额 | 每月配额 | 并发数 | 适用场景 |
-|------|----------|----------|--------|----------|
-| 普通用户 (NORMAL) | 5次 | 100次 | 1 | 小型药店试用 |
-| VIP会员 (VIP) | 20次 | 500次 | 3 | 中型药店日常采购 |
-| SVIP会员 (SVIP) | 100次 | 3000次 | 5 | 大型连锁药店批量采购 |
+**会员来源**:
+- `ADMIN_GRANT`:运营手动发放
+- `RECHARGE`:用户充值购买
+- `SYSTEM`:系统赠送(如新用户注册赠送、邀请奖励)
 
-**版本化管理**:支持运营动态调整等级规则,新用户自动使用最新版本,老用户由运营决定迁移到哪个版本。
+**会员有效期**:支持按天/月设置有效期,到期自动失效。
+
+**用户侧接口**:
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/membership/my | 查询我的会员信息(等级、到期时间、来源等) |
+| GET /api/membership/benefits?level=PLUS | 查询指定等级的权益配置详情 |
+| GET /api/level/list | 获取所有会员等级列表(公开,含定价方案) |
+| GET /api/level/{id} | 获取等级详情(公开) |
+| GET /api/level/my | 获取当前用户等级信息(含配额、到期时间、套餐) |
 
 ### 2.3 爬虫配额体系
 
-用户总配额 = **等级基础配额** + **额外配额**
+用户总配额 = **会员权益配额** + **额外配额**
 
 额外配额来源:
 
 | 来源 | 类型标识 | 过期策略 | 说明 |
 |------|----------|----------|------|
-| 邀请奖励 | INVITE | 永不过期 | 每成功邀请1家药店注册,获得配置次数 |
+| 邀请奖励 | INVITE | 永不过期 | 历史遗留的爬虫次数奖励(当前邀请奖励已改为会员时长) |
 | 购买会员 | PURCHASE | 可配置 | 购买/升级会员时发放 |
 | 运营发放 | ADMIN | 可配置 | 运营手动为指定用户发放 |
 
-**消耗优先级**:优惠券 > 额外配额(快过期优先)> 等级基础配额
+**配额周期**:日配额(每日重置)+ 月配额(每月重置)+ 年配额(每年重置)
+
+**消耗优先级**:优惠券 > 额外配额(快过期优先)> 会员权益配额
+
+**过期控制**:每笔发放独立设置 `expire_time`,定时任务每小时扫描标记过期记录。
+
+**用户侧接口**:
 
-### 2.4 药品比价核心功能(Windows桌面应用)
+| 接口 | 说明 |
+|------|------|
+| GET /api/crawler/status | 获取爬虫配额状态(今日/本月/本年 已用、剩余、来源明细) |
+| POST /api/crawler/consume | 消耗爬虫次数(前端启动爬虫前调用) |
+| GET /api/crawler/logs?days=7 | 获取爬虫使用记录(最近N天) |
+| GET /api/invite/extra-quota | 查询我的可用额外配额总数(邀请奖励 + 运营发放) |
+
+### 2.4 药品比价核心功能
 
 #### 业务流程
 
@@ -106,6 +148,13 @@
 → 点击"去XX平台购买"跳转到原平台完成交易
 ```
 
+#### 药品搜索接口
+
+| 接口 | 说明 |
+|------|------|
+| POST /api/search/query | 药品搜索比价,消耗1次爬虫配额,返回各平台报价 |
+| GET /api/search/suggestions | 获取热门药品搜索建议(公开,无需登录) |
+
 #### 支持的医药平台(MVP首期3个核心平台)
 
 **B2B批发平台(3个 - MVP启用):**
@@ -120,16 +169,21 @@
 
 **说明**:代码支持34个平台,但MVP首期仅启用3个核心平台,后续通过运营配置逐步开放。
 
-#### Windows桌面应用特点
+#### 平台账号绑定
 
-| 特性 | 说明 |
+| 接口 | 说明 |
 |------|------|
-| **独立浏览器引擎** | 内置WebView2(Microsoft Edge Chromium内核),不依赖系统浏览器 |
-| **自建安全策略** | Cookie管理、Session隔离、AES-256加密存储均由应用自行管理 |
-| **窗口布局** | 左侧导航栏220px + 顶栏56px + 主内容区 |
-| **多标签支持** | 可同时打开多个查询结果和原平台页面 |
-| **离线缓存** | 查询历史本地缓存(SQLite),提升二次查询速度 |
-| **托盘通知** | 降价提醒、审核结果等通过系统托盘通知 |
+| POST /api/platform-account/bind | 绑定B2B平台账号(AES-256加密存储) |
+| DELETE /api/platform-account/unbind/{platformCode} | 解绑平台账号 |
+| GET /api/platform-account/list | 查询已绑定的平台账号列表 |
+| POST /api/platform-account/verify/{platformCode} | 验证平台账号有效性 |
+
+#### 平台配置查询
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/platform-config/enabled | 查询所有启用的平台列表(公开) |
+| GET /api/platform-config/{platformCode} | 查询指定平台配置(公开) |
 
 #### 比价结果展示
 
@@ -152,19 +206,34 @@
 药店A获取邀请链接 → 分享给其他药店B 
 → B打开落地页(显示A的药店名称和奖励说明)
 → B下载客户端并注册(自动填入邀请码)
-→ A获得爬虫次数奖励(用于更多比价)
+→ A获得会员时长奖励(邀请人1个月普通会员)
+→ B获得新用户注册赠送(2个月普通会员)
 ```
 
+#### 奖励机制
+
+| 角色 | 奖励内容 | 说明 |
+|------|----------|------|
+| 邀请人 | 1个月普通会员 | 每成功邀请1家药店注册即发放 |
+| 被邀请人 | 2个月普通会员 | 新用户注册即送1个月,填写邀请码加送1个月 |
+
 #### 支持渠道
 
 | 渠道 | channel | 打开方式 | 说明 |
 |------|---------|----------|------|
-| Windows App | app | download | 直接下载客户端 |
+| Windows App | windows | download | 直接下载客户端(默认渠道) |
 | 微信公众号 | wechat | redirect | 跳转公众号/H5 |
 | 小程序 | miniapp | miniapp | 打开小程序 |
 | 钉钉 | dingtalk | deeplink | 应用内深度链接 |
 | 飞书 | feishu | deeplink | 应用内深度链接 |
 
+#### 邀请码安全生成与智能解析
+
+- 邀请码采用安全随机生成算法,支持链接和码双模式呈现
+- 落地页支持URL Scheme(`zhijiayun://invite?code=XXX`)和DeepLink自动填入邀请码
+- 支持链接/邀请码双模式智能解析与注册自动绑定
+- 落地页标题支持 `{inviter}` 和 `{pharmacy}` 动态占位符替换
+
 #### 分享文案示例
 
 ```
@@ -177,8 +246,22 @@ https://app.zhijiayun.com/invite/ABC123
 
 #### 邀请统计
 
-- 摘要文本:`已邀请5家药店,3家注册 +21次`
+- 摘要文本:`已邀请5家药店,3家注册,累计获得3个月会员`
 - 每条记录显示:被邀请药店名称、注册状态(已注册/待注册)、奖励次数
+- 统计数据:总邀请数、已注册数、累计奖励、链接点击次数、今日邀请数/剩余次数
+
+#### 用户侧接口
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/invite/code | 获取我的邀请码和邀请链接(含分享文案) |
+| GET /api/invite/stats | 获取邀请统计(邀请人数、注册人数、累计奖励) |
+| GET /api/invite/rewards | 获取邀请记录列表(含注册状态) |
+| GET /api/invite/extra-quota | 获取可用额外爬虫次数 |
+| GET /api/invite/page/{code}?channel=windows | 邀请落地页数据(公开,无需登录) |
+| GET /invite/{code}?channel=wechat | 邀请链接直接访问入口(公开,自动计入点击数) |
+| POST /api/invite/click/{code} | 追踪邀请链接被打开(公开) |
+| GET /api/invite/resolve-link?link=xxx | 智能解析邀请链接或邀请码(公开,适用于粘贴文本场景) |
 
 ### 2.6 营业执照上传(可选)
 
@@ -194,29 +277,143 @@ https://app.zhijiayun.com/invite/ABC123
 → 运营审核(通过/驳回)→ 通过后显示"已认证"标识
 ```
 
+**营业执照信息**:药店名称、统一社会信用代码、法人姓名、注册地址、经营范围、有效期等。
+
+**用户侧接口**:
+
+| 接口 | 说明 |
+|------|------|
+| POST /api/business-license/upload | 上传营业执照(JSON方式,传图片URL) |
+| POST /api/business-license/upload-file | 上传营业执照(文件方式,multipart) |
+| GET /api/business-license/info | 查询我的营业执照信息及审核状态 |
+
 ### 2.7 优惠券系统
 
 | 功能 | 说明 |
 |------|------|
-| 优惠券模板 | 运营创建,设置面值、总量、有效期、等级门槛 |
-| 发放 | 运营批量发放给指定药店 |
+| 发放 | 运营指定已有优惠券,批量发放给指定用户 |
 | 使用 | 爬虫消耗时优先使用优惠券 |
 | 类型 | CRAWLER(爬虫次数券) |
 
-### 2.8 运营管理后台
+**用户侧接口**:
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/coupon/my?status=0 | 获取我的优惠券列表(可按状态筛选) |
+| GET /api/coupon/my/available?type=CRAWLER | 获取我当前可用的优惠券列表 |
+
+### 2.8 文件上传
+
+| 接口 | 说明 |
+|------|------|
+| POST /api/upload | 上传单个文件(multipart/form-data),返回文件访问路径 |
+
+**说明**:支持营业执照图片、用户头像等文件上传,文件存储在服务器 `/opt/uploads` 目录。
+
+### 2.9 支付与会员购买系统
+
+| 功能 | 接口 | 说明 |
+|------|------|------|
+| 支付方案列表 | GET /api/payment/plans | 获取可用会员套餐(公开) |
+| 创建支付订单 | POST /api/payment/order/create | 选择套餐+支付渠道创建订单 |
+| 查询订单 | GET /api/payment/order/{orderNo} | 查询单个订单状态 |
+| 我的订单 | GET /api/payment/orders | 查询用户所有订单 |
+| 模拟支付 | POST /api/payment/order/{orderNo}/pay | 开发测试用模拟支付(当前仅支持此模式) |
+| 微信回调 | POST /api/payment/callback/wechat | 微信支付异步通知(公开,当前仅模拟模式) |
+| 支付宝回调 | POST /api/payment/callback/alipay | 支付宝异步通知(公开,当前仅模拟模式) |
+
+**支付渠道**:微信支付(Native扫码)、支付宝(当面付/扫码)
+
+> **⚠️ 当前状态**:生产环境微信/支付宝回调接口为占位实现(含TODO),当前仅模拟支付模式可用。上线前需接入微信支付SDK和支付宝SDK完成验签与解密。
+
+**支付方案(初始套餐)**:
+
+| 方案编码 | 方案名称 | 会员等级 | 价格 | 原价 | 有效天数 |
+|----------|----------|----------|------|------|----------|
+| MONTHLY_PLUS | 普通会员·月卡 | PLUS | ¥9.90 | ¥19.90 | 30天 |
+| YEARLY_PLUS | 普通会员·年卡 | PLUS | ¥99.00 | ¥199.90 | 365天 |
+| MONTHLY_PRO | 高级会员·月卡 | PRO | ¥29.90 | ¥59.90 | 30天 |
+| YEARLY_PRO | 高级会员·年卡 | PRO | ¥299.90 | ¥599.90 | 365天 |
+| ULTRA | 超级会员·连锁专享 | ULTRA | - | - | 连锁专用(敬请期待) |
+
+**运营管理**:
+
+| 接口 | 说明 |
+|------|------|
+| POST /api/admin/payment/plans | 创建支付方案 |
+| PUT /api/admin/payment/plans/{id} | 更新支付方案 |
+| PUT /api/admin/payment/plans/{id}/status | 上架/下架方案 |
+
+### 2.10 搜索历史与关注列表
+
+#### 搜索历史
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/search/history | 获取用户搜索历史 |
+| DELETE /api/search/history/{id} | 删除指定搜索记录 |
+| DELETE /api/search/history | 清空搜索历史 |
+
+#### 关注/收藏列表
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/watchlist | 获取关注列表 |
+| POST /api/watchlist | 添加关注药品 |
+| DELETE /api/watchlist/{id} | 取消关注 |
+| GET /api/watchlist/stats | 获取关注统计 |
+
+**关注信息**:药品名称、规格、厂家、关注时最低价、最低价平台、上次查询价格、价格变动、最后检查时间。
+
+### 2.11 运营管理后台
 
 | 功能 | 接口 |
 |------|------|
 | 邀请配置管理 | GET/PUT /api/admin/invite/config |
+| 邀请配置列表 | GET /api/admin/invite/configs |
 | 手动发放爬虫次数 | POST /api/admin/crawler/grant |
 | 查看发放记录 | GET /api/admin/crawler/grants |
 | 查看邀请记录 | GET /api/admin/invite/rewards |
 | 用户等级调整 | PUT /api/level/user/{userId} |
+| 会员发放/调整 | POST /api/admin/membership/grant |
+| 会员记录查询 | GET /api/admin/memberships |
 | 优惠券发放 | POST /api/coupon/issue |
+| 支付方案管理 | POST/PUT /api/admin/payment/plans |
 | 平台分布统计 | GET /api/admin/crawler/user-detail |
 | 营业执照审核 | GET/POST /api/admin/license/review |
-| 等级配置版本管理 | POST/GET /api/admin/user-level-config/* |
-| 操作审计日志 | GET /api/admin/audit-log/* |
+| 用户列表管理 | GET /api/admin/users |
+| 用户角色修改 | PUT /api/admin/users/{userId}/role(仅SUPER_ADMIN) |
+| 体验配额配置 | GET/POST/PUT/DELETE /api/admin/trial-quota/*(详见下方) |
+| 操作审计日志 | GET /api/admin/audit-log/*(详见下方) |
+| 邀请转化统计 | GET /api/admin/invite/conversion-stats?userId=1 |
+
+### 2.12 操作审计日志系统
+
+系统对所有关键操作进行审计记录,支持追溯:
+
+- **记录维度**:操作人、操作时间、IP地址、设备信息、操作模块、操作类型、目标对象、操作前后数据快照、耗时
+- **敏感操作标记**:禁用用户、审核操作、配置变更等标记为敏感操作
+- **操作模块覆盖**:用户管理、平台配置、账号绑定、营业执照、邀请系统、优惠券、体验配额、爬虫配额、系统管理
+
+**运营侧接口**:
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/admin/audit-log | 分页查询审计日志(支持按模块、操作类型、操作人、时间范围、是否敏感筛选) |
+| GET /api/admin/audit-log/sensitive | 查询敏感操作日志 |
+| GET /api/admin/audit-log/user/{userId} | 查询指定用户的操作历史 |
+| GET /api/admin/audit-log/target/{type}/{id} | 查询指定目标对象的操作历史 |
+| GET /api/admin/audit-log/stats | 按模块统计操作次数 |
+
+**体验配额配置接口**:
+
+| 接口 | 说明 |
+|------|------|
+| GET /api/admin/trial-quota/configs | 查询所有体验配额配置 |
+| POST /api/admin/trial-quota/config | 创建体验配额配置 |
+| PUT /api/admin/trial-quota/config/{id} | 更新体验配额配置 |
+| DELETE /api/admin/trial-quota/config/{id} | 删除体验配额配置 |
+| GET /api/trial-quota/active | 用户端查询当前生效的体验配额配置 |
 
 ---
 
@@ -227,7 +424,8 @@ https://app.zhijiayun.com/invite/ABC123
 ```
 药店采购员打开App → 选择登录方式
 ├── 短信登录:输入手机号 → 获取验证码 → 输入验证码 → 登录/注册
-│   └── 如有邀请码,自动建立邀请关系
+│   └── 如有邀请码,自动建立邀请关系并发放会员奖励
+├── 密码登录:输入手机号/昵称 + 密码 → 验证通过 → 登录
 └── 微信登录:扫码 → 授权 → 绑定手机号 → 登录
 ```
 
@@ -240,13 +438,14 @@ https://app.zhijiayun.com/invite/ABC123
 → 系统逐个平台爬取价格(消耗配额)
 → 展示比价结果表格(按价格从低到高排序)
 → 点击"去采购"跳转到对应平台购买页面
+→ 可关注药品,持续追踪价格变动
 ```
 
 ### 3.3 爬虫消耗流程
 
 ```
 用户发起比价请求
-→ 校验日/月配额(等级基础 + 额外配额)
+→ 校验日/月/年配额(会员权益 + 额外配额)
 → 配额充足 → 扣减额外配额(快过期优先)→ 记录日志(含平台信息)
 → 配额不足 → 尝试使用优惠券补充 → 仍不足则拒绝
 ```
@@ -256,8 +455,19 @@ https://app.zhijiayun.com/invite/ABC123
 ```
 药店A获取邀请链接 → 复制链接分享到微信群/朋友圈
 → 药店B浏览器打开 → 展示落地页(A的药店名称 + 奖励说明 + 下载链接)
-→ B下载注册 → 携带邀请码 → 建立邀请关系 → 发放奖励给A
-→ A查看统计:"已邀请X家药店,Y家注册 +Z次"
+→ B下载注册 → 携带邀请码 → 建立邀请关系 → 发放奖励
+  ├── 邀请人A:获赠1个月普通会员
+  └── 被邀请人B:获赠2个月普通会员(注册1个月 + 邀请码1个月)
+→ A查看统计:"已邀请X家药店,Y家注册,累计获得Y个月会员"
+```
+
+### 3.5 会员购买流程
+
+```
+用户查看会员套餐 → 选择方案(月卡/年卡)
+→ 选择支付渠道(微信/支付宝)→ 创建订单 → 生成二维码
+→ 用户扫码支付 → 第三方回调通知 → 订单状态更新为已支付
+→ 自动发放对应会员等级 → 配额立即生效
 ```
 
 ---
@@ -340,7 +550,8 @@ https://app.zhijiayun.com/invite/ABC123
 1. **平台扩展性**:代码支持34个平台,MVP首期启用3个,后续通过运营配置逐步开放
 2. **营业执照可选**:用户可以自愿上传,非强制要求,审核后显示认证标识
 3. **体验配额可配**:运营可以动态调整体验天数、查询次数等参数
-4. **会员体系迭代**:当前简化实现,后续根据运营需求迭代优化
+4. **会员体系灵活**:会员权益配置表支持运营动态调整各等级配额加成
+5. **支付方案可配**:运营可创建/上架/下架会员套餐方案
 
 ### 5.2 平台配置策略
 
@@ -371,17 +582,20 @@ https://app.zhijiayun.com/invite/ABC123
 | 每月关注品种数 | 199种 | 体验期每月可关注品种数 |
 
 **配置类型**:
-- **默认配置**:适用于所有新注册用户
-- **体验券配置**:运营批量生成体验券,用户输入券码核销
-- **活动配置**:特定活动期间自动应用
+- **默认配置**:适用于所有新注册用户(通过运营后台 CRUD 管理)
 
-### 5.4 等级配置版本管理
+### 5.4 会员权益配置
 
-**版本化管理能力**:
-- 运营可以创建新的等级配置版本(草稿)
-- 激活新版本后,新用户自动使用最新版本
-- 老用户由运营决定迁移到哪个版本
-- 支持灰度发布和A/B测试
+**会员权益配置表**支持运营动态调整各等级权益加成:
+
+| 权益类型 | 说明 |
+|----------|------|
+| DAILY_QUOTA | 每日爬虫配额加成 |
+| MONTHLY_QUOTA | 每月爬虫配额加成 |
+| YEARLY_QUOTA | 每年爬虫配额加成 |
+| MAX_CONCURRENT | 最大并发数加成 |
+
+运营可通过数据库管理界面调整各会员等级的权益数值,修改后立即生效。
 
 ---
 
@@ -390,8 +604,8 @@ https://app.zhijiayun.com/invite/ABC123
 | 维度 | MVP要求 | 生产环境目标 |
 |------|---------|-------------|
 | 性能 | 接口响应 < 500ms(P95),单次比价 < 3s | 接口响应 < 200ms(P95),单次比价 < 1s |
-| 安全 | JWT 认证、参数校验、AES-256加密 | 增加接口限流、SQL注入防护、慢请求告警 |
-| 可用性 | 单体部署,HikariCP 连接池 | Redis 缓存、集群部署、负载均衡 |
+| 安全 | JWT 认证、三级RBAC、参数校验、AES-256加密 | 增加接口限流、SQL注入防护、慢请求告警 |
+| 可用性 | 单体部署,HikariCP 连接池,Redis缓存 | 集群部署、负载均衡 |
 | 扩展性 | Maven 多模块架构 | 微服务拆分、事件驱动架构 |
 | 兼容性 | JDK 17+,MySQL 8.0+,Windows 10/11 | 容器化部署、云原生支持 |
 | 数据准确性 | 价格误差 < 5% | 价格误差 < 1%,实时同步 |
@@ -405,17 +619,25 @@ https://app.zhijiayun.com/invite/ABC123
 
 **核心功能**:
 - [x] **Windows独立桌面应用**(.exe格式,内置WebView2浏览器引擎)
-- [x] 用户认证(短信验证码登录、微信扫码登录)
+- [x] 用户认证(短信验证码登录、密码登录、微信扫码登录)
+- [x] 密码管理(设置/修改密码、修改手机号)
 - [x] 营业执照上传(可选,审核后显示认证标识)
 - [x] B2B平台账号绑定(药师帮/药帮忙/1药城,密码AES-256加密存储)
-- [x] 聚合比价功能(并行查询已绑平台,价格升序展示
+- [x] 药品搜索比价功能(消耗配额查询,返回各平台报价
 - [x] 跳转原平台购买(WebView内嵌跳转,不依赖系统浏览器)
-- [x] 邀请裂变系统(多渠道落地页、URL Scheme自动填入邀请码)
+- [x] 搜索历史记录管理
+- [x] 药品关注/收藏列表(价格变动追踪)
+- [x] 会员体系(PLUS/PRO/ULTRA三档,权益驱动配额)
+- [x] 爬虫配额管理(日/月/年三周期 + 额外配额 + 优惠券)
+- [x] 邀请裂变系统(多渠道落地页、URL Scheme自动填入邀请码、会员时长奖励)
+- [x] 支付系统(模拟支付、支付方案管理;微信/支付宝回调为占位实现,待接入SDK)
 - [x] 体验配额管理(运营可配置天数、查询次数、关注品种数)
-- [x] 运营管理后台(配置管理、执照审核、数据统计)
+- [x] 优惠券系统(批量发放、配额消耗优先使用)
+- [x] 运营管理后台(配置管理、执照审核、数据统计、会员管理、支付方案管理)
+- [x] 用户管理(三级RBAC角色模型、用户列表、角色赋权)
 - [x] 平台配置管理(34个平台预留,MVP启用3个,运营动态配置)
-- [x] 用户等级配置版本管理(支持版本化、灰度发布)
 - [x] 操作审计日志系统(记录所有关键操作,支持追溯)
+- [x] 网关层请求日志与限流(基于Redis的IP+路径限流)
 
 **多端扩展准备**:
 - [x] 统一账号体系设计(支持多端登录)
@@ -431,10 +653,10 @@ https://app.zhijiayun.com/invite/ABC123
 #### Phase 2:性能优化与多端扩展(上线后1-3个月)
 
 **技术优化**:
-- [ ] 接入真实Redis缓存(当前为Mock)
-- [ ] 接口限流(Redis + Sentinel)
-- [ ] 慢请求监控与告警
 - [ ] 数据库读写分离
+- [ ] SQL慢查询优化与索引调优
+- [ ] 接口限流细化(按用户维度限流)
+- [ ] 慢请求监控与告警
 
 **多端扩展**:
 - [ ] **微信小程序开发**(轻量级查询工具)
@@ -445,7 +667,6 @@ https://app.zhijiayun.com/invite/ABC123
 #### Phase 3:功能增强与渠道完善(上线后3-6个月)
 
 **功能增强**:
-- [ ] 支付系统集成(购买会员)
 - [ ] 国家集采数据爬取(增强权威性)
 - [ ] 价格趋势图表(近30天价格走势)
 - [ ] 供应商评分系统
@@ -500,6 +721,7 @@ https://app.zhijiayun.com/invite/ABC123
 | 日均比价次数 | 5000+ | 所有用户的比价操作总和 |
 | 邀请转化率 | 15%+ | 被邀请人注册比例 |
 | 用户留存率(7日) | 40%+ | 注册后7天内再次使用 |
+| 付费转化率 | 5%+ | 购买会员套餐的比例 |
 
 ### 8.2 长期目标(1年内)
 
@@ -508,7 +730,7 @@ https://app.zhijiayun.com/invite/ABC123
 | 注册用户数 | 50万+ | 覆盖全国主要药店 |
 | 日活跃用户(DAU) | 20万+ | **核心目标** |
 | 日均比价次数 | 100万+ | 高频使用场景 |
-| 付费转化率 | 10%+ | 升级为VIP/SVIP的比例 |
+| 付费转化率 | 10%+ | 升级为PRO/ULTRA的比例 |
 | 平均采购成本降低 | 15%+ | 相比单平台采购 |
 
 ---

+ 225 - 73
docs/02-架构文档.md

@@ -4,23 +4,24 @@
 > 本项目为**全新项目,尚未上线**,当前处于 **MVP(最小可行产品)阶段**。  
 > 本文档描述的是 MVP 版本的架构设计,后续将根据业务发展和技术演进持续优化。
 >
-> 版本:v1.0-MVP | 更新日期:2026-06-16 | 状态:开发中
+> 版本:v1.1-MVP | 更新日期:2026-06-25 | 状态:开发中
 
 ---
 
 ## 1. 技术栈
 
-| 层次 | 技术 | 版本 | MVP说明 |
+| 层次 | 技术 | 版本 | MVP状态 |
 |------|------|------|---------|
 | 语言 | Java | 17 | ✅ |
 | 框架 | Spring Boot | 3.5.14 | ✅ |
 | ORM | MyBatis-Plus | 3.5.16 | ✅ |
-| 数据库 | MySQL | 8.0+ | ✅ |
-| 缓存 | Redis (Lettuce) | 6.0+ | ⚠️ Mock实现,生产环境需接入真实Redis |
+| 数据库 | MySQL | 8.0+(驱动 9.3.0) | ✅ |
+| 缓存 | Redis (Lettuce) | 6.0+ | ✅ 已接入真实Redis |
 | 认证 | JWT (jjwt) | 0.12.6 | ✅ |
+| 安全 | Spring Security + BouncyCastle | 1.84 | ✅ |
 | 短信 | 阿里云 SMS | 4.5.1 | ✅ |
+| 二维码 | Google ZXing | 3.5.3 | ✅ |
 | 工具库 | Hutool | 5.8.44 | ✅ |
-| 安全 | Spring Security + BouncyCastle | 1.84 | ✅ |
 | 构建 | Maven | 3.8+ | ✅ |
 
 ---
@@ -31,11 +32,12 @@
 
 ```
 zhijiayun-pharmacy (父工程)
-├── zhijiayun-common    # 公共模块:Result、ErrorCode、BusinessException
-├── zhijiayun-user      # 用户模块:认证、等级、优惠券、爬虫配额
-├── zhijiayun-invite    # 邀请模块:邀请码、裂变、运营配置
-├── zhijiayun-payment   # 支付模块(预留)
-└── zhijiayun-gateway   # 网关模块:聚合启动、请求日志、限流
+├── zhijiayun-common    # 公共模块:Result、ErrorCode、BusinessException、TraceIdGenerator、ValidateUtil
+├── zhijiayun-user      # 用户模块:认证、会员、优惠券、爬虫配额、搜索、关注、营业执照、审计日志
+├── zhijiayun-invite    # 邀请模块:邀请码、裂变、会员奖励、运营配置
+├── zhijiayun-payment   # 支付模块:微信/支付宝支付、订单管理、套餐方案、二维码生成
+├── zhijiayun-gateway   # 网关模块:聚合启动、请求日志、Redis限流、全局异常处理
+└── zhijiayun-test      # 测试模块:集成测试页面、端到端测试
 ```
 
 ### 2.2 模块依赖关系
@@ -45,7 +47,7 @@ zhijiayun-gateway (聚合启动入口)
 ├── zhijiayun-user
 │   └── zhijiayun-common
 ├── zhijiayun-invite
-│   ├── zhijiayun-user (依赖 CrawlerQuotaGrantService)
+│   ├── zhijiayun-user (依赖 CrawlerQuotaGrantService、MembershipService)
 │   └── zhijiayun-common
 └── zhijiayun-payment
     └── zhijiayun-common
@@ -57,42 +59,82 @@ zhijiayun-gateway (聚合启动入口)
 com.xuekairui
 ├── common/                    # zhijiayun-common
 │   ├── Result.java            # 统一响应封装
-│   ├── ErrorCode.java         # 错误码枚举
-│   └── BusinessException.java # 业务异常
+│   ├── ErrorCode.java         # 错误码枚举(用户/验证码/微信/Token/支付/等级/优惠券/爬虫/邀请/搜索/关注)
+│   ├── BusinessException.java # 业务异常
+│   ├── TraceIdGenerator.java  # 链路追踪ID生成器
+│   └── ValidateUtil.java      # 参数校验工具(IP提取等)
 ├── user/                      # zhijiayun-user
 │   ├── config/                # 配置类
 │   │   ├── AliyunSmsProperties.java
 │   │   ├── MyBatisPlusMetaHandler.java
 │   │   ├── RedisConfig.java
-│   │   └── SecurityConfig.java
-│   ├── controller/            # 控制器
-│   │   ├── AuthController.java
-│   │   ├── CrawlerController.java
-│   │   ├── CouponController.java
-│   │   └── UserLevelController.java
-│   ├── dto/                   # 数据传输对象
-│   ├── entity/                # 数据库实体
-│   ├── mapper/                # MyBatis-Plus Mapper
+│   │   └── SecurityConfig.java  # Spring Security + RBAC
+│   ├── controller/            # 控制器(18个)
+│   │   ├── AuthController.java           # 认证(短信/密码/微信登录、Token刷新、用户信息)
+│   │   ├── CrawlerController.java        # 爬虫配额状态、消耗、日志
+│   │   ├── CouponController.java         # 优惠券发放、查询
+│   │   ├── UserLevelController.java      # 等级列表、用户等级设置
+│   │   ├── MembershipController.java     # 会员信息查询、权益查询
+│   │   ├── DrugSearchController.java     # 药品搜索比价、搜索建议
+│   │   ├── SearchHistoryController.java  # 搜索历史管理
+│   │   ├── WatchlistController.java      # 关注/收藏列表管理
+│   │   ├── PlatformAccountController.java # B2B平台账号绑定
+│   │   ├── CrawlerPlatformConfigController.java # 平台配置查询
+│   │   ├── BusinessLicenseController.java # 营业执照上传
+│   │   ├── FileUploadController.java     # 文件上传
+│   │   ├── TrialQuotaConfigController.java # 体验配额查询
+│   │   ├── AdminUserController.java      # 用户管理(SUPER_ADMIN)
+│   │   ├── AdminMembershipController.java # 会员发放管理
+│   │   ├── AdminLicenseController.java   # 营业执照审核
+│   │   ├── AdminTrialQuotaConfigController.java # 体验配额配置管理
+│   │   └── OperationAuditLogController.java # 审计日志查询
+│   ├── dto/                   # 数据传输对象(30+)
+│   ├── entity/                # 数据库实体(16个)
+│   ├── enums/                 # 枚举(MembershipLevel、UserRole、OperationType)
+│   ├── event/                 # 事件(UserRegisteredEvent)
+│   ├── mapper/                # MyBatis-Plus Mapper(15个)
 │   ├── security/              # JWT过滤器
-│   ├── service/               # 业务逻辑
+│   ├── service/               # 业务逻辑(18个服务)
 │   └── util/                  # 工具类
 ├── invite/                    # zhijiayun-invite
 │   ├── controller/
-│   │   ├── InviteController.java
-│   │   └── AdminController.java
+│   │   ├── InviteController.java  # 邀请码、统计、记录、落地页
+│   │   └── AdminController.java   # 邀请配置、爬虫发放、记录查看
 │   ├── dto/
 │   ├── entity/
+│   ├── listener/              # UserRegisteredListener(事件驱动)
 │   ├── mapper/
 │   └── service/
+│       ├── InviteService.java       # 邀请核心服务
+│       └── InviteConfigService.java # 邀请配置服务
+│
+├── payment/                   # zhijiayun-payment
+│   ├── config/                # PaymentConfig、PaymentProperties
+│   ├── controller/
+│   │   ├── PaymentController.java        # 支付方案、创建订单、查询订单
+│   │   ├── PaymentCallbackController.java # 微信/支付宝回调
+│   │   └── AdminPaymentController.java   # 支付方案管理
+│   ├── dto/
+│   ├── entity/                # PaymentOrder、PaymentPlan
+│   ├── enums/                 # PaymentChannel、PaymentStatus
+│   ├── mapper/
+│   └── service/
+│       ├── PaymentOrderService.java  # 订单管理
+│       ├── PaymentPlanService.java   # 方案管理
+│       └── QRCodeService.java        # 二维码生成
 └── gateway/                   # zhijiayun-gateway
     ├── Application.java       # 启动类
-    ├── handler/
-    │   └── GlobalExceptionHandler.java
-    ├── filter/                # 请求日志、限流过滤器
-    └── config/
+    ├── config/
+    │   ├── GatewayConfig.java       # 网关配置
+    │   └── GatewayProperties.java   # 网关属性(日志、限流规则)
+    ├── filter/
+    │   ├── RequestLoggingFilter.java # 请求日志+慢请求检测
+    │   └── RateLimitFilter.java      # Redis限流过滤器
+    └── handler/
+        └── GlobalExceptionHandler.java # 全局异常处理
 ```
 
 ---
@@ -103,27 +145,44 @@ com.xuekairui
 
 | 表名 | 说明 | 核心字段 |
 |------|------|----------|
-| t_user | 用户表 | phone, nickname, avatar, wechat_open_id, level_id, status |
-| t_user_level | 等级配置表 | level_name, level_code, crawler_quota, monthly_quota, max_concurrent |
-| t_coupon | 优惠券模板表 | name, type, value, total_count, valid_type, min_level_code |
-| t_user_coupon | 用户优惠券表 | user_id, coupon_id, status, expire_time, source |
-| t_crawler_usage_log | 爬虫使用记录表 | user_id, usage_date, usage_count, source(QUOTA/COUPON) |
-| t_crawler_quota_grant | 爬虫配额发放表 | user_id, grant_type(INVITE/PURCHASE/ADMIN), quota_count, used_count, expire_time |
-| t_invite_config | 邀请奖励配置表 | reward_crawler_count, app_download_url, landing_title, 多渠道字段 |
-| t_invite_code | 邀请码表 | user_id, code, max_uses, used_count, click_count |
+| t_user | 用户表 | phone, nickname, password, wechat_open_id, role(USER/ADMIN/SUPER_ADMIN), membership_level(PLUS/PRO/ULTRA), pharmacy_name, login_source |
+| t_user_membership | 用户会员记录表 | user_id, level(PLUS/PRO/ULTRA), source(ADMIN_GRANT/RECHARGE/SYSTEM), effective_from, effective_to, status |
+| t_membership_benefit | 会员权益配置表 | level, benefit_type(DAILY/MONTHLY/YEARLY_QUOTA, MAX_CONCURRENT), benefit_value |
+| t_coupon | 优惠券模板表 | name, type(CRAWLER), value, total_count, valid_type, min_level_code |
+| t_user_coupon | 用户优惠券表 | user_id, coupon_id, status(0未使用/1已使用/2已过期), expire_time, source |
+| t_crawler_usage_log | 爬虫使用记录表 | user_id, usage_date, usage_count, platform, source(QUOTA/COUPON) |
+| t_crawler_quota_grant | 爬虫配额发放表 | user_id, grant_type(INVITE/PURCHASE/ADMIN), quota_count, used_count, expire_time, status(1有效/2用完/3过期) |
+| t_invite_config | 邀请奖励配置表 | reward_type(MEMBERSHIP/CRAWLER), reward_months, max_daily_reward, 多渠道字段, landing_title |
+| t_invite_code | 邀请码表 | user_id, code, max_uses, used_count, click_count, expire_time |
 | t_invite_relation | 邀请关系表 | inviter_id, invitee_id, invite_code_id, registered, reward_granted |
+| t_payment_plan | 支付方案表 | plan_code, plan_name, membership_level, price(DECIMAL), original_price(DECIMAL), duration_days, status |
+| t_payment_order | 支付订单表 | order_no, user_id, plan_id, amount(DECIMAL), channel(WECHAT/ALIPAY), status(PENDING/PAID/EXPIRED/REFUNDED), qr_code_url, trade_no |
+| t_platform_account | B2B平台账号绑定表 | user_id, platform_code, account_encrypted, password_encrypted, verify_status(PENDING/SUCCESS/FAILED) |
+| t_platform_config | 平台配置表 | platform_code, platform_name, enabled, query_cost, priority, official_url, binding_instructions |
+| t_business_license | 营业执照表 | user_id, license_image_url, credit_code, pharmacy_name, review_status(PENDING/APPROVED/REJECTED) |
+| t_trial_quota_config | 体验配额配置表 | config_name, trial_days, daily_query_limit, monthly_watchlist_limit, config_type(DEFAULT/COUPON/ACTIVITY) |
+| t_operation_audit_log | 操作审计日志表 | operator_id, operator_role, module, operation_type, target_type, target_id, before_data, after_data, result, is_sensitive |
+| t_search_history | 搜索历史表 | user_id, keyword, platform, result_count, search_time |
+| t_watchlist | 关注/收藏表 | user_id, drug_name, spec, manufacturer, min_price, min_price_platform, last_price, price_change |
 
 ### 3.2 ER关系
 
 ```
 t_user ──1:N──> t_invite_code (每个用户一个邀请码)
 t_user ──1:N──> t_invite_relation (作为邀请人)
+t_user ──1:N──> t_user_membership (会员记录)
 t_user ──1:N──> t_crawler_quota_grant (配额发放)
 t_user ──1:N──> t_crawler_usage_log (使用记录)
-t_user ──N:1──> t_user_level (等级)
 t_user ──1:N──> t_user_coupon (优惠券)
+t_user ──1:N──> t_platform_account (平台账号绑定)
+t_user ──1:1──> t_business_license (营业执照)
+t_user ──1:N──> t_search_history (搜索历史)
+t_user ──1:N──> t_watchlist (关注列表)
+t_user ──1:N──> t_payment_order (支付订单)
 t_coupon ──1:N──> t_user_coupon (模板-实例)
 t_invite_code ──1:N──> t_invite_relation (邀请码-关系)
+t_membership_benefit ──N:1──> MembershipLevel (等级-权益)
+t_payment_plan ──1:N──> t_payment_order (方案-订单)
 ```
 
 ---
@@ -133,35 +192,72 @@ t_invite_code ──1:N──> t_invite_relation (邀请码-关系)
 ### 4.1 认证流程
 
 ```
-客户端 → POST /api/auth/sms/login → 验证成功
+客户端 → POST /api/auth/sms/login 或 /api/auth/password/login → 验证成功
 → 生成 JWT (Access Token 24h + Refresh Token 7d)
 → 后续请求携带 Authorization: Bearer <token>
 → JwtAuthenticationFilter 校验 → 放行/拒绝
 ```
 
-### 4.2 接口权限
+### 4.2 三级RBAC权限模型
+
+| 角色 | 权限范围 | 说明 |
+|------|----------|------|
+| USER | 用户侧接口 | 比价、搜索、关注、邀请、支付等 |
+| ADMIN | 运营后台接口 | 用户管理、配置管理、审核、发放等 |
+| SUPER_ADMIN | 全部权限 | 包括角色赋权(/api/admin/users/**) |
+
+### 4.3 接口权限
 
 | 类型 | 路径 | 说明 |
 |------|------|------|
-| 公开 | /api/auth/sms/**, /api/auth/wechat/** | 登录注册 |
-| 公开 | /api/invite/page/**, /api/invite/click/** | 邀请落地页 |
+| 公开 | /api/auth/sms/send, /api/auth/sms/login | 短信验证码 |
+| 公开 | /api/auth/password/login | 密码登录 |
+| 公开 | /api/auth/refresh | 刷新Token |
+| 公开 | /api/auth/wechat/** | 微信登录相关 |
+| 公开 | /api/invite/page/**, /api/invite/click/**, /api/invite/{code} | 邀请落地页 |
+| 公开 | /api/invite/resolve-link, /api/invite/validate | 邀请码解析/验证 |
+| 公开 | /api/platform-config/** | 平台配置查询 |
+| 公开 | /api/trial-quota/** | 体验配额查询 |
+| 公开 | /api/search/suggestions | 搜索建议 |
+| 公开 | /api/payment/plans | 支付方案列表 |
+| 公开 | /api/payment/callback/** | 支付回调 |
 | 公开 | /api/level/list, /api/level/{id} | 等级列表 |
-| 认证 | /api/auth/user/** | 用户信息 |
-| 认证 | /api/crawler/** | 爬虫操作 |
-| 认证 | /api/invite/** | 邀请操作 |
-| 认证 | /api/coupon/** | 优惠券 |
-| 管理 | /api/admin/** | 运营管理 |
+| 认证 | /api/auth/user/**, /api/auth/password/set, /api/auth/phone/change | 用户信息 |
+| 认证 | /api/crawler/**, /api/search/query | 爬虫/搜索 |
+| 认证 | /api/invite/code, /api/invite/stats, /api/invite/rewards | 邀请操作 |
+| 认证 | /api/coupon/**, /api/membership/** | 优惠券/会员 |
+| 认证 | /api/platform-account/**, /api/watchlist/** | 账号绑定/关注 |
+| 认证 | /api/payment/order/** | 支付订单 |
+| 管理 | /api/admin/** (ADMIN + SUPER_ADMIN) | 运营管理 |
+| 超管 | /api/admin/users/** (仅SUPER_ADMIN) | 用户角色管理 |
+| 管理 | /api/coupon/issue, /api/level/user/** (ADMIN + SUPER_ADMIN) | 散落管理操作 |
 
-### 4.3 限流策略
+### 4.4 限流策略
 
-基于 Redis 滑动窗口计数器:
+基于 Redis 固定窗口计数器,按客户端IP + 路径前缀限流
 
-| 路径 | 窗口 | 限制 |
-|------|------|------|
-| /api/auth/sms | 60s | 10次 |
-| /api/auth/login | 60s | 20次 |
-| /api/crawler | 60s | 60次 |
-| 其他 | 60s | 100次 |
+| 路径前缀 | 窗口 | 限制 | 说明 |
+|----------|------|------|------|
+| /api/auth/sms | 60s | 10次 | 短信验证码发送 |
+| /api/auth/login | 60s | 20次 | 登录接口 |
+| /api/crawler | 60s | 60次 | 爬虫操作 |
+| 其他 | 60s | 100次 | 默认限制 |
+
+**限流响应**:HTTP 429 + `X-RateLimit-Limit` / `X-RateLimit-Remaining` 响应头
+
+### 4.5 密码安全
+
+- 密码使用 **BCryptPasswordEncoder** 加密存储
+- 支持 手机号 + 密码 / 昵称 + 密码 双模登录
+- 支持设置/修改密码(需验证旧密码)
+- 平台账号密码使用 **AES-256** 加密存储
+
+### 4.6 短信验证码安全
+
+- 开发环境:测试模式(固定验证码 123456,不发送真实短信)
+- 生产环境:阿里云SMS真实发送
+- IP维度限流:1分钟内同IP最多1次,防止短信轰炸
+- 验证码5分钟过期
 
 ---
 
@@ -170,8 +266,9 @@ t_invite_code ──1:N──> t_invite_relation (邀请码-关系)
 ### 5.1 配额计算
 
 ```
-每日总配额 = 等级每日配额 + 额外配额之和
-每月总配额 = 等级每月配额 + 额外配额之和
+每日总配额 = 会员每日权益配额 + 额外配额之和
+每月总配额 = 会员每月权益配额 + 额外配额之和
+每年总配额 = 会员每年权益配额 + 额外配额之和
 
 额外配额 = SUM(所有有效 grant 的 quota_count - used_count)
 ```
@@ -181,7 +278,7 @@ t_invite_code ──1:N──> t_invite_relation (邀请码-关系)
 ```
 1. 优惠券 (COUPON) → 优先使用,避免浪费
 2. 额外配额 (EXTRA) → 快过期优先消耗
-3. 等级基础配额 (QUOTA) → 兜底
+3. 会员权益配额 (QUOTA) → 兜底
 ```
 
 ### 5.3 过期控制
@@ -190,54 +287,106 @@ t_invite_code ──1:N──> t_invite_relation (邀请码-关系)
 - 定时任务每小时扫描标记过期记录(status = 3)
 - 消耗时自动跳过已过期记录
 
+### 5.4 会员权益配置
+
+会员权益通过 `t_membership_benefit` 表配置,支持运营动态调整:
+
+| 会员等级 | 每日配额 | 每月配额 | 每年配额 | 最大并发 |
+|----------|----------|----------|----------|----------|
+| PLUS | 10 | 300 | 3650 | - |
+| PRO | 999 | 29970 | 364635 | 5 |
+| ULTRA | 999 | 29970 | 364635 | 10 |
+
+---
+
+## 6. 支付架构
+
+### 6.1 支付流程
+
+```
+用户选择套餐 → 创建订单(PENDING) → 生成支付二维码
+→ 用户扫码支付 → 第三方异步回调 → 验签
+→ 订单状态更新为PAID → 自动发放会员等级 → 配额生效
+```
+
+### 6.2 支付渠道
+
+| 渠道 | 编码 | 回调地址 | 说明 |
+|------|------|----------|------|
+| 微信支付 | WECHAT | /api/payment/callback/wechat | Native扫码支付(API v3) |
+| 支付宝 | ALIPAY | /api/payment/callback/alipay | 当面付/扫码支付(RSA2验签) |
+
+### 6.3 模拟支付
+
+开发环境(`payment.mock=true`)支持模拟支付:
+- POST /api/payment/order/{orderNo}/pay 直接标记订单为已支付
+- 回调接口接收模拟通知,跳过验签
+
+### 6.4 生产环境接入
+
+生产环境需配置:
+- 微信支付:app-id、mch-id、api-v3-key、private-key-path、serial-no、notify-url
+- 支付宝:app-id、private-key、alipay-public-key、notify-url
+- 回调验签:微信需接入SDK实现RSA验签+解密;支付宝需接入SDK实现RSA2验签
+
 ---
 
-## 6. 模块间通信
+## 7. 模块间通信
 
-### 6.1 直接依赖
+### 7.1 直接依赖
 
 - invite 模块依赖 user 模块的 `CrawlerQuotaGrantService`(配额发放)
+- invite 模块依赖 user 模块的 `MembershipService`(会员时长奖励发放)
 
-### 6.2 事件驱动
+### 7.2 事件驱动
 
-- 用户注册事件 → 检查邀请码 → 建立邀请关系 → 发放奖励
+- 用户注册事件(`UserRegisteredEvent`)→ `UserRegisteredListener` → 检查邀请码 → 建立邀请关系 → 发放会员奖励
 - 避免循环依赖,保持模块边界清晰
 
+### 7.3 支付与会员联动
+
+- 支付成功回调 → `PaymentOrderService.processPaidCallback` → 调用 `MembershipService` 发放对应会员等级
+
 ---
 
-## 7. MVP 架构特点与后续演进
+## 8. MVP 架构特点与后续演进
 
-### 7.1 MVP 架构特点 ✅
+### 8.1 MVP 架构特点 ✅
 
 **当前采用单体架构,模块边界清晰:**
 
-1. **Maven 多模块**:common、user、invite、payment、gateway 五个模块
+1. **Maven 多模块**:common、user、invite、payment、gateway、test 六个模块
 2. **直接依赖**:模块间通过接口调用,无消息队列
 3. **单数据库**:所有表在同一 MySQL 实例
-4. **Mock Redis**:测试环境使用 Mock,生产需接入真实 Redis
-5. **同步处理**:邀请奖励发放为同步操作
+4. **真实 Redis**:已接入 Redis 缓存(Lettuce 连接池),用于限流、验证码存储
+5. **同步处理**:邀请奖励发放为同步操作(事件驱动但同步执行)
+6. **RBAC 安全**:三级角色模型 + Spring Security 细粒度权限控制
+7. **网关层增强**:请求日志 + 慢请求检测 + Redis IP限流
 
 **优势:**
 - 部署简单,运维成本低
 - 开发效率高,调试方便
 - 适合 MVP 阶段快速验证
 
-### 7.2 后续架构演进规划 🚀
+### 8.2 后续架构演进规划 🚀
 
 #### Phase 2:性能优化
-- [ ] 接入真实 Redis 缓存
-- [ ] 数据库连接池优化(HikariCP 调优)
-- [ ] SQL 慢查询优化
+- [ ] 数据库读写分离
+- [ ] SQL 慢查询优化与索引调优
+- [ ] 接口限流细化(按用户维度)
+- [ ] Redis 缓存策略优化
 
 #### Phase 3:异步化改造
 - [ ] 引入消息队列(RabbitMQ/Kafka)
 - [ ] 邀请奖励发放改为异步处理
 - [ ] 爬虫日志记录异步化
+- [ ] 支付回调异步处理
 
 #### Phase 4:微服务拆分
 - [ ] 用户服务独立部署(zhijiayun-user)
 - [ ] 配额服务独立部署(新建 zhijiayun-quota)
 - [ ] 邀请服务独立部署(zhijiayun-invite)
+- [ ] 支付服务独立部署(zhijiayun-payment)
 - [ ] API Gateway 替代当前 gateway 模块
 
 #### Phase 5:云原生
@@ -248,13 +397,16 @@ t_invite_code ──1:N──> t_invite_relation (邀请码-关系)
 
 ---
 
-## 8. 扩展点
+## 9. 扩展点
 
 | 扩展方向 | 预留设计 | MVP状态 |
 |----------|----------|---------|
-| 支付模块 | zhijiayun-payment 已预留,待实现微信/支付宝支付 | ⏳ 未实现 |
+| 支付渠道 | PaymentChannel 枚举可扩展新渠道 | ✅ 已实现微信/支付宝 |
 | 新渠道 | t_invite_config 支持 dingtalk/feishu 等渠道字段 | ✅ 已支持 |
 | 新配额来源 | t_crawler_quota_grant.grant_type 可扩展新类型 | ✅ 已支持 |
+| 会员权益类型 | t_membership_benefit.benefit_type 可扩展 | ✅ 已支持 |
+| 平台扩展 | t_platform_config 支持动态启用/禁用平台 | ✅ 已支持 |
+| 操作审计 | OperationType 枚举覆盖9大模块 | ✅ 已实现 |
 | 分布式 | 当前单体,模块边界清晰,可拆分为微服务 | ⏳ 后续演进 |
 
 ---

+ 158 - 55
docs/06-验收流程与标准.md

@@ -4,7 +4,7 @@
 > 本项目为**全新项目,尚未上线**,当前处于 **MVP(最小可行产品)阶段**。  
 > 本文档定义的是 MVP 版本的验收标准,正式上线前需全部通过。
 >
-> 版本:v1.0-MVP | 更新日期:2026-06-16 | 状态:开发中
+> 版本:v1.1-MVP | 更新日期:2026-06-25 | 状态:开发中
 
 ---
 
@@ -36,64 +36,149 @@
 
 | 编号 | 验收项 | 通过标准 | 优先级 |
 |------|--------|----------|--------|
-| FA-01 | 短信验证码发送 | 60s内收到6位验证码,格式正确 | P0 |
+| FA-01 | 短信验证码发送 | 60s内收到6位验证码,格式正确;1分钟内同IP限1次 | P0 |
 | FA-02 | 短信验证码登录 | 正确验证码登录成功,返回Token | P0 |
-| FA-03 | 新用户自动注册 | 首次登录自动创建用户,分配默认等级 | P0 |
-| FA-04 | 邀请码注册 | 携带邀请码注册,自动建立邀请关系 | P1 |
+| FA-03 | 新用户自动注册 | 首次登录自动创建用户,分配默认权益 | P0 |
+| FA-04 | 邀请码注册 | 携带邀请码注册,自动建立邀请关系并发放会员奖励 | P1 |
 | FA-05 | 微信扫码登录 | 扫码后获取授权码,登录成功 | P0 |
 | FA-06 | 微信绑定手机号 | 首次微信登录引导绑定手机号 | P1 |
 | FA-07 | Token刷新 | Refresh Token有效期内可刷新 | P1 |
 | FA-08 | 验证码过期 | 5分钟后验证码失效 | P1 |
+| FA-09 | 密码登录(手机号) | 手机号+正确密码登录成功,返回Token | P0 |
+| FA-10 | 密码登录(昵称) | 昵称+正确密码登录成功,返回Token | P1 |
+| FA-11 | 设置密码 | 已登录用户设置密码成功 | P1 |
+| FA-12 | 修改密码 | 验证旧密码后修改密码成功 | P1 |
+| FA-13 | 修改手机号 | 验证码验证通过后修改绑定手机号 | P1 |
+| FA-14 | 登录来源追踪 | 记录登录设备、IP、来源(WINDOWS/MINIAPP等) | P2 |
 
-### 2.2 用户等级模块
+### 2.2 会员体系模块
 
 | 编号 | 验收项 | 通过标准 | 优先级 |
 |------|--------|----------|--------|
-| FA-09 | 等级列表 | 返回3个等级(NORMAL/VIP/SVIP) | P0 |
-| FA-10 | 用户等级查询 | 新用户默认NORMAL等级 | P0 |
-| FA-11 | 管理员设置等级 | 设置后用户配额立即更新 | P1 |
+| FA-20 | 会员信息查询 | 返回当前会员等级、有效期、权益信息 | P0 |
+| FA-21 | 会员权益查询 | 返回指定等级的权益配额列表 | P0 |
+| FA-22 | 运营发放会员 | 指定用户发放会员,立即生效 | P0 |
+| FA-23 | 会员记录查询 | 分页查询会员记录,支持按用户/等级筛选 | P1 |
+| FA-24 | 会员到期失效 | 到期后会员状态自动变为过期 | P1 |
+| FA-25 | 会员权益配置 | 数据库中3个等级的权益配置正确(PLUS/PRO/ULTRA) | P0 |
 
 ### 2.3 爬虫配额模块
 
 | 编号 | 验收项 | 通过标准 | 优先级 |
 |------|--------|----------|--------|
-| FA-12 | 配额状态查询 | 返回日/月配额、额外配额明细 | P0 |
-| FA-13 | 等级配额消耗 | 消耗次数正确扣减 | P0 |
-| FA-14 | 额外配额消耗 | 快过期优先消耗 | P0 |
-| FA-15 | 优惠券消耗 | useCouponFirst=true时优先使用优惠券 | P1 |
-| FA-16 | 日配额上限 | 超出日配额返回拒绝 | P0 |
-| FA-17 | 月配额上限 | 超出月配额返回拒绝 | P0 |
-| FA-18 | 配额明细展示 | extraQuotaBreakdown显示各来源明细 | P1 |
-| FA-19 | 过期配额清理 | 定时任务每小时标记过期记录 | P2 |
+| FA-30 | 配额状态查询 | 返回日/月/年配额、额外配额明细 | P0 |
+| FA-31 | 会员权益配额消耗 | 消耗次数正确扣减 | P0 |
+| FA-32 | 额外配额消耗 | 快过期优先消耗 | P0 |
+| FA-33 | 优惠券消耗 | useCouponFirst=true时优先使用优惠券 | P1 |
+| FA-34 | 日配额上限 | 超出日配额返回拒绝 | P0 |
+| FA-35 | 月配额上限 | 超出月配额返回拒绝 | P0 |
+| FA-36 | 年配额上限 | 超出年配额返回拒绝 | P1 |
+| FA-37 | 配额明细展示 | extraQuotaBreakdown显示各来源明细 | P1 |
+| FA-38 | 过期配额清理 | 定时任务每小时标记过期记录 | P2 |
+| FA-39 | 爬虫使用日志 | 记录含平台信息、消耗来源 | P1 |
+
+### 2.4 药品搜索比价模块
 
-### 2.4 邀请系统模块
+| 编号 | 验收项 | 通过标准 | 优先级 |
+|------|--------|----------|--------|
+| FA-40 | 药品搜索比价 | 输入药品名返回各平台报价,消耗1次配额 | P0 |
+| FA-41 | 搜索建议 | 返回热门药品列表(公开,无需登录) | P1 |
+| FA-42 | 搜索结果排序 | 按价格从低到高排序展示 | P0 |
+| FA-43 | 平台筛选 | 支持多选平台查询 | P1 |
+
+### 2.5 平台账号绑定模块
+
+| 编号 | 验收项 | 通过标准 | 优先级 |
+|------|--------|----------|--------|
+| FA-45 | 绑定平台账号 | 绑定成功,密码AES-256加密存储 | P0 |
+| FA-46 | 解绑平台账号 | 解绑成功,记录删除 | P0 |
+| FA-47 | 查询已绑账号 | 返回已绑定平台列表(不返回密码) | P0 |
+| FA-48 | 验证平台账号 | 验证账号有效性,记录验证状态 | P1 |
+| FA-49 | 平台配置查询 | 返回启用平台列表(公开) | P1 |
+
+### 2.6 搜索历史与关注列表模块
+
+| 编号 | 验收项 | 通过标准 | 优先级 |
+|------|--------|----------|--------|
+| FA-50 | 搜索历史记录 | 每次搜索自动记录历史 | P1 |
+| FA-52 | 删除搜索记录 | 删除指定记录/清空全部 | P1 |
+| FA-53 | 添加关注 | 关注药品成功,记录价格信息 | P1 |
+| FA-54 | 关注列表查询 | 返回关注列表含价格变动信息 | P1 |
+| FA-55 | 取消关注 | 取消关注成功 | P1 |
+| FA-56 | 关注统计 | 返回关注总数等统计信息 | P2 |
+
+### 2.7 邀请系统模块
+
+| 编号 | 验收项 | 通过标准 | 优先级 |
+|------|--------|----------|--------|
+| FA-60 | 获取邀请码 | 自动生成唯一邀请码和链接 | P0 |
+| FA-61 | 邀请统计 | 显示邀请人数、注册人数、累计奖励、摘要文本 | P0 |
+| FA-62 | 邀请记录 | 每条记录含被邀请人昵称、注册状态、奖励次数 | P0 |
+| FA-63 | 落地页(公开) | 展示邀请人信息、下载链接、应用名称 | P0 |
+| FA-64 | 点击追踪 | 每次打开链接click_count+1 | P1 |
+| FA-65 | 多渠道落地页 | channel=windows/wechat/miniapp/dingtalk/feishu 正确返回 | P1 |
+| FA-66 | 邀请会员奖励 | 被邀请人注册后,邀请人获赠1个月普通会员 | P0 |
+| FA-67 | 被邀请人奖励 | 被邀请人注册获赠2个月普通会员 | P0 |
+| FA-68 | 邀请码解析 | 支持链接/邀请码双模式智能解析 | P1 |
+| FA-69 | URL Scheme自动填入 | zhijiayun://invite?code=XXX 正确生成 | P1 |
+| FA-70 | 落地页占位符 | {inviter}和{pharmacy}动态替换正确 | P2 |
+
+### 2.8 支付系统模块
+
+| 编号 | 验收项 | 通过标准 | 优先级 |
+|------|--------|----------|--------|
+| FA-75 | 支付方案列表 | 返回上架的会员套餐列表(公开) | P0 |
+| FA-76 | 创建支付订单 | 选择套餐+渠道创建订单,生成二维码 | P0 |
+| FA-77 | 查询订单 | 按订单号查询订单状态 | P0 |
+| FA-78 | 我的订单 | 返回用户所有订单列表 | P1 |
+| FA-79 | 模拟支付 | 开发环境模拟支付成功,订单状态更新 | P1 |
+| FA-80 | 微信回调 | 接收微信支付回调,验签通过后更新订单 | P0 |
+| FA-81 | 支付宝回调 | 接收支付宝回调,验签通过后更新订单 | P0 |
+| FA-82 | 支付成功发放会员 | 支付成功后自动发放对应会员等级 | P0 |
+| FA-83 | 订单过期 | 超过15分钟未支付订单自动过期 | P1 |
+| FA-84 | 创建支付方案(运营) | 运营创建套餐方案成功 | P1 |
+| FA-85 | 上下架方案(运营) | 运营上架/下架方案成功 | P1 |
+
+### 2.9 运营管理模块
+
+| 编号 | 验收项 | 通过标准 | 优先级 |
+|------|--------|----------|--------|
+| FA-90 | 邀请配置读取 | 返回当前激活配置 | P0 |
+| FA-91 | 邀请配置更新 | 更新奖励月数、下载链接、落地页配置 | P0 |
+| FA-92 | 手动发放爬虫次数 | 指定用户发放成功,配额立即生效 | P0 |
+| FA-93 | 发放记录查询 | 按用户/类型筛选,返回完整记录 | P1 |
+| FA-94 | 用户列表查询 | 分页查询用户,支持按手机号/角色筛选 | P0 |
+| FA-95 | 用户角色修改 | SUPER_ADMIN修改用户角色成功 | P0 |
+| FA-96 | 体验配额配置 | 创建/更新体验配额配置成功 | P1 |
+| FA-97 | 营业执照审核 | 运营审核通过/驳回成功 | P1 |
+
+### 2.10 优惠券模块
 
 | 编号 | 验收项 | 通过标准 | 优先级 |
 |------|--------|----------|--------|
-| FA-20 | 获取邀请码 | 自动生成唯一邀请码和链接 | P0 |
-| FA-21 | 邀请统计 | 显示邀请人数、注册人数、累计奖励、摘要文本 | P0 |
-| FA-22 | 邀请记录 | 每条记录含被邀请人昵称、注册状态、奖励次数 | P0 |
-| FA-23 | 落地页(公开) | 展示邀请人信息、下载链接、应用名称 | P0 |
-| FA-24 | 点击追踪 | 每次打开链接click_count+1 | P1 |
-| FA-25 | 多渠道落地页 | channel=app/wechat/miniapp/dingtalk/feishu 正确返回 | P1 |
-| FA-26 | 邀请奖励发放 | 被邀请人注册后,邀请人获得配置次数 | P0 |
+| FA-100 | 优惠券发放 | 批量发放给指定用户,设置有效期 | P1 |
+| FA-101 | 我的优惠券 | 按状态筛选(未使用/已使用/已过期) | P1 |
+| FA-102 | 可用优惠券 | 返回未过期且未使用的优惠券 | P1 |
 
-### 2.5 运营管理模块
+### 2.11 操作审计日志模块
 
 | 编号 | 验收项 | 通过标准 | 优先级 |
 |------|--------|----------|--------|
-| FA-27 | 邀请配置读取 | 返回当前激活配置 | P0 |
-| FA-28 | 邀请配置更新 | 更新奖励次数、下载链接、落地页配置 | P0 |
-| FA-29 | 手动发放爬虫次数 | 指定用户发放成功,配额立即生效 | P0 |
-| FA-30 | 发放记录查询 | 按用户/类型筛选,返回完整记录 | P1 |
+| FA-105 | 审计日志记录 | 关键操作自动记录审计日志 | P0 |
+| FA-106 | 审计日志查询 | 按模块/操作类型/时间范围筛选查询 | P1 |
+| FA-107 | 敏感操作标记 | 禁用用户、审核操作等标记为敏感 | P1 |
+| FA-108 | 操作前后数据快照 | 记录操作前后的数据变化 | P2 |
 
-### 2.6 优惠券模块
+### 2.12 RBAC权限控制模块
 
 | 编号 | 验收项 | 通过标准 | 优先级 |
 |------|--------|----------|--------|
-| FA-31 | 优惠券发放 | 批量发放给指定用户,设置有效期 | P1 |
-| FA-32 | 我的优惠券 | 按状态筛选(未使用/已使用/已过期) | P1 |
-| FA-33 | 可用优惠券 | 返回未过期且未使用的优惠券 | P1 |
+| FA-110 | USER访问用户接口 | 普通用户可访问用户侧接口 | P0 |
+| FA-111 | USER访问管理接口 | 普通用户访问管理接口返回403 | P0 |
+| FA-112 | ADMIN访问管理接口 | 运营可访问运营后台接口 | P0 |
+| FA-113 | ADMIN访问超管接口 | 运营访问超管接口返回403 | P0 |
+| FA-114 | SUPER_ADMIN全权限 | 超管可访问所有接口 | P0 |
+| FA-115 | 未认证访问保护接口 | 未认证访问保护接口返回401 | P0 |
 
 ---
 
@@ -101,13 +186,15 @@
 
 | 编号 | 指标 | 标准 | 测试方法 |
 |------|------|------|----------|
-| PA-01 | 接口响应时间(P95) | < 200ms | 压测工具 |
-| PA-02 | 接口响应时间(P99) | < 500ms | 压测工具 |
+| PA-01 | 接口响应时间(P95) | < 500ms | 压测工具 |
+| PA-02 | 接口响应时间(P99) | < 1000ms | 压测工具 |
 | PA-03 | 登录接口 QPS | > 100 | 并发压测 |
 | PA-04 | 爬虫状态接口 QPS | > 200 | 并发压测 |
-| PA-05 | 数据库连接池 | 正常负载不超过 maximum-pool-size 80% | 监控 |
-| PA-06 | Redis 连接池 | 正常负载不超过 max-active 80% | 监控 |
-| PA-07 | 慢请求比例 | < 1%(超过1000ms) | 日志分析 |
+| PA-05 | 搜索比价接口响应 | < 3s(P95) | 压测工具 |
+| PA-06 | 数据库连接池 | 正常负载不超过 maximum-pool-size 80% | 监控 |
+| PA-07 | Redis 连接池 | 正常负载不超过 max-active 80% | 监控 |
+| PA-08 | 慢请求比例 | < 1%(超过1000ms) | 日志分析 |
+| PA-09 | 限流响应 | 超出阈值返回429 + 限流响应头 | 压测验证 |
 
 ---
 
@@ -116,13 +203,18 @@
 | 编号 | 验收项 | 通过标准 | 检查方法 |
 |------|--------|----------|----------|
 | SA-01 | JWT认证 | 未认证接口返回401 | 接口测试 |
-| SA-02 | 白名单配置 | 仅公开接口可无Token访问 | 安全扫描 |
-| SA-03 | 限流策略 | 超出阈值返回429 | 压测验证 |
-| SA-04 | SQL注入 | 无SQL注入漏洞 | 安全扫描工具 |
-| SA-05 | 参数校验 | 非法参数返回400 | 接口测试 |
-| SA-06 | 敏感信息 | 密码加密存储,AK/SK不硬编码 | 代码审查 |
-| SA-07 | CVE漏洞 | 无高危CVE漏洞(BouncyCastle已升级至1.84) | 依赖扫描 |
-| SA-08 | HTTPS | 生产环境启用HTTPS | 配置检查 |
+| SA-02 | RBAC权限控制 | 不同角色访问越权接口返回403 | 接口测试 |
+| SA-03 | 白名单配置 | 仅公开接口可无Token访问 | 安全扫描 |
+| SA-04 | 限流策略 | 超出阈值返回429 | 压测验证 |
+| SA-05 | SQL注入 | 无SQL注入漏洞 | 安全扫描工具 |
+| SA-06 | 参数校验 | 非法参数返回400 | 接口测试 |
+| SA-07 | 密码加密 | 用户密码BCrypt加密存储,平台密码AES-256加密 | 代码审查 |
+| SA-08 | AK/SK安全 | 阿里云AK/SK、微信/支付宝密钥通过环境变量注入 | 代码审查 |
+| SA-09 | CVE漏洞 | 无高危CVE漏洞(BouncyCastle已升级至1.84) | 依赖扫描 |
+| SA-10 | HTTPS | 生产环境启用HTTPS | 配置检查 |
+| SA-11 | 短信限流 | IP维度1分钟1次限制有效 | 接口测试 |
+| SA-12 | 支付回调验签 | 生产环境微信/支付宝回调验签通过 | 代码审查 |
+| SA-13 | CORS配置 | 跨域配置正确,不暴露敏感头 | 配置检查 |
 
 ---
 
@@ -133,9 +225,12 @@
 | DA-01 | 应用启动 | 启动无报错,健康检查返回UP |
 | DA-02 | 数据库连接 | 正常读写,建表脚本执行成功 |
 | DA-03 | Redis连接 | 缓存、限流正常工作 |
-| DA-04 | 日志输出 | 日志格式正确,包含请求路径和耗时 |
-| DA-05 | 环境变量 | AK/SK通过环境变量注入 |
-| DA-06 | 初始数据 | 3个默认等级数据已插入 |
+| DA-04 | 日志输出 | 日志格式正确,包含请求路径、耗时、traceId |
+| DA-05 | 环境变量 | AK/SK、支付密钥通过环境变量注入 |
+| DA-06 | 初始数据 | 3个平台配置、会员权益配置、体验配额配置、邀请配置、支付方案已初始化 |
+| DA-07 | SQL初始化 | schema.sql 幂等执行(CREATE IF NOT EXISTS + ON DUPLICATE KEY UPDATE) |
+| DA-08 | 短信测试模式 | 开发环境短信测试模式启用(固定验证码123456) |
+| DA-09 | 支付模拟模式 | 开发环境支付模拟模式启用(mock=true) |
 
 ---
 
@@ -159,20 +254,28 @@
 
 ### 6.3 功能完整性
 
-- [ ] 用户认证(短信+微信)
-- [ ] 用户等级体系
-- [ ] 爬虫配额管理(统一配额)
-- [ ] 邀请裂变系统(多渠道)
+- [ ] 用户认证(短信+密码+微信)
+- [ ] 会员体系(PLUS/PRO/ULTRA + 权益配置)
+- [ ] 爬虫配额管理(日/月/年三周期 + 额外配额 + 优惠券)
+- [ ] 药品搜索比价(搜索+建议+消耗配额)
+- [ ] 平台账号绑定(绑定/解绑/验证/加密存储)
+- [ ] 搜索历史与关注列表
+- [ ] 邀请裂变系统(多渠道 + 会员时长奖励 + 智能解析)
+- [ ] 支付系统(微信/支付宝 + 套餐管理 + 模拟支付)
 - [ ] 优惠券系统
-- [ ] 运营管理后台
+- [ ] 运营管理后台(用户管理 + 配置管理 + 审核管理)
+- [ ] 操作审计日志系统
+- [ ] RBAC三级角色权限控制
+- [ ] 网关层请求日志与Redis限流
 
 ### 6.4 运维就绪
 
-- [ ] 配置文件模板完整
+- [ ] 配置文件模板完整(.env.example)
 - [ ] 环境变量说明清晰
 - [ ] 健康检查接口可用
-- [ ] 日志输出规范
+- [ ] 日志输出规范(含traceId、请求路径、耗时)
 - [ ] 数据库备份策略
+- [ ] Docker Compose 部署配置可用
 
 ---
 

+ 16 - 0
pom.xml

@@ -50,6 +50,8 @@
         <aliyun-sms.version>4.5.1</aliyun-sms.version>
         <mybatis-plus.version>3.5.16</mybatis-plus.version>
         <bouncycastle.version>1.84</bouncycastle.version>
+        <alipay-sdk.version>4.40.865.ALL</alipay-sdk.version>
+        <wxjava.version>4.7.0</wxjava.version>
     </properties>
 
     <!-- 依赖版本管理(子模块按需引入,无需写版本号) -->
@@ -153,6 +155,20 @@
                 <artifactId>bcprov-jdk18on</artifactId>
                 <version>${bouncycastle.version}</version>
             </dependency>
+
+            <!-- 支付宝SDK -->
+            <dependency>
+                <groupId>com.alipay.sdk</groupId>
+                <artifactId>alipay-sdk-java</artifactId>
+                <version>${alipay-sdk.version}</version>
+            </dependency>
+
+            <!-- 微信支付SDK (WxJava) -->
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>weixin-java-pay</artifactId>
+                <version>${wxjava.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 

+ 2 - 0
zhijiayun-common/src/main/java/com/xuekairui/common/ErrorCode.java

@@ -89,6 +89,8 @@ public enum ErrorCode {
     INVITE_ALREADY_USED(3004, "该邀请码已被使用"),
     INVITE_RELATION_EXISTS(3005, "邀请关系已存在"),
     INVITE_REWARD_FAILED(3006, "邀请奖励发放失败"),
+    INVITE_BIND_EXPIRED(3007, "注册时间已超过有效期,无法补填邀请码"),
+    INVITE_LIMIT_REACHED(3008, "邀请人数已达上限"),
 
     // ========== 搜索模块 4301-4399 ==========
     SEARCH_QUOTA_EXCEEDED(4301, "搜索次数已用完"),

binární
zhijiayun-gateway/src/main/resources/apiclient_cert.p12


+ 32 - 15
zhijiayun-gateway/src/main/resources/application-dev.yml

@@ -43,26 +43,43 @@ sms:
   test-mode: true
   test-code: "123456"
 
-# 邀请码落地页基础地址(开发本地)
+# 邀请码配置
 invite:
   base-url: http://localhost:8001
+  # 补填邀请码有效窗口(天),注册后30天内可补填
+  bind-window-days: 30
 
-# 支付模块(开发环境启用模拟支付)
+# 支付模块(开发环境启用真实支付,手动确认按钮用于测试回调
 payment:
-  mock: true
+  mock: false
   order-expire-minutes: 15
   qrcode:
     width: 200
     height: 200
-  # 开发环境无需配置真实支付参数,mock=true 时不会使用
-  # wechat:
-  #   app-id: wx_sandbox_app_id
-  #   mch-id: sandbox_mch_id
-  #   api-v3-key: sandbox_api_v3_key
-  #   notify-url: http://localhost:8002/api/payment/callback/wechat
-  # alipay:
-  #   app-id: sandbox_app_id
-  #   private-key: sandbox_private_key
-  #   alipay-public-key: sandbox_public_key
-  #   notify-url: http://localhost:8002/api/payment/callback/alipay
-  #   server-url: https://openapi-sandbox.dl.alipaydev.com/gateway.do
+  # 支付宝(开发/测试环境):将沙箱证书文件复制到 certs/dev/alipay/ 目录下
+  alipay:
+    app-id: ${ALIPAY_APP_ID:2021006169624093}
+    notify-url: ${ALIPAY_NOTIFY_URL:https://priceapi.kailin.com.cn/api/payment/callback/alipay}
+    server-url: ${ALIPAY_SERVER_URL:https://openapi.alipay.com/gateway.do}
+    # 公钥模式(优先):配置了 alipay-public-key 即走公钥模式
+    private-key: ${ALIPAY_PRIVATE_KEY:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUw9VUpXoHxL6ycUd/3kaVBtZtkHFDyoCJVbS6QCaPA6Z+kTRGX7z81TWZbSUDZelI5TK6bIA+Duh4smr7vscdNadp65cQP+vIVRYyAbQKPWtwK4rfF07gMcW+CXI3KmJsKt8zZtqyD5cXs0IcGLEhF8EmUTVDSiWGSqHiXl7ZU94dFxDuY0h0iafxPghpDUCrCJTtkfQUsC82aWknRVvPqsyw/0G+V3xupkHbJlAlNwj0ITSt+SvgARZlHtcjVKgLDaO2aE/ydqGyp64nLb253mCc0Rt9tI75rgZiVK7JeMcVKUy8B1gpOZxkvfwxwVzpKi5a7Y3tlth+37vqhFXXAgMBAAECggEAKk11wFg91y+vOnbT6hx+3anQF9/XFXvkCSmB9uR/EnPrSFpqDwjyaMMINtS+jbTRHZVvU2u/G+aJXKj/mw6o+Dllnc0Ma+aBWbo4UYgCdlb40jDPkJjEKSR+Iof1HpLC4hde2PMKhU/Bm0+yIOM0m0ByAD9fcf8HpKRcXE6RkiLELaxMjEL14JHaKSa0l5F4sGbscChFVHgr0KVI0HYtWeV5bm3877QOD86DnlAkeKomXHeNhw+jVbHVzHkV9S0bhfizHloXIbFYaNAFpyBAIypyqhyPlfpvENRjn7y05X1QgobtOX7CBOMWiGbAd9F4RhtDpCvF3546iKMz6N3NQQKBgQDDPKpVii/OU6PD1miT4a3imWCRBY4N7sqQVZDxWge8TlJab7oPALVpK16+D/sH2o33cagsW60yFLwLwXUxbVBlsGznpZEBPGFmQ44jGFyiDwkHmiIh4Wck0PbeJcbQAg9AwSHKjJspZ8kUdOcBHMIainnf0qyOJu8mO7ls4fJiyQKBgQDDEI1XGsxPty7S3XIWGRCRv64aVmUUn5W5Icc/111pp+E4O1gNyvjCDWxIVEQipD5fuAUtC55PG+qa5kY2y8nlOxXB9Uu4fO5a/8U4UZtTHgXJQqcmMWblXHvAtsCwGTLTF4o7WiwCv2QtAYPja1kKxNegBuptygFlWfvhvfujnwKBgE3zDLb2YzvnnPz1v+87LafJQQrlwt/BNVMHNsWGAoymYK8G6KxQxh1lWYBc9QcftYrT8Cc1uyLBsIqpBNdlI7JWDEU4j3qE1wZzdm6ArYtuoVZNrc8FJZ9HZykuwAAQsnJSKeGq3dBcRaZLPmftSXo7S4mXnZlr5hPHyLK2S4YxAoGBALQ2X45AuM9Q2pZ/df5Ka89reirk9H/IK+gc99rYWnnZWq67zM75XTbSdd6g00351ZJTDZ0fJcPKc8VlxyqqvbzlCGXn/GvWJ9O0hv1QD6MLkqbsyEX10C1IcyCI9p36ucTh85h/5zcmoSMfSjzaGSpkzx0OJpkFzqJXLujQv2HbAoGAN6JTrmbZzdviJRBGysEzImUKfZh5MFmlc9de1dBAsGXu5n4DIvsESK4QivooSL7sAhoXj19VWxVAl5JLh2uIYVZyJXbWq6CgimYVtMNbUcIdyuhLM+LlUuAHI7z5erehBs7bxVh+4yKO53axXUSfU/hcxvz9g9wmz26HirzmVGo=}
+    alipay-public-key: ${ALIPAY_PUBLIC_KEY:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnVxW1gw1r7o5MhKl0OrKSLvoOeunFcdbwyObFi3D+WlD2y4Mr1uBMtEB9fo1Qqe5aNiIqGPRI+4D3ewmWlDEU3agzMbONDK17t+XRmkEuMoQBVkBoJ5ZbXqv5zX1w3Er0FU/ifbo7gepQDAWPqjZB7iZpkm9gVUVFhDLIudegljlu8qS1SIMx/bLcgMmKe8/Fl9G7ptpLRKqu8uiPXapwNNds5DC5wadGDThrszrv+VMSq9GKf9hIUgSDYKaJx44Cdgn0OsEHW4Sy2y0q3oInTU+lxteYaMmGZ3SDrA6GX3Y4YCI+j6RuZ2HL1LgQoA93381PKry6yPG7Pe264HbMwIDAQAB}
+    # 证书模式(备选):未配置 alipay-public-key 时回退到证书模式,从 certs/dev/alipay/ 读取
+    private-key-path: ${ALIPAY_PRIVATE_KEY_PATH:certs/dev/alipay/appPrivateKey.txt}
+    app-cert-path: ${ALIPAY_APP_CERT_PATH:certs/dev/alipay/appCertPublicKey_2021005188648051.crt}
+    alipay-cert-path: ${ALIPAY_CERT_PATH:certs/dev/alipay/alipayCertPublicKey_RSA2.crt}
+    alipay-root-cert-path: ${ALIPAY_ROOT_CERT_PATH:certs/dev/alipay/alipayRootCert.crt}
+  # 微信支付(开发/测试环境):将测试商户证书文件复制到 certs/dev/wechat/ 目录下
+  wechat:
+    app-id: ${WECHAT_PAY_APP_ID:wxf2f254f9981651dd}
+    mch-id: ${WECHAT_PAY_MCH_ID:1612111355}
+    api-v3-key: ${WECHAT_PAY_API_V3_KEY:iD1xB1mH6zO7pP2wD1rU4aD2nH7oL2lN}
+    # 三份证书文件(从微信支付商户平台下载,放入 certs/dev/wechat/):
+    private-key-path: ${WECHAT_PAY_PRIVATE_KEY_PATH:certs/dev/wechat/apiclient_key.pem}         # 证书密钥pem(商户私钥)
+    private-cert-path: ${WECHAT_PAY_PRIVATE_CERT_PATH:certs/dev/wechat/apiclient_cert.pem}      # 证书pem格式(商户证书)
+    key-path: ${WECHAT_PAY_KEY_PATH:certs/dev/wechat/apiclient_cert.p12}                        # 证书pkcs12格式(密码默认为商户号)
+    serial-no: ${WECHAT_PAY_SERIAL_NO:68E43B250D73977F1DC0E0D0827AF25A8BFB1777}
+    # 微信支付公钥(新商户必须配置,从商户平台 → 账户中心 → API安全 → 申请微信支付公钥)
+    public-key-path: ${WECHAT_PAY_PUBLIC_KEY_PATH:certs/dev/wechat/pub_key.pem}              # 微信支付公钥文件
+    public-key-id: ${WECHAT_PAY_PUBLIC_KEY_ID:}                                              # 微信支付公钥ID
+    notify-url: ${WECHAT_PAY_NOTIFY_URL:https://priceapi.kailin.com.cn/api/payment/callback/wechat}

+ 38 - 3
zhijiayun-gateway/src/main/resources/application-prod.yml

@@ -49,7 +49,7 @@ wechat:
     app-id: ${WECHAT_APP_ID:wxd5f6a9d072f9a9f0}
     app-secret: ${WECHAT_APP_SECRET:67d696a9f0a9d072f9a9f0d5f6a9d072}
     qr-connect:
-      redirect-uri: ${WECHAT_REDIRECT_URI:https://your-domain.com/api/auth/wechat/callback}
+      redirect-uri: ${WECHAT_REDIRECT_URI:https://priceapi.kailin.com.cn/api/auth/wechat/callback}
       scope: snsapi_login
 
 # 阿里云短信:生产部署时通过环境变量覆盖
@@ -95,6 +95,41 @@ gateway:
         limit: 30
         window: 60
 
-# 邀请码落地页基础地址(生产域名)
+# 邀请码配置
 invite:
-  base-url: ${INVITE_BASE_URL:https://your-domain.com}
+  base-url: ${INVITE_BASE_URL:https://priceapi.kailin.com.cn}
+  # 补填邀请码有效窗口(天),注册后30天内可补填
+  bind-window-days: ${INVITE_BIND_WINDOW_DAYS:30}
+
+# 支付模块(生产环境必须设为 false)
+payment:
+  mock: false
+  order-expire-minutes: 15
+  # 支付宝(生产环境):将生产证书文件复制到 certs/prod/alipay/ 或通过环境变量覆盖
+  # 支付宝(开发/测试环境):将沙箱证书文件复制到 certs/dev/alipay/ 目录下
+  alipay:
+    app-id: ${ALIPAY_APP_ID:2021006169624093}
+    notify-url: ${ALIPAY_NOTIFY_URL:https://priceapi.kailin.com.cn/api/payment/callback/alipay}
+    server-url: ${ALIPAY_SERVER_URL:https://openapi.alipay.com/gateway.do}
+    # 公钥模式(优先):配置了 alipay-public-key 即走公钥模式
+    private-key: ${ALIPAY_PRIVATE_KEY:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUw9VUpXoHxL6ycUd/3kaVBtZtkHFDyoCJVbS6QCaPA6Z+kTRGX7z81TWZbSUDZelI5TK6bIA+Duh4smr7vscdNadp65cQP+vIVRYyAbQKPWtwK4rfF07gMcW+CXI3KmJsKt8zZtqyD5cXs0IcGLEhF8EmUTVDSiWGSqHiXl7ZU94dFxDuY0h0iafxPghpDUCrCJTtkfQUsC82aWknRVvPqsyw/0G+V3xupkHbJlAlNwj0ITSt+SvgARZlHtcjVKgLDaO2aE/ydqGyp64nLb253mCc0Rt9tI75rgZiVK7JeMcVKUy8B1gpOZxkvfwxwVzpKi5a7Y3tlth+37vqhFXXAgMBAAECggEAKk11wFg91y+vOnbT6hx+3anQF9/XFXvkCSmB9uR/EnPrSFpqDwjyaMMINtS+jbTRHZVvU2u/G+aJXKj/mw6o+Dllnc0Ma+aBWbo4UYgCdlb40jDPkJjEKSR+Iof1HpLC4hde2PMKhU/Bm0+yIOM0m0ByAD9fcf8HpKRcXE6RkiLELaxMjEL14JHaKSa0l5F4sGbscChFVHgr0KVI0HYtWeV5bm3877QOD86DnlAkeKomXHeNhw+jVbHVzHkV9S0bhfizHloXIbFYaNAFpyBAIypyqhyPlfpvENRjn7y05X1QgobtOX7CBOMWiGbAd9F4RhtDpCvF3546iKMz6N3NQQKBgQDDPKpVii/OU6PD1miT4a3imWCRBY4N7sqQVZDxWge8TlJab7oPALVpK16+D/sH2o33cagsW60yFLwLwXUxbVBlsGznpZEBPGFmQ44jGFyiDwkHmiIh4Wck0PbeJcbQAg9AwSHKjJspZ8kUdOcBHMIainnf0qyOJu8mO7ls4fJiyQKBgQDDEI1XGsxPty7S3XIWGRCRv64aVmUUn5W5Icc/111pp+E4O1gNyvjCDWxIVEQipD5fuAUtC55PG+qa5kY2y8nlOxXB9Uu4fO5a/8U4UZtTHgXJQqcmMWblXHvAtsCwGTLTF4o7WiwCv2QtAYPja1kKxNegBuptygFlWfvhvfujnwKBgE3zDLb2YzvnnPz1v+87LafJQQrlwt/BNVMHNsWGAoymYK8G6KxQxh1lWYBc9QcftYrT8Cc1uyLBsIqpBNdlI7JWDEU4j3qE1wZzdm6ArYtuoVZNrc8FJZ9HZykuwAAQsnJSKeGq3dBcRaZLPmftSXo7S4mXnZlr5hPHyLK2S4YxAoGBALQ2X45AuM9Q2pZ/df5Ka89reirk9H/IK+gc99rYWnnZWq67zM75XTbSdd6g00351ZJTDZ0fJcPKc8VlxyqqvbzlCGXn/GvWJ9O0hv1QD6MLkqbsyEX10C1IcyCI9p36ucTh85h/5zcmoSMfSjzaGSpkzx0OJpkFzqJXLujQv2HbAoGAN6JTrmbZzdviJRBGysEzImUKfZh5MFmlc9de1dBAsGXu5n4DIvsESK4QivooSL7sAhoXj19VWxVAl5JLh2uIYVZyJXbWq6CgimYVtMNbUcIdyuhLM+LlUuAHI7z5erehBs7bxVh+4yKO53axXUSfU/hcxvz9g9wmz26HirzmVGo=}
+    alipay-public-key: ${ALIPAY_PUBLIC_KEY:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnVxW1gw1r7o5MhKl0OrKSLvoOeunFcdbwyObFi3D+WlD2y4Mr1uBMtEB9fo1Qqe5aNiIqGPRI+4D3ewmWlDEU3agzMbONDK17t+XRmkEuMoQBVkBoJ5ZbXqv5zX1w3Er0FU/ifbo7gepQDAWPqjZB7iZpkm9gVUVFhDLIudegljlu8qS1SIMx/bLcgMmKe8/Fl9G7ptpLRKqu8uiPXapwNNds5DC5wadGDThrszrv+VMSq9GKf9hIUgSDYKaJx44Cdgn0OsEHW4Sy2y0q3oInTU+lxteYaMmGZ3SDrA6GX3Y4YCI+j6RuZ2HL1LgQoA93381PKry6yPG7Pe264HbMwIDAQAB}
+    # 证书模式(备选):未配置 alipay-public-key 时回退到证书模式,从 certs/dev/alipay/ 读取
+    private-key-path: ${ALIPAY_PRIVATE_KEY_PATH:certs/prod/alipay/appPrivateKey.txt}
+    app-cert-path: ${ALIPAY_APP_CERT_PATH:certs/prod/alipay/appCertPublicKey_2021005188648051.crt}
+    alipay-cert-path: ${ALIPAY_CERT_PATH:certs/prod/alipay/alipayCertPublicKey_RSA2.crt}
+    alipay-root-cert-path: ${ALIPAY_ROOT_CERT_PATH:certs/prod/alipay/alipayRootCert.crt}
+  # 微信支付(开发/测试环境):将测试商户证书文件复制到 certs/dev/wechat/ 目录下
+  wechat:
+    app-id: ${WECHAT_PAY_APP_ID:wxf2f254f9981651dd}
+    mch-id: ${WECHAT_PAY_MCH_ID:1612111355}
+    api-v3-key: ${WECHAT_PAY_API_V3_KEY:iD1xB1mH6zO7pP2wD1rU4aD2nH7oL2lN}
+    # 三份证书文件(从微信支付商户平台下载,放入 certs/dev/wechat/):
+    private-key-path: ${WECHAT_PAY_PRIVATE_KEY_PATH:certs/prod/wechat/apiclient_key.pem}         # 证书密钥pem(商户私钥)
+    private-cert-path: ${WECHAT_PAY_PRIVATE_CERT_PATH:certs/prod/wechat/apiclient_cert.pem}      # 证书pem格式(商户证书)
+    key-path: ${WECHAT_PAY_KEY_PATH:certs/prod/wechat/apiclient_cert.p12}                        # 证书pkcs12格式(密码默认为商户号)
+    serial-no: ${WECHAT_PAY_SERIAL_NO:68E43B250D73977F1DC0E0D0827AF25A8BFB1777}
+    # 微信支付公钥(新商户必须配置,从商户平台 → 账户中心 → API安全 → 申请微信支付公钥)
+    public-key-path: ${WECHAT_PAY_PUBLIC_KEY_PATH:certs/prod/wechat/pub_key.pem}              # 微信支付公钥文件
+    public-key-id: ${WECHAT_PAY_PUBLIC_KEY_ID:}                                              # 微信支付公钥ID
+    notify-url: ${WECHAT_PAY_NOTIFY_URL:https://priceapi.kailin.com.cn/api/payment/callback/wechat}

+ 47 - 0
zhijiayun-gateway/src/main/resources/db/migration-v2.sql

@@ -0,0 +1,47 @@
+-- ============================================
+-- 智价云(药店端) - 增量迁移脚本
+-- 版本: v2 (基于 git diff 生成)
+-- 日期: 2026-06-26
+-- 说明: 此脚本安全可重复执行,不会删除任何已有数据
+-- ============================================
+
+SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
+USE `zhijiayun_pharmacy`;
+
+-- ============================================
+-- 1. DDL: t_invite_config 新增 max_total_invites 列
+-- ============================================
+ALTER TABLE `t_invite_config`
+    ADD COLUMN IF NOT EXISTS `max_total_invites` INT NOT NULL DEFAULT 30
+    COMMENT '单个账号总邀请上限' AFTER `max_invite_per_day`;
+
+-- ============================================
+-- 2. DML: t_payment_plan 普通会员方案改为免费
+-- ============================================
+INSERT INTO `t_payment_plan` (`plan_code`, `plan_name`, `membership_level`, `price`, `original_price`, `duration_days`, `sort_order`, `status`, `description`)
+VALUES
+    ('MONTHLY_PLUS', '普通会员(免费)',  'PLUS', 0,     NULL,   0,   1, 0, '普通会员免费开放,注册即拥有'),
+    ('YEARLY_PLUS',  '普通会员(免费)',  'PLUS', 0,     NULL,   0,   2, 0, '普通会员免费开放,注册即拥有'),
+    ('MONTHLY_PRO',  '高级会员·月卡',  'PRO',  29.90, 59.90,  30,  3, 1, '每天不限量(最高999次)· 1个月29.9(原价59.9)'),
+    ('YEARLY_PRO',   '高级会员·年卡',  'PRO',  299.90,599.90, 365, 4, 1, '每天不限量(最高999次)· 1年299.9(原价599.9)'),
+    ('ULTRA',        '超级会员·连锁专享','ULTRA',0,     NULL,   0,   5, 0, '连锁专用,多店铺管理,敬请期待')
+ON DUPLICATE KEY UPDATE
+    `plan_name`        = VALUES(`plan_name`),
+    `membership_level` = VALUES(`membership_level`),
+    `price`            = VALUES(`price`),
+    `original_price`   = VALUES(`original_price`),
+    `duration_days`    = VALUES(`duration_days`),
+    `sort_order`       = VALUES(`sort_order`),
+    `status`           = VALUES(`status`),
+    `description`      = VALUES(`description`);
+
+-- ============================================
+-- 3. DML: t_invite_config 增加总邀请上限配置
+-- ============================================
+INSERT INTO `t_invite_config` (`id`, `reward_type`, `reward_months`, `reward_crawler_count`, `max_daily_reward`, `invite_code_expire_days`, `max_invite_per_day`, `max_total_invites`, `landing_title`, `landing_desc`, `app_name`, `status`)
+VALUES
+    (1, 'MEMBERSHIP', 1, 5, 50, 30, 10, 30, '邀请你加入智价云(药店端)', '登录智价云(药店端)平台,聚合比价查低价', '智价云(药店端)', 1)
+ON DUPLICATE KEY UPDATE
+    `reward_type` = 'MEMBERSHIP',
+    `landing_desc` = '登录智价云(药店端)平台,聚合比价查低价',
+    `max_total_invites` = VALUES(`max_total_invites`);

+ 12 - 28
zhijiayun-gateway/src/main/resources/db/schema.sql

@@ -13,27 +13,8 @@ CREATE DATABASE IF NOT EXISTS `zhijiayun_pharmacy`
 USE `zhijiayun_pharmacy`;
 
 -- ============================================
--- 开发阶段:每次启动重建所有表(正式上线前去掉此段)
--- ============================================
-DROP TABLE IF EXISTS `t_payment_order`;
-DROP TABLE IF EXISTS `t_payment_plan`;
-DROP TABLE IF EXISTS `t_watchlist`;
-DROP TABLE IF EXISTS `t_search_history`;
-DROP TABLE IF EXISTS `t_operation_audit_log`;
-DROP TABLE IF EXISTS `t_trial_quota_config`;
-DROP TABLE IF EXISTS `t_business_license`;
-DROP TABLE IF EXISTS `t_platform_account`;
-DROP TABLE IF EXISTS `t_platform_config`;
-DROP TABLE IF EXISTS `t_crawler_quota_grant`;
-DROP TABLE IF EXISTS `t_crawler_usage_log`;
-DROP TABLE IF EXISTS `t_invite_relation`;
-DROP TABLE IF EXISTS `t_invite_code`;
-DROP TABLE IF EXISTS `t_invite_config`;
-DROP TABLE IF EXISTS `t_user_coupon`;
-DROP TABLE IF EXISTS `t_coupon`;
-DROP TABLE IF EXISTS `t_membership_benefit`;
-DROP TABLE IF EXISTS `t_user_membership`;
-DROP TABLE IF EXISTS `t_user`;
+-- 开发阶段注意:生产环境禁止 DROP TABLE,使用 migration-v*.sql 增量迁移
+-- 开发环境如需重建表,手动执行 DROP TABLE 后再运行本脚本
 -- ============================================
 
 -- 用户表
@@ -167,6 +148,7 @@ CREATE TABLE IF NOT EXISTS `t_invite_config` (
     `max_daily_reward`      INT           NOT NULL DEFAULT 50 COMMENT '每日最大奖励次数上限',
     `invite_code_expire_days` INT         NOT NULL DEFAULT 30 COMMENT '邀请码有效天数',
     `max_invite_per_day`    INT           NOT NULL DEFAULT 10 COMMENT '每人每天最大邀请次数',
+    `max_total_invites`     INT           NOT NULL DEFAULT 30 COMMENT '单个账号总邀请上限',
     -- 多渠道配置
     `app_download_url`      VARCHAR(512)  DEFAULT NULL COMMENT 'Windows客户端下载地址',
     `miniapp_path`          VARCHAR(256)  DEFAULT NULL COMMENT '小程序页面路径(如 /pages/download/index)',
@@ -304,11 +286,11 @@ CREATE TABLE IF NOT EXISTS `t_payment_order` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单表';
 
 
--- 初始化支付方案(含原价
+-- 初始化支付方案(普通会员免费,仅高级会员付费
 INSERT INTO `t_payment_plan` (`plan_code`, `plan_name`, `membership_level`, `price`, `original_price`, `duration_days`, `sort_order`, `status`, `description`)
 VALUES
-    ('MONTHLY_PLUS', '普通会员·月卡',  'PLUS', 9.90,  19.90,  30,  1, 1, '每天有效查询10次 · 1个月9.9(原价19.9)'),
-    ('YEARLY_PLUS',  '普通会员·年卡',  'PLUS', 99.00, 199.90, 365, 2, 1, '每天有效查询10次 · 1年99(原价199.9)'),
+    ('MONTHLY_PLUS', '普通会员(免费)',  'PLUS', 0,     NULL,   0,   1, 0, '普通会员免费开放,注册即拥有'),
+    ('YEARLY_PLUS',  '普通会员(免费)',  'PLUS', 0,     NULL,   0,   2, 0, '普通会员免费开放,注册即拥有'),
     ('MONTHLY_PRO',  '高级会员·月卡',  'PRO',  29.90, 59.90,  30,  3, 1, '每天不限量(最高999次)· 1个月29.9(原价59.9)'),
     ('YEARLY_PRO',   '高级会员·年卡',  'PRO',  299.90,599.90, 365, 4, 1, '每天不限量(最高999次)· 1年299.9(原价599.9)'),
     ('ULTRA',        '超级会员·连锁专享','ULTRA',0,     NULL,   0,   5, 0, '连锁专用,多店铺管理,敬请期待')
@@ -319,6 +301,7 @@ ON DUPLICATE KEY UPDATE
     `original_price`   = VALUES(`original_price`),
     `duration_days`    = VALUES(`duration_days`),
     `sort_order`       = VALUES(`sort_order`),
+    `status`           = VALUES(`status`),
     `description`      = VALUES(`description`);
 
 -- ============================================
@@ -528,10 +511,11 @@ ON DUPLICATE KEY UPDATE
     `enabled`                 = VALUES(`enabled`),
     `description`             = VALUES(`description`);
 
--- 初始化邀请配置(奖励类型为赠送会员时长:邀请人送1个月,被邀请人送5次爬虫+2个月普通会员
-INSERT INTO `t_invite_config` (`id`, `reward_type`, `reward_months`, `reward_crawler_count`, `max_daily_reward`, `invite_code_expire_days`, `max_invite_per_day`, `landing_title`, `landing_desc`, `app_name`, `status`)
+-- 初始化邀请配置(邀请人30天高级会员,被邀请人30天高级会员,总邀请上限30人
+INSERT INTO `t_invite_config` (`id`, `reward_type`, `reward_months`, `reward_crawler_count`, `max_daily_reward`, `invite_code_expire_days`, `max_invite_per_day`, `max_total_invites`, `landing_title`, `landing_desc`, `app_name`, `status`)
 VALUES
-    (1, 'MEMBERSHIP', 1, 5, 50, 30, 10, '邀请你加入智价云(药店端)', '登录智价云(药店端)平台,聚合比价查低价', '智价云(药店端)', 1)
+    (1, 'MEMBERSHIP', 1, 5, 50, 30, 10, 30, '邀请你加入智价云(药店端)', '登录智价云(药店端)平台,聚合比价查低价', '智价云(药店端)', 1)
 ON DUPLICATE KEY UPDATE
     `reward_type` = 'MEMBERSHIP',
-    `landing_desc` = '登录智价云(药店端)平台,聚合比价查低价';
+    `landing_desc` = '登录智价云(药店端)平台,聚合比价查低价',
+    `max_total_invites` = VALUES(`max_total_invites`);

+ 10 - 2
zhijiayun-gateway/src/test/resources/test-schema.sql

@@ -137,6 +137,7 @@ CREATE TABLE IF NOT EXISTS t_invite_config (
     max_daily_reward        INT           NOT NULL DEFAULT 50,
     invite_code_expire_days INT           NOT NULL DEFAULT 30,
     max_invite_per_day      INT           NOT NULL DEFAULT 10,
+    max_total_invites       INT           NOT NULL DEFAULT 30,
     -- 多渠道配置
     app_download_url        VARCHAR(512)  DEFAULT NULL,
     miniapp_path            VARCHAR(256)  DEFAULT NULL,
@@ -161,6 +162,12 @@ PREPARE stmt FROM @sql;
 EXECUTE stmt;
 DEALLOCATE PREPARE stmt;
 
+SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_invite_config' AND COLUMN_NAME = 'max_total_invites');
+SET @sql = IF(@col_exists = 0, 'ALTER TABLE t_invite_config ADD COLUMN max_total_invites INT NOT NULL DEFAULT 30 AFTER max_invite_per_day', 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
 -- 邀请码表
 CREATE TABLE IF NOT EXISTS t_invite_code (
     id              BIGINT AUTO_INCREMENT PRIMARY KEY,
@@ -288,9 +295,9 @@ ON DUPLICATE KEY UPDATE
     membership_level = VALUES(membership_level);
 
 -- 初始化邀请配置
-INSERT IGNORE INTO t_invite_config (id, reward_type, reward_months, reward_crawler_count, max_daily_reward, invite_code_expire_days, max_invite_per_day, landing_title, landing_desc, app_name, status)
+INSERT IGNORE INTO t_invite_config (id, reward_type, reward_months, reward_crawler_count, max_daily_reward, invite_code_expire_days, max_invite_per_day, max_total_invites, landing_title, landing_desc, app_name, status)
 VALUES
-    (1, 'MEMBERSHIP', 1, 5, 50, 30, 10, '邀请你加入智价云(药店端)', '登录智价云(药店端)平台,邀请好友各得好礼', '智价云(药店端)', 1)
+    (1, 'MEMBERSHIP', 1, 5, 50, 30, 10, 30, '邀请你加入智价云(药店端)', '登录智价云(药店端)平台,邀请好友各得好礼', '智价云(药店端)', 1)
 ON DUPLICATE KEY UPDATE
     reward_type = VALUES(reward_type),
     reward_months = VALUES(reward_months),
@@ -298,6 +305,7 @@ ON DUPLICATE KEY UPDATE
     max_daily_reward = VALUES(max_daily_reward),
     invite_code_expire_days = VALUES(invite_code_expire_days),
     max_invite_per_day = VALUES(max_invite_per_day),
+    max_total_invites = VALUES(max_total_invites),
     landing_title = VALUES(landing_title),
     landing_desc = VALUES(landing_desc),
     app_name = VALUES(app_name),

+ 14 - 0
zhijiayun-invite/src/main/java/com/xuekairui/invite/controller/InviteController.java

@@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 邀请接口(用户端)
@@ -59,6 +60,19 @@ public class InviteController {
         return Result.success(inviteService.getAvailableExtraQuota(userId));
     }
 
+    /**
+     * 补填邀请码(注册时未填,注册后有效窗口内可补填)
+     * 补填成功后:邀请人获赠1个月高级会员时长
+     * POST /api/invite/bind
+     * Body: {"inviteCode": "A3K7M9P2"} 或 {"inviteCode": "https://app.zhijiayun.com/invite/A3K7M9P2"}
+     */
+    @PostMapping("/bind")
+    public Result<String> bindInviteCode(@RequestBody Map<String, String> body,
+                                         HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        return Result.success(inviteService.bindInviteCode(userId, body.get("inviteCode")));
+    }
+
     // ==========================================
     // 公开接口(无需登录,浏览器访问)
     // ==========================================

+ 5 - 0
zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/InviteConfigRequest.java

@@ -36,6 +36,11 @@ public class InviteConfigRequest {
     @Max(100)
     private Integer maxInvitePerDay;
 
+    /** 单个账号总邀请上限 */
+    @Min(1)
+    @Max(999)
+    private Integer maxTotalInvites;
+
     // ========== 多渠道配置 ==========
 
     /** Windows客户端下载地址 */

+ 4 - 0
zhijiayun-invite/src/main/java/com/xuekairui/invite/entity/InviteConfig.java

@@ -46,6 +46,10 @@ public class InviteConfig {
     @Builder.Default
     private Integer maxInvitePerDay = 10;
 
+    /** 单个账号总邀请上限 */
+    @Builder.Default
+    private Integer maxTotalInvites = 30;
+
     // ========== 多渠道配置 ==========
 
     /** Windows客户端下载地址 */

+ 4 - 0
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteConfigService.java

@@ -34,6 +34,7 @@ public class InviteConfigService {
                     .maxDailyReward(50)
                     .inviteCodeExpireDays(30)
                     .maxInvitePerDay(10)
+                    .maxTotalInvites(30)
                     .landingTitle("邀请你加入智价云(药店端)")
                     .landingDesc("登录智价云(药店端)平台,邀请好友各得好礼")
                     .appName("智价云(药店端)")
@@ -71,6 +72,9 @@ public class InviteConfigService {
         if (request.getMaxInvitePerDay() != null) {
             config.setMaxInvitePerDay(request.getMaxInvitePerDay());
         }
+        if (request.getMaxTotalInvites() != null) {
+            config.setMaxTotalInvites(request.getMaxTotalInvites());
+        }
         // 多渠道配置
         if (request.getAppDownloadUrl() != null) {
             config.setAppDownloadUrl(request.getAppDownloadUrl());

+ 126 - 6
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java

@@ -49,6 +49,10 @@ public class InviteService {
     @Value("${invite.base-url:https://app.zhijiayun.com}")
     private String inviteBaseUrl;
 
+    /** 补填邀请码有效窗口(天),默认30天 */
+    @Value("${invite.bind-window-days:30}")
+    private int bindWindowDays;
+
     // ==========================================
     // 邀请码管理
     // ==========================================
@@ -410,6 +414,17 @@ public class InviteService {
             return;
         }
 
+        // 5.1 检查邀请人总邀请人数上限
+        Long totalInvited = inviteRelationMapper.selectCount(
+                new LambdaQueryWrapper<InviteRelation>()
+                        .eq(InviteRelation::getInviterId, inviterId)
+                        .eq(InviteRelation::getRegistered, 1));
+        int maxTotal = config.getMaxTotalInvites() != null ? config.getMaxTotalInvites() : 30;
+        if (totalInvited >= maxTotal) {
+            log.warn("邀请人{}已达邀请上限{}人", inviterId, maxTotal);
+            return;
+        }
+
         // 6. 创建邀请关系(直接标记为已注册,奖励类型为赠送会员时长)
         InviteRelation relation = InviteRelation.builder()
                 .inviterId(inviterId)
@@ -425,15 +440,120 @@ public class InviteService {
         inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
         inviteCodeMapper.updateById(inviteCode);
 
-        // 8. 给邀请人发放会员奖励(邀请人1个月,被邀请人2个月普通会员)
+        // 8. 给邀请人发放奖励(赠送30天高级会员)
         grantInviteReward(inviterId, inviteeId);
 
-        log.info("邀请成功: 用户 {} 邀请了用户 {},奖励邀请人{}个月会员",
-                inviterId, inviteeId, config.getRewardMonths() != null ? config.getRewardMonths() : 1);
+        log.info("邀请成功: 用户 {} 邀请了用户 {},邀请人获赠30天高级会员", inviterId, inviteeId);
+    }
+
+    /**
+     * 补填邀请码(注册时未填,注册后有效窗口内可补填)
+     * 补填成功后:被邀请人额外获赠1个月普通会员,邀请人获赠1个月同等级会员时长
+     *
+     * @param userId           当前登录用户ID
+     * @param inviteCodeInput  邀请码或邀请链接
+     * @return 成功提示信息
+     */
+    @Transactional
+    public String bindInviteCode(Long userId, String inviteCodeInput) {
+        if (inviteCodeInput == null || inviteCodeInput.isBlank()) {
+            throw new BusinessException(ErrorCode.PARAM_ERROR, "邀请码不能为空");
+        }
+
+        // 1. 提取邀请码(支持纯码或链接)
+        String inviteCodeStr = extractInviteCode(inviteCodeInput.trim());
+        if (inviteCodeStr == null || inviteCodeStr.isBlank()) {
+            throw new BusinessException(ErrorCode.INVITE_CODE_INVALID, "无法识别邀请码,请检查输入");
+        }
+
+        // 2. 查找邀请码
+        InviteCode inviteCode = inviteCodeMapper.selectOne(
+                new LambdaQueryWrapper<InviteCode>()
+                        .eq(InviteCode::getCode, inviteCodeStr));
+        if (inviteCode == null) {
+            throw new BusinessException(ErrorCode.INVITE_CODE_INVALID);
+        }
+
+        // 3. 校验邀请码状态
+        if (inviteCode.getStatus() != 1) {
+            throw new BusinessException(ErrorCode.INVITE_CODE_INVALID, "邀请码已禁用");
+        }
+        if (inviteCode.getExpireTime() != null && inviteCode.getExpireTime().isBefore(LocalDateTime.now())) {
+            throw new BusinessException(ErrorCode.INVITE_CODE_EXPIRED);
+        }
+
+        Long inviterId = inviteCode.getUserId();
+
+        // 4. 不能邀请自己
+        if (inviterId.equals(userId)) {
+            throw new BusinessException(ErrorCode.INVITE_SELF_FORBIDDEN);
+        }
+
+        // 5. 检查是否已被邀请过(每个用户只能被邀请一次)
+        Long existingCount = inviteRelationMapper.selectCount(
+                new LambdaQueryWrapper<InviteRelation>()
+                        .eq(InviteRelation::getInviteeId, userId));
+        if (existingCount > 0) {
+            throw new BusinessException(ErrorCode.INVITE_RELATION_EXISTS, "您已绑定过邀请码,无法重复绑定");
+        }
+
+        // 6. 检查注册时间是否在有效窗口内
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+        }
+        if (user.getCreateTime() == null) {
+            throw new BusinessException(ErrorCode.BUSINESS_ERROR, "用户注册时间缺失,无法补填");
+        }
+        LocalDateTime deadline = user.getCreateTime().plusDays(bindWindowDays);
+        if (LocalDateTime.now().isAfter(deadline)) {
+            throw new BusinessException(ErrorCode.INVITE_BIND_EXPIRED,
+                    String.format("注册已超过%d天,无法补填邀请码", bindWindowDays));
+        }
+
+        // 7. 获取邀请配置
+        InviteConfig config = inviteConfigService.getActiveConfig();
+        if (config.getStatus() != 1) {
+            throw new BusinessException(ErrorCode.BUSINESS_ERROR, "邀请功能已停用");
+        }
+
+        // 7.1 检查邀请人总邀请人数上限
+        Long totalInvited = inviteRelationMapper.selectCount(
+                new LambdaQueryWrapper<InviteRelation>()
+                        .eq(InviteRelation::getInviterId, inviterId)
+                        .eq(InviteRelation::getRegistered, 1));
+        int maxTotal = config.getMaxTotalInvites() != null ? config.getMaxTotalInvites() : 30;
+        if (totalInvited >= maxTotal) {
+            throw new BusinessException(ErrorCode.INVITE_LIMIT_REACHED,
+                    String.format("邀请人已达邀请上限%d人", maxTotal));
+        }
+
+        // 8. 创建邀请关系
+        InviteRelation relation = InviteRelation.builder()
+                .inviterId(inviterId)
+                .inviteeId(userId)
+                .inviteCodeId(inviteCode.getId())
+                .registered(1)
+                .rewardGranted(config.getRewardMonths() != null ? config.getRewardMonths() : 1)
+                .status(1)
+                .build();
+        inviteRelationMapper.insert(relation);
+
+        // 9. 更新邀请码使用次数
+        inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
+        inviteCodeMapper.updateById(inviteCode);
+
+        // 10. 发放奖励:邀请人30天高级会员
+        membershipService.grantInviteRewardMembership(inviterId, userId);
+
+        log.info("补填邀请码成功: 用户{}绑定邀请码{},邀请人{},邀请人获赠30天高级会员",
+                userId, inviteCodeStr, inviterId);
+
+        return "补填邀请码成功!邀请人获赠1个月高级会员时长";
     }
 
     /**
-     * 发放邀请奖励:邀请人获赠1个月普通会员
+     * 发放邀请奖励:邀请人30天高级会员
      */
     private void grantInviteReward(Long inviterId, Long inviteeId) {
         membershipService.grantInviteRewardMembership(inviterId, inviteeId);
@@ -500,7 +620,7 @@ public class InviteService {
         int todayRemaining = Math.max(0, config.getMaxInvitePerDay() - todayInvited.intValue());
 
         // 生成摘要文本(每邀请1家注册 = 邀请人获1个月会员)
-        String summaryText = String.format("已邀请%d家药店,%d家注册,累计获得%d个月会员",
+        String summaryText = String.format("已邀请%d家药店,%d家注册,累计获得%d个月高级会员",
                 totalInvited, registeredCount, registeredCount);
 
         return InviteStatsResponse.builder()
@@ -630,7 +750,7 @@ public class InviteService {
         String shareDescription = config.getLandingDesc() != null
                 && !config.getLandingDesc().isBlank()
                 ? config.getLandingDesc() : "登录智价云(药店端)平台,聚合比价查低价";
-        String rewardDesc = "新用户注册即送1个月普通会员,填写邀请码加送1个月(共2个月)\n邀请人每成功邀请1家药店注册,再送1个月会员时长";
+        String rewardDesc = "邀请人每成功邀请1家药店注册,赠送1个月高级会员时长";
 
         // 复制文本
         String copyText = String.format(

+ 8 - 0
zhijiayun-payment/pom.xml

@@ -48,6 +48,14 @@
             <groupId>com.google.zxing</groupId>
             <artifactId>javase</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>weixin-java-pay</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 205 - 0
zhijiayun-payment/src/main/java/com/xuekairui/payment/config/PaymentConfig.java

@@ -1,12 +1,217 @@
 package com.xuekairui.payment.config;
 
+import com.alipay.api.AlipayClient;
+import com.alipay.api.CertAlipayRequest;
+import com.alipay.api.DefaultAlipayClient;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /**
  * 支付模块配置
+ *
+ * <p>当 payment.mock=false 时,自动注册 AlipayClient 和 WxPayService Bean。
+ * 开发环境(mock=true)不会创建这些 Bean,避免依赖支付平台配置。
  */
+@Slf4j
 @Configuration
 @EnableConfigurationProperties(PaymentProperties.class)
+@RequiredArgsConstructor
 public class PaymentConfig {
+
+    private final PaymentProperties paymentProperties;
+
+    // ======================== 工具方法 ========================
+
+    /**
+     * 从 classpath 或文件系统读取文件内容(字符串)
+     * <p>优先从 classpath 读取(兼容 IDE 和 JAR),找不到则尝试文件系统路径
+     */
+    private String readFileContent(String path) {
+        if (path == null || path.isBlank()) return null;
+
+        // 优先从 classpath 读取
+        ClassPathResource resource = new ClassPathResource(path);
+        if (resource.exists()) {
+            try (InputStream is = resource.getInputStream()) {
+                return new String(is.readAllBytes(), StandardCharsets.UTF_8).trim();
+            } catch (Exception e) {
+                log.warn("从 classpath 读取文件失败: {}, 尝试文件系统", path);
+            }
+        }
+
+        // 回退到文件系统
+        try {
+            return Files.readString(Path.of(path), StandardCharsets.UTF_8).trim();
+        } catch (Exception e) {
+            throw new RuntimeException("无法读取文件: " + path, e);
+        }
+    }
+
+    /**
+     * 解析证书文件路径为文件系统绝对路径
+     * <p>优先从 classpath 解析(IDE 模式返回 target/classes 下的路径),
+     * 找不到则返回原始路径(适用于外部挂载证书的生产环境)
+     */
+    private String resolveFilePath(String path) {
+        if (path == null || path.isBlank()) return path;
+
+        ClassPathResource resource = new ClassPathResource(path);
+        if (resource.exists()) {
+            try {
+                return resource.getFile().getAbsolutePath();
+            } catch (Exception e) {
+                log.warn("从 classpath 解析文件路径失败(可能运行在JAR中): {}, 使用原始路径", path);
+            }
+        }
+
+        return path;
+    }
+
+    // ======================== 支付宝客户端 ========================
+
+    /**
+     * 支付宝客户端(仅 payment.mock=false 时注册)
+     *
+     * <p>支持两种模式,根据配置自动选择(公钥模式优先):
+     * <ul>
+     *   <li>公钥模式(优先):配置 private-key + alipay-public-key</li>
+     *   <li>证书模式(备选):配置 app-cert-path + alipay-cert-path + alipay-root-cert-path + private-key-path</li>
+     * </ul>
+     * 公共配置:app-id、server-url、notify-url
+     */
+    @Bean
+    @ConditionalOnProperty(prefix = "payment", name = "mock", havingValue = "false")
+    public AlipayClient alipayClient() throws Exception {
+        PaymentProperties.Alipay al = paymentProperties.getAlipay();
+
+        // 公钥模式(优先):配置了支付宝公钥即走公钥模式
+        if (al.getAlipayPublicKey() != null && !al.getAlipayPublicKey().isBlank()) {
+            log.info("初始化支付宝客户端(公钥模式): appId={}, serverUrl={}, notifyUrl={}",
+                    al.getAppId(), al.getServerUrl(), al.getNotifyUrl());
+
+            // 如果配置了私钥文件路径且未直接配置私钥字符串,自动读取文件内容
+            String privateKey = al.getPrivateKey();
+            if ((privateKey == null || privateKey.isBlank())
+                    && al.getPrivateKeyPath() != null && !al.getPrivateKeyPath().isBlank()) {
+                privateKey = readFileContent(al.getPrivateKeyPath());
+                log.info("已从文件读取应用私钥: {}", al.getPrivateKeyPath());
+            }
+
+            return new DefaultAlipayClient(
+                    al.getServerUrl(),
+                    al.getAppId(),
+                    privateKey,
+                    al.getFormat(),
+                    al.getCharset(),
+                    al.getAlipayPublicKey(),
+                    al.getSignType()
+            );
+        }
+
+        // 证书模式(备选):配置了应用公钥证书路径即走证书模式
+        if (al.getAppCertPath() != null && !al.getAppCertPath().isBlank()) {
+            log.info("初始化支付宝客户端(证书模式): appId={}, serverUrl={}, notifyUrl={}",
+                    al.getAppId(), al.getServerUrl(), al.getNotifyUrl());
+
+            // 如果配置了私钥文件路径,自动读取文件内容(从 classpath 或文件系统)
+            String privateKey = al.getPrivateKey();
+            if (al.getPrivateKeyPath() != null && !al.getPrivateKeyPath().isBlank()) {
+                privateKey = readFileContent(al.getPrivateKeyPath());
+                log.info("已从文件读取应用私钥: {}", al.getPrivateKeyPath());
+            }
+
+            // 证书路径解析为绝对路径(Alipay SDK 需要文件系统路径)
+            CertAlipayRequest certRequest = new CertAlipayRequest();
+            certRequest.setServerUrl(al.getServerUrl());
+            certRequest.setAppId(al.getAppId());
+            certRequest.setPrivateKey(privateKey);
+            certRequest.setFormat(al.getFormat());
+            certRequest.setCharset(al.getCharset());
+            certRequest.setSignType(al.getSignType());
+            certRequest.setCertPath(resolveFilePath(al.getAppCertPath()));
+            certRequest.setAlipayPublicCertPath(resolveFilePath(al.getAlipayCertPath()));
+            certRequest.setRootCertPath(resolveFilePath(al.getAlipayRootCertPath()));
+
+            return new DefaultAlipayClient(certRequest);
+        }
+
+        throw new IllegalStateException(
+                "支付宝配置不完整:需要配置公钥模式(alipay-public-key)或证书模式(app-cert-path)");
+    }
+
+    /**
+     * 微信支付客户端(仅 payment.mock=false 时注册)
+     *
+     * <p>使用 WxJava (weixin-java-pay) SDK,支持 API v3。
+     * 所需配置(application.yml 或环境变量):
+     * <ul>
+     *   <li>payment.wechat.app-id — 微信公众号/小程序 appId</li>
+     *   <li>payment.wechat.mch-id — 商户号</li>
+     *   <li>payment.wechat.api-v3-key — API v3 密钥(用于回调解密)</li>
+     *   <li>payment.wechat.private-key-path — 商户私钥文件路径(apiclient_key.pem)</li>
+     *   <li>payment.wechat.private-cert-path — 商户证书文件路径(apiclient_cert.pem)</li>
+     *   <li>payment.wechat.key-path — 商户 PKCS12 证书路径(apiclient_cert.p12,部分高安全 API 需要)</li>
+     *   <li>payment.wechat.serial-no — 商户证书序列号</li>
+     *   <li>payment.wechat.notify-url — 支付回调通知URL(需公网可访问)</li>
+     * </ul>
+     */
+    @Bean
+    @ConditionalOnProperty(prefix = "payment", name = "mock", havingValue = "false")
+    public WxPayService wxPayService() {
+        PaymentProperties.Wechat wc = paymentProperties.getWechat();
+        log.info("初始化微信支付客户端: appId={}, mchId={}, notifyUrl={}",
+                wc.getAppId(), wc.getMchId(), wc.getNotifyUrl());
+
+        WxPayConfig config = new WxPayConfig();
+        config.setAppId(wc.getAppId());
+        config.setMchId(wc.getMchId());
+        config.setApiV3Key(wc.getApiV3Key());
+        config.setCertSerialNo(wc.getSerialNo());
+        config.setNotifyUrl(wc.getNotifyUrl());
+
+        // apiclient_key.pem(商户私钥,API v3 签名用)
+        if (wc.getPrivateKeyPath() != null && !wc.getPrivateKeyPath().isBlank()) {
+            config.setPrivateKeyPath(resolveFilePath(wc.getPrivateKeyPath()));
+        }
+
+        // apiclient_cert.pem(商户证书,API v3 验签 / 部分 API 强制要求)
+        if (wc.getPrivateCertPath() != null && !wc.getPrivateCertPath().isBlank()) {
+            config.setPrivateCertPath(resolveFilePath(wc.getPrivateCertPath()));
+        }
+
+        // apiclient_cert.p12(PKCS12 证书,部分高安全接口如退款/企业付款需要)
+        // p12 密码默认为商户号
+        if (wc.getKeyPath() != null && !wc.getKeyPath().isBlank()) {
+            config.setKeyPath(resolveFilePath(wc.getKeyPath()));
+            config.setMchKey(wc.getMchId());
+        }
+
+        // 微信支付公钥(新商户必须配置,替代平台证书验签)
+        // 从微信支付商户平台 → 账户中心 → API安全 → 申请微信支付公钥
+        if (wc.getPublicKeyPath() != null && !wc.getPublicKeyPath().isBlank()) {
+            config.setPublicKeyPath(resolveFilePath(wc.getPublicKeyPath()));
+            log.info("已配置微信支付公钥: {}", wc.getPublicKeyPath());
+        }
+        if (wc.getPublicKeyId() != null && !wc.getPublicKeyId().isBlank()) {
+            config.setPublicKeyId(wc.getPublicKeyId());
+            log.info("已配置微信支付公钥ID: {}", wc.getPublicKeyId());
+        }
+
+        WxPayService service = new WxPayServiceImpl();
+        service.setConfig(config);
+        return service;
+    }
 }

+ 24 - 6
zhijiayun-payment/src/main/java/com/xuekairui/payment/config/PaymentProperties.java

@@ -6,12 +6,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
 /**
  * 支付模块配置属性
  *
- * <p>生产环境切换步骤:
+ * <p>支付宝当面付和微信Native支付均已接入SDK,生产环境切换步骤:
  * <ol>
  *   <li>将 payment.mock 设为 false</li>
- *   <li>填写 payment.wechat 或 payment.alipay 对应渠道的配置参数</li>
- *   <li>引入对应支付SDK依赖(weixin-java-pay / alipay-sdk-java)</li>
- *   <li>实现 PaymentChannelStrategy 中对应渠道的预下单和验签逻辑</li>
+ *   <li>填写 payment.alipay 和 payment.wechat 对应配置参数</li>
+ *   <li>确保已引入 alipay-sdk-java 和 weixin-java-pay 依赖(已在 pom.xml 中配置)</li>
  * </ol>
  */
 @Data
@@ -55,26 +54,45 @@ public class PaymentProperties {
         private String apiV3Key;
         /** 商户私钥文件路径(apiclient_key.pem) */
         private String privateKeyPath;
+        /** 商户证书文件路径(apiclient_cert.pem,API v3 验签用) */
+        private String privateCertPath;
+        /** 商户 PKCS12 证书文件路径(apiclient_cert.p12,部分高安全 API 需要) */
+        private String keyPath;
         /** 商户证书序列号 */
         private String serialNo;
         /** 支付回调通知URL(需公网可访问) */
         private String notifyUrl;
+        /** 微信支付公钥文件路径(pub_key.pem,新商户必须配置,替代平台证书验签) */
+        private String publicKeyPath;
+        /** 微信支付公钥ID(公钥序列号,下载公钥时一起获取) */
+        private String publicKeyId;
         /** 微信支付平台证书路径(用于验签,可选,SDK可自动下载) */
         private String platformCertPath;
     }
 
     /**
      * 支付宝配置(当面付/扫码支付)
+     * 支持两种模式(公钥模式优先):
+     * 1. 公钥模式(优先):配置 private-key + alipay-public-key
+     * 2. 证书模式(备选):配置 private-key-path + app-cert-path + alipay-cert-path + alipay-root-cert-path
      * 文档:https://opendocs.alipay.com/open/194/106078
      */
     @Data
     public static class Alipay {
         /** 支付宝应用 appId */
         private String appId;
-        /** 应用私钥(RSA2) */
+        /** 应用私钥(RSA2),公钥模式时填写私钥字符串 */
         private String privateKey;
-        /** 支付宝公钥(用于验签) */
+        /** 应用私钥文件路径,证书模式时填写(设置后自动读取文件内容作为 privateKey) */
+        private String privateKeyPath;
+        /** 支付宝公钥(公钥模式验签用) */
         private String alipayPublicKey;
+        /** 应用公钥证书路径(证书模式,.crt 文件) */
+        private String appCertPath;
+        /** 支付宝公钥证书路径(证书模式,.crt 文件) */
+        private String alipayCertPath;
+        /** 支付宝根证书路径(证书模式,.crt 文件) */
+        private String alipayRootCertPath;
         /** 支付回调通知URL(需公网可访问) */
         private String notifyUrl;
         /** 支付宝网关地址(正式:https://openapi.alipay.com/gateway.do,沙箱:https://openapi-sandbox.dl.alipaydev.com/gateway.do) */

+ 99 - 61
zhijiayun-payment/src/main/java/com/xuekairui/payment/controller/PaymentCallbackController.java

@@ -1,5 +1,9 @@
 package com.xuekairui.payment.controller;
 
+import com.alipay.api.internal.util.AlipaySignature;
+import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
+import com.github.binarywang.wxpay.service.WxPayService;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
 import com.xuekairui.payment.config.PaymentProperties;
@@ -7,8 +11,11 @@ import com.xuekairui.payment.service.PaymentOrderService;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.web.bind.annotation.*;
 
+import org.springframework.core.io.ClassPathResource;
+
 import java.io.BufferedReader;
 import java.util.HashMap;
 import java.util.Map;
@@ -32,6 +39,7 @@ public class PaymentCallbackController {
 
     private final PaymentOrderService paymentOrderService;
     private final PaymentProperties paymentProperties;
+    private final ObjectProvider<WxPayService> wxPayServiceProvider;
 
     // ======================== 微信支付回调 ========================
 
@@ -83,11 +91,12 @@ public class PaymentCallbackController {
     /**
      * 微信回调 - 生产模式处理
      *
-     * <p>上线时需
+     * <p>使用 WxJava SDK 的 parseOrderNotifyV3Result 自动完成验签+解密
      * <ol>
-     *   <li>引入 weixin-java-pay 依赖</li>
-     *   <li>使用 WxPayService.parseOrderNotifyResult() 自动验签+解密</li>
-     *   <li>替换下方 TODO 为真实SDK调用</li>
+     *   <li>从 HTTP 头构造 SignatureHeader</li>
+     *   <li>SDK 自动用微信平台证书验签</li>
+     *   <li>SDK 用 API v3 密钥解密 resource.ciphertext</li>
+     *   <li>提取 out_trade_no 和 transaction_id</li>
      * </ol>
      */
     private String handleWechatProduction(HttpServletRequest request) {
@@ -96,31 +105,42 @@ public class PaymentCallbackController {
             String body = readRequestBody(request);
             log.info("收到微信支付回调通知: body={}", body);
 
-            // TODO: 接入微信支付SDK后替换为真实验签+解密
-            //  WxPayService wxPayService = ...;
-            //  // 自动验签 + 解密
-            //  WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(body);
-            //  String orderNo = notifyResult.getOutTradeNo();
-            //  String tradeNo = notifyResult.getTransactionId();
-            //  if (!"SUCCESS".equals(notifyResult.getTradeState())) {
-            //      log.warn("微信回调交易状态非成功: {}", notifyResult.getTradeState());
-            //      return "SUCCESS"; // 非成功状态也返回SUCCESS避免重复通知
-            //  }
-
-            // 占位:从请求头和请求体提取关键信息
-            String timestamp = request.getHeader("Wechatpay-Timestamp");
-            String nonce = request.getHeader("Wechatpay-Nonce");
-            String serial = request.getHeader("Wechatpay-Serial");
-            String signature = request.getHeader("Wechatpay-Signature");
-
-            if (!verifyWechatSign(timestamp, nonce, body, signature, serial)) {
-                log.error("微信回调验签失败: timestamp={}, nonce={}, serial={}", timestamp, nonce, serial);
-                throw new BusinessException(ErrorCode.PAY_SIGN_VERIFY_FAILED, "微信回调验签失败");
+            WxPayService wxPayService = wxPayServiceProvider.getIfAvailable();
+            if (wxPayService == null) {
+                log.error("WxPayService未初始化,无法处理微信回调");
+                return "FAIL";
+            }
+
+            // 构造签名头(SDK 自动验签)
+            SignatureHeader header = new SignatureHeader();
+            header.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+            header.setNonce(request.getHeader("Wechatpay-Nonce"));
+            header.setSerial(request.getHeader("Wechatpay-Serial"));
+            header.setSignature(request.getHeader("Wechatpay-Signature"));
+
+            // SDK 自动验签 + AES-GCM 解密
+            WxPayNotifyV3Result notifyResult = wxPayService.parseOrderNotifyV3Result(body, header);
+            WxPayNotifyV3Result.DecryptNotifyResult decryptResult = notifyResult.getResult();
+
+            String orderNo = decryptResult.getOutTradeNo();
+            String tradeNo = decryptResult.getTransactionId();
+            String tradeState = decryptResult.getTradeState();
+
+            log.info("微信回调验签解密成功: orderNo={}, tradeNo={}, tradeState={}", orderNo, tradeNo, tradeState);
+
+            // 非成功状态也返回 SUCCESS 避免重复通知
+            if (!"SUCCESS".equals(tradeState)) {
+                log.warn("微信回调交易状态非成功: tradeState={}, orderNo={}", tradeState, orderNo);
+                return "SUCCESS";
             }
 
-            // 占位:从解密后的JSON中提取订单号和交易号(需SDK解密后替换)
-            // 实际上线时由SDK的 parseOrderNotifyResult 返回
-            log.warn("【占位】微信回调验签通过,但尚未接入SDK解密,无法提取订单号");
+            if (orderNo == null) {
+                log.warn("微信回调缺少订单号");
+                return "FAIL";
+            }
+
+            paymentOrderService.processPaidCallback(orderNo,
+                    tradeNo != null ? tradeNo : "WECHAT_" + orderNo);
             return "SUCCESS";
 
         } catch (BusinessException e) {
@@ -131,28 +151,6 @@ public class PaymentCallbackController {
         }
     }
 
-    /**
-     * 微信回调验签(占位实现)
-     *
-     * <p>上线时替换为 WxPayService.checkV3Sign() 或手动 RSA 验签
-     */
-    private boolean verifyWechatSign(String timestamp, String nonce, String body,
-                                      String signature, String serial) {
-        // TODO: 使用微信平台证书进行 RSA 验签
-        //  验签串 = timestamp + "\n" + nonce + "\n" + body + "\n"
-        //  Signature signature = Signature.getInstance("SHA256withRSA");
-        //  signature.initVerify(platformPublicKey);
-        //  signature.update(signStr.getBytes(StandardCharsets.UTF_8));
-        //  return signature.verify(Base64.getDecoder().decode(signatureStr));
-
-        // 占位:未配置时返回 true(仅开发调试用,上线必须实现真实验签)
-        if (paymentProperties.getWechat().getApiV3Key() == null) {
-            log.warn("【占位】微信API v3密钥未配置,跳过验签(上线前必须配置)");
-            return true;
-        }
-        return false; // 配置了但未实现验签逻辑时,拒绝通过
-    }
-
     // ======================== 支付宝回调 ========================
 
     /**
@@ -250,25 +248,65 @@ public class PaymentCallbackController {
     }
 
     /**
-     * 支付宝回调验签
+     * 支付宝回调验签(RSA2,公钥模式优先)
      *
-     * <p>上线时替换为 AlipaySignature.rsaCheckV2()
+     * <p>使用支付宝SDK的 AlipaySignature 进行验签:
+     * <ol>
+     *   <li>公钥模式(优先):用支付宝公钥 rsaCheckV2 验签</li>
+     *   <li>证书模式(备选):用支付宝公钥证书 rsaCertCheckV2 验签</li>
+     * </ol>
      */
     private boolean verifyAlipaySign(Map<String, String> params) {
         PaymentProperties.Alipay al = paymentProperties.getAlipay();
 
-        if (al.getAlipayPublicKey() == null) {
-            // 占位:未配置公钥时跳过验签(上线前必须配置)
-            log.warn("【占位】支付宝公钥未配置,跳过验签(上线前必须配置)");
-            return true;
-        }
+        try {
+            // 公钥模式(优先):使用支付宝公钥验签
+            if (al.getAlipayPublicKey() != null && !al.getAlipayPublicKey().isBlank()) {
+                return AlipaySignature.rsaCheckV2(
+                        params,
+                        al.getAlipayPublicKey(),
+                        al.getCharset(),
+                        al.getSignType()
+                );
+            }
+
+            // 证书模式(备选):使用支付宝公钥证书验签
+            if (al.getAppCertPath() != null && !al.getAppCertPath().isBlank()) {
+                String alipayPublicCertPath = resolveFilePath(al.getAlipayCertPath());
+                if (alipayPublicCertPath == null) {
+                    log.error("支付宝公钥证书路径未配置,无法验签");
+                    return false;
+                }
+                return AlipaySignature.rsaCertCheckV2(
+                        params,
+                        alipayPublicCertPath,
+                        al.getCharset(),
+                        al.getSignType()
+                );
+            }
 
-        // TODO: 接入支付宝SDK后替换为真实验签
-        //  boolean verified = AlipaySignature.rsaCheckV2(
-        //      params, al.getAlipayPublicKey(), al.getCharset(), al.getSignType());
-        //  return verified;
+            log.error("支付宝未配置公钥或证书,无法验签(需配置 alipay-public-key 或 app-cert-path)");
+            return false;
+        } catch (Exception e) {
+            log.error("支付宝验签异常: {}", e.getMessage());
+            return false;
+        }
+    }
 
-        return false; // 配置了公钥但未实现验签逻辑时,拒绝通过
+    /**
+     * 解析证书文件路径为文件系统绝对路径(优先从 classpath 解析)
+     */
+    private String resolveFilePath(String path) {
+        if (path == null || path.isBlank()) return path;
+        ClassPathResource resource = new ClassPathResource(path);
+        if (resource.exists()) {
+            try {
+                return resource.getFile().getAbsolutePath();
+            } catch (Exception e) {
+                log.warn("从 classpath 解析文件路径失败: {}, 使用原始路径", path);
+            }
+        }
+        return path;
     }
 
     // ======================== 工具方法 ========================

+ 4 - 3
zhijiayun-payment/src/main/java/com/xuekairui/payment/controller/PaymentController.java

@@ -61,10 +61,11 @@ public class PaymentController {
     }
 
     /**
-     * 模拟支付(开发测试用)
+     * 手动确认支付(开发/测试用)
+     * mock模式:模拟支付;真实支付模式:手动确认(用于测试回调流程)
      */
     @PostMapping("/order/{orderNo}/pay")
-    public Result<PaymentResultResponse> mockPay(@PathVariable String orderNo) {
-        return Result.success(paymentOrderService.mockPay(orderNo));
+    public Result<PaymentResultResponse> confirmPay(@PathVariable String orderNo) {
+        return Result.success(paymentOrderService.confirmPay(orderNo));
     }
 }

+ 109 - 63
zhijiayun-payment/src/main/java/com/xuekairui/payment/service/PaymentOrderService.java

@@ -1,6 +1,13 @@
 package com.xuekairui.payment.service;
 
 import cn.hutool.core.util.IdUtil;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.domain.AlipayTradePrecreateModel;
+import com.alipay.api.request.AlipayTradePrecreateRequest;
+import com.alipay.api.response.AlipayTradePrecreateResponse;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.service.WxPayService;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
@@ -16,6 +23,7 @@ import com.xuekairui.user.enums.MembershipLevel;
 import com.xuekairui.user.service.MembershipService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -37,6 +45,8 @@ public class PaymentOrderService {
     private final QRCodeService qrCodeService;
     private final MembershipService membershipService;
     private final PaymentProperties paymentProperties;
+    private final ObjectProvider<AlipayClient> alipayClientProvider;
+    private final ObjectProvider<WxPayService> wxPayServiceProvider;
 
     /**
      * 创建支付订单
@@ -125,14 +135,14 @@ public class PaymentOrderService {
     }
 
     /**
-     * 模拟支付(开发/测试用)
+     * 手动确认支付(开发/测试用)
+     * <p>
+     * mock模式:模拟支付,自动生成交易号
+     * 真实支付模式:手动确认支付(用于测试回调流程,不实际扣款)
+     * </p>
      */
     @Transactional
-    public PaymentResultResponse mockPay(String orderNo) {
-        if (!paymentProperties.isMock()) {
-            throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_SUPPORT,
-                    "模拟支付仅在开发环境可用");
-        }
+    public PaymentResultResponse confirmPay(String orderNo) {
         PaymentOrder order = paymentOrderMapper.selectByOrderNo(orderNo);
         if (order == null) {
             throw new BusinessException(ErrorCode.PAY_ORDER_NOT_FOUND);
@@ -143,8 +153,9 @@ public class PaymentOrderService {
         if (PaymentStatus.EXPIRED.name().equals(order.getStatus())) {
             throw new BusinessException(ErrorCode.PAY_ORDER_EXPIRED);
         }
-        String mockTradeNo = "MOCK_" + IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
-        return processPaidCallback(orderNo, mockTradeNo);
+        String prefix = paymentProperties.isMock() ? "MOCK_" : "MANUAL_";
+        String tradeNo = prefix + IdUtil.fastSimpleUUID().substring(0, 16).toUpperCase();
+        return processPaidCallback(orderNo, tradeNo);
     }
 
     /**
@@ -287,14 +298,11 @@ public class PaymentOrderService {
     }
 
     /**
-     * 微信Native支付预下单
+     * 微信Native支付预下单(API v3)
      *
-     * <p>接入步骤:
-     * <ol>
-     *   <li>引入依赖:implementation 'com.github.binarywang:weixin-java-pay:4.6.0'</li>
-     *   <li>配置 WxPayService Bean(参考 PaymentConfig)</li>
-     *   <li>替换下方 TODO 为真实SDK调用</li>
-     * </ol>
+     * <p>使用 WxJava SDK 调用微信支付 v3 Native下单接口,
+     * 返回 codeUrl(二维码链接),前端可直接生成二维码展示。
+     * 用户扫码后在微信App中完成付款,微信异步回调 notifyUrl。
      */
     private PrepayResult createWechatPrepay(String orderNo, java.math.BigDecimal amount, String subject) {
         PaymentProperties.Wechat wc = paymentProperties.getWechat();
@@ -303,37 +311,53 @@ public class PaymentOrderService {
                     "微信支付未配置,请检查 payment.wechat 参数");
         }
 
-        // TODO: 接入微信支付SDK后替换为真实预下单调用
-        //  WxPayService wxPayService = ...;
-        //  WxPayNativeOrderResult result = wxPayService.createOrder(
-        //      new WxPayNativeOrderRequest()
-        //          .setOutTradeNo(orderNo)
-        //          .setTotalFee(amount.movePointRight(2).intValue())  // 微信单位为分
-        //          .setBody(subject)
-        //          .setNotifyUrl(wc.getNotifyUrl())
-        //          .setTimeExpire(expireTimeStr)
-        //  );
-        //  String codeUrl = result.getCodeUrl();
-
-        // 当前为占位实现,返回格式化内容(上线前必须替换)
-        String codeUrl = String.format("weixin://wxpay/bizpayurl?pr=%s&ck=1", orderNo);
-        log.warn("【占位】微信预下单: orderNo={}, amount={}, subject={}, codeUrl={}", orderNo, amount, subject, codeUrl);
-
-        return PrepayResult.builder()
-                .qrContent(codeUrl)
-                .payUrl(codeUrl)
-                .build();
+        WxPayService wxPayService = wxPayServiceProvider.getIfAvailable();
+        if (wxPayService == null) {
+            throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED,
+                    "WxPayService未初始化,请确认 payment.mock=false 且已配置微信支付参数");
+        }
+
+        try {
+            WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+            request.setOutTradeNo(orderNo);
+            request.setDescription(subject);
+            request.setNotifyUrl(wc.getNotifyUrl());
+
+            // 金额单位为分
+            WxPayUnifiedOrderV3Request.Amount amountObj = new WxPayUnifiedOrderV3Request.Amount();
+            amountObj.setTotal(amount.movePointRight(2).intValue());
+            amountObj.setCurrency("CNY");
+            request.setAmount(amountObj);
+
+            // 过期时间(RFC3339格式)
+            String timeExpire = java.time.format.DateTimeFormatter
+                    .ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")
+                    .format(java.time.ZonedDateTime.now()
+                            .plusMinutes(paymentProperties.getOrderExpireMinutes()));
+            request.setTimeExpire(timeExpire);
+
+            // WxJava 4.7.0: NATIVE 模式下 createOrderV3 直接返回 code_url 字符串
+            String codeUrl = wxPayService.createOrderV3(TradeTypeEnum.NATIVE, request);
+            log.info("微信预下单成功: orderNo={}, amount={}, codeUrl={}", orderNo, amount, codeUrl);
+
+            return PrepayResult.builder()
+                    .qrContent(codeUrl)
+                    .payUrl(codeUrl)
+                    .build();
+        } catch (BusinessException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("微信预下单异常: orderNo={}", orderNo, e);
+            throw new BusinessException(ErrorCode.PAY_FAILED, "微信下单异常: " + e.getMessage());
+        }
     }
 
     /**
-     * 支付宝扫码支付预下单
+     * 支付宝扫码支付预下单(当面付)
      *
-     * <p>接入步骤:
-     * <ol>
-     *   <li>引入依赖:implementation 'com.alipay.sdk:alipay-sdk-java:4.39.79.ALL'</li>
-     *   <li>配置 AlipayClient Bean(参考 PaymentConfig)</li>
-     *   <li>替换下方 TODO 为真实SDK调用</li>
-     * </ol>
+     * <p>使用 AlipayTradePrecreateRequest 调用支付宝预下单接口,
+     * 返回二维码链接(qrCode),前端可直接用此链接生成二维码展示。
+     * 用户扫码后在支付宝App中完成付款,支付宝异步回调 notifyUrl。
      */
     private PrepayResult createAlipayPrepay(String orderNo, java.math.BigDecimal amount, String subject) {
         PaymentProperties.Alipay al = paymentProperties.getAlipay();
@@ -342,27 +366,49 @@ public class PaymentOrderService {
                     "支付宝未配置,请检查 payment.alipay 参数");
         }
 
-        // TODO: 接入支付宝SDK后替换为真实预下单调用
-        //  AlipayClient alipayClient = ...;
-        //  AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
-        //  request.setBizContent("{"
-        //      + "\"out_trade_no\":\"" + orderNo + "\","
-        //      + "\"total_amount\":\"" + amount.toPlainString() + "\","
-        //      + "\"subject\":\"" + subject + "\","
-        //      + "\"timeout_express\":\"" + paymentProperties.getOrderExpireMinutes() + "m\""
-        //      + "}");
-        //  request.setNotifyUrl(al.getNotifyUrl());
-        //  AlipayTradePrecreateResponse response = alipayClient.execute(request);
-        //  String qrCode = response.getQrCode();
-
-        // 当前为占位实现,返回格式化内容(上线前必须替换)
-        String qrCode = String.format("https://qr.alipay.com/%s", orderNo);
-        log.warn("【占位】支付宝预下单: orderNo={}, amount={}, subject={}, qrCode={}", orderNo, amount, subject, qrCode);
-
-        return PrepayResult.builder()
-                .qrContent(qrCode)
-                .payUrl(qrCode)
-                .build();
+        AlipayClient alipayClient = alipayClientProvider.getIfAvailable();
+        if (alipayClient == null) {
+            throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED,
+                    "AlipayClient未初始化,请确认 payment.mock=false 且已配置支付宝参数");
+        }
+
+        AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
+        request.setNotifyUrl(al.getNotifyUrl());
+
+        AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
+        model.setOutTradeNo(orderNo);
+        model.setTotalAmount(amount.toPlainString());
+        model.setSubject(subject);
+        model.setTimeoutExpress(paymentProperties.getOrderExpireMinutes() + "m");
+        request.setBizModel(model);
+
+        try {
+            // 公钥模式优先:配置了支付宝公钥用 execute,否则用 certificateExecute
+            boolean publicKeyMode = al.getAlipayPublicKey() != null && !al.getAlipayPublicKey().isBlank();
+            AlipayTradePrecreateResponse response = publicKeyMode
+                    ? alipayClient.execute(request)
+                    : alipayClient.certificateExecute(request);
+            if (!response.isSuccess()) {
+                log.error("支付宝预下单失败: orderNo={}, code={}, msg={}, subCode={}, subMsg={}",
+                        orderNo, response.getCode(), response.getMsg(),
+                        response.getSubCode(), response.getSubMsg());
+                throw new BusinessException(ErrorCode.PAY_FAILED,
+                        "支付宝下单失败: " + response.getSubMsg());
+            }
+
+            String qrCode = response.getQrCode();
+            log.info("支付宝预下单成功: orderNo={}, amount={}, qrCode={}", orderNo, amount, qrCode);
+
+            return PrepayResult.builder()
+                    .qrContent(qrCode)
+                    .payUrl(qrCode)
+                    .build();
+        } catch (BusinessException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("支付宝预下单异常: orderNo={}", orderNo, e);
+            throw new BusinessException(ErrorCode.PAY_FAILED, "支付宝下单异常: " + e.getMessage());
+        }
     }
 
     private OrderResponse toResponse(PaymentOrder order) {

+ 40 - 12
zhijiayun-test/src/main/resources/static/invite-test.html

@@ -294,6 +294,29 @@ async function runTest(sectionId, method, path, body, requireAuth, validatorName
     if (durEl) durEl.textContent = result.duration + 'ms';
     if (rawEl) rawEl.textContent = JSON.stringify(result.data, null, 2);
 
+    // 检测响应中的二维码并渲染图片预览
+    const qrBase64 = result.data?.data?.qrCodeBase64;
+    let qrBox = document.getElementById(sectionId + '-qrcode');
+    if (qrBase64) {
+        if (!qrBox) {
+            qrBox = document.createElement('div');
+            qrBox.id = sectionId + '-qrcode';
+            qrBox.style.cssText = 'text-align:center;padding:12px;background:#fff;border-radius:4px;margin-top:8px;';
+            rawEl.parentNode.insertBefore(qrBox, rawEl.nextSibling);
+        }
+        qrBox.innerHTML = '<img src="' + qrBase64 + '" style="max-width:220px;border:1px solid #eee;border-radius:4px;"><div style="margin-top:6px;font-size:12px;color:#666;">支付二维码(扫码付款后回调自动激活,也可手动确认支付测试回调)</div>';
+
+        // 自动回填订单号到相关输入框
+        const orderNo = result.data?.data?.orderNo;
+        if (orderNo) {
+            ['payment-query-order-orderNo', 'payment-mock-pay-orderNo',
+             'payment-callback-wechat-out_trade_no', 'payment-callback-alipay-out_trade_no']
+                .forEach(fid => { const el = document.getElementById(fid); if (el) el.value = orderNo; });
+        }
+    } else if (qrBox) {
+        qrBox.remove();
+    }
+
     let passed = false;
     if (validatorName && typeof window[validatorName] === 'function') {
         try { passed = window[validatorName](result); } catch (e) { passed = false; }
@@ -665,7 +688,7 @@ const FIELD_DESC = {
         ['createTime', 'DateTime', '创建时间']
     ],
     'inviteCode': [
-        ['code', 'String', '邀请码(6位)'],
+        ['code', 'String', '邀请码(8位)'],
         ['inviteLink', 'String', '完整邀请链接'],
         ['inviterNickname', 'String', '邀请人昵称'],
         ['usedCount', 'Integer', '已使用次数(注册)'],
@@ -852,9 +875,11 @@ const FIELD_DESC = {
     ],
     'inviteConfig': [
         ['id', 'Long', '配置ID'],
-        ['rewardCrawlerCount', 'Integer', '每次邀请奖励爬虫次数'],
-        ['rewardType', 'String', '奖励类型(CRAWLER_COUNT等)'],
+        ['rewardCrawlerCount', 'Integer', '每次邀请奖励爬虫次数(历史字段)'],
+        ['rewardType', 'String', '奖励类型(MEMBERSHIP等)'],
+        ['rewardMonths', 'Integer', '邀请人奖励会员时长(月)'],
         ['maxDailyReward', 'Integer', '每日最大奖励次数'],
+        ['maxTotalInvites', 'Integer', '单个账号总邀请上限(默认30)'],
         ['inviteCodeExpireDays', 'Integer', '邀请码过期天数'],
         ['maxInvitePerDay', 'Integer', '每日最大邀请数'],
         ['appDownloadUrl', 'String', 'APP下载地址'],
@@ -1106,6 +1131,8 @@ function renderInviteModule() {
     ${testSection('invite-stats', '邀请统计', 'GET', 'get', '/api/invite/stats', '需认证', 'auth-required', null, true, 'inviteStats', '获取邀请人数、注册人数、累计奖励等统计')}
     ${testSection('invite-rewards', '邀请记录', 'GET', 'get', '/api/invite/rewards', '需认证', 'auth-required', null, true, 'inviteReward', '获取邀请记录列表,含被邀请人注册状态')}
     ${testSection('invite-extra-quota', '额外配额', 'GET', 'get', '/api/invite/extra-quota', '需认证', 'auth-required', null, false, null, '获取可用额外爬虫次数(邀请奖励+运营发放),返回Integer数值')}
+    ${testSection('invite-bind', '补填邀请码', 'POST', 'post', '/api/invite/bind', '需认证', 'auth-required',
+        [{label:'邀请码/链接(inviteCode)', name:'inviteCode', placeholder:'A3K7M9P2 或 https://app.zhijiayun.com/invite/A3K7M9P2', default:''}], true, null, '注册时未填邀请码,注册后30天内可补填。补填后邀请人获赠1个月高级会员时长')}
     ${testSection('invite-page', '落地页', 'GET', 'get', '/api/invite/page/{code}?channel=windows', '公开', 'auth-public',
         [{label:'邀请码(code)', name:'code', placeholder:'输入邀请码', default:''},
          {label:'渠道(channel)', name:'channel', placeholder:'windows/app/wechat/miniapp/dingtalk/feishu', default:'windows'}], true, 'invitePage', '浏览器打开邀请链接时展示的落地页数据,默认渠道为windows(Windows桌面客户端)')}
@@ -1143,21 +1170,21 @@ function renderPaymentModule() {
     <div class="batch-buttons">
         <button class="btn btn-warning btn-sm" onclick="runPaymentTests()">运行全部支付模块测试</button>
     </div>
-    ${testSection('payment-plans', '支付方案列表', 'GET', 'get', '/api/payment/plans', '公开', 'auth-public', null, true, null, '获取所有可用的支付方案列表(月卡/季卡/年卡)')}
+    ${testSection('payment-plans', '支付方案列表', 'GET', 'get', '/api/payment/plans', '公开', 'auth-public', null, true, null, '获取所有可用的支付方案列表(高级会员月卡/年卡)')}
     ${testSection('payment-create-order', '创建支付订单', 'POST', 'post', '/api/payment/order/create', '需认证', 'auth-required',
         [{label:'方案ID(planId)', name:'planId', placeholder:'1', default:'1'},
-         {label:'支付渠道(channel)', name:'channel', placeholder:'WECHAT/ALIPAY', default:'WECHAT'}], true, null, '创建支付订单并返回二维码(qrCodeBase64)和支付链接(payUrl),15分钟内未付款订单自动过期。mock模式返回模拟二维码,生产模式返回真实支付链接')}
+         {label:'支付渠道(channel)', name:'channel', placeholder:'WECHAT/ALIPAY', default:'WECHAT'}], true, null, '创建支付订单并返回二维码(qrCodeBase64)和支付链接(payUrl),15分钟内未付款订单自动过期。mock模式返回模拟二维码,真实支付模式返回真实支付链接,也可点击「手动确认支付」按钮测试回调流程')}
     ${testSection('payment-query-order', '查询订单', 'GET', 'get', '/api/payment/order/{orderNo}', '需认证', 'auth-required',
         [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], true, null, '根据订单号查询支付订单详情,响应中expireHint含过期提示')}
     ${testSection('payment-orders', '我的订单列表', 'GET', 'get', '/api/payment/orders', '需认证', 'auth-required', null, true, null, '查询当前用户的所有支付订单,待支付订单含expireHint过期提示')}
-    ${testSection('payment-mock-pay', '模拟支付', 'POST', 'post', '/api/payment/order/{orderNo}/pay', '需认证', 'auth-required',
-        [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], false, null, '开发环境模拟支付(自动激活会员),需在15分钟过期前操作')}
+    ${testSection('payment-mock-pay', '手动确认支付', 'POST', 'post', '/api/payment/order/{orderNo}/pay', '需认证', 'auth-required',
+        [{label:'订单号(orderNo)', name:'orderNo', placeholder:'OP...', default:''}], false, null, '开发测试用。mock模式=模拟支付自动激活会员;真实支付模式=手动确认支付(不实际扣款,用于测试回调流程)。需在15分钟过期前操作')}
     ${testSection('payment-callback-wechat', '微信回调', 'POST', 'post', '/api/payment/callback/wechat', '公开', 'auth-public',
         [{label:'订单号(out_trade_no)', name:'out_trade_no', placeholder:'OP...', default:''},
-         {label:'交易号(transaction_id,可选)', name:'transaction_id', placeholder:'wx_', default:''}], false, null, '微信支付回调通知。mock模式从参数取订单号,生产模式需验签+解密(API v3)')}
+         {label:'交易号(transaction_id,可选)', name:'transaction_id', placeholder:'wx_', default:''}], false, null, '微信支付回调通知。mock模式从参数取订单号,真实支付模式需验签+解密(API v3)')}
     ${testSection('payment-callback-alipay', '支付宝回调', 'POST', 'post', '/api/payment/callback/alipay', '公开', 'auth-public',
         [{label:'订单号(out_trade_no)', name:'out_trade_no', placeholder:'OP...', default:''},
-         {label:'交易号(trade_no,可选)', name:'trade_no', placeholder:'2024...', default:''}], false, null, '支付宝回调通知。mock模式从参数取订单号,生产模式需RSA2验签')}`;
+         {label:'交易号(trade_no,可选)', name:'trade_no', placeholder:'2024...', default:''}], false, null, '支付宝回调通知。mock模式从参数取订单号,真实支付模式需RSA2验签')}`;
 }
 
 function renderUploadModule() {
@@ -1263,12 +1290,13 @@ function renderAdminInviteModule() {
     ${testSection('admin-invite-config', '邀请配置查询', 'GET', 'get', '/api/admin/invite/config', '需认证', 'auth-required', null, true, 'inviteConfig', '获取当前激活的邀请配置')}
     ${testSection('admin-invite-configs', '全部邀请配置', 'GET', 'get', '/api/admin/invite/configs', '需认证', 'auth-required', null, true, 'inviteConfig', '获取所有邀请配置历史记录')}
     ${testSection('admin-invite-config-update', '更新邀请配置', 'PUT', 'put', '/api/admin/invite/config', '需认证', 'auth-required',
-        [{label:'奖励次数(rewardCrawlerCount)', name:'rewardCrawlerCount', placeholder:'5', default:'5'},
+        [{label:'奖励月数(rewardMonths)', name:'rewardMonths', placeholder:'1', default:'1'},
+         {label:'总邀请上限(maxTotalInvites)', name:'maxTotalInvites', placeholder:'30', default:'30'},
          {label:'日奖励上限(maxDailyReward)', name:'maxDailyReward', placeholder:'50', default:'50'},
          {label:'邀请码过期天(inviteCodeExpireDays)', name:'inviteCodeExpireDays', placeholder:'30', default:'30'},
          {label:'日邀请上限(maxInvitePerDay)', name:'maxInvitePerDay', placeholder:'10', default:'10'},
          {label:'APP下载URL(appDownloadUrl)', name:'appDownloadUrl', placeholder:'https://...', default:'https://download.example.com/app.exe'},
-         {label:'落地页标题(landingTitle)', name:'landingTitle', placeholder:'标题', default:'邀请你加入智价云药店'}], false, 'inviteConfig', '更新邀请配置参数')}
+         {label:'落地页标题(landingTitle)', name:'landingTitle', placeholder:'标题', default:'邀请你加入智价云药店'}], false, 'inviteConfig', '更新邀请配置参数,rewardMonths=邀请人每次获赠高级会员月数,maxTotalInvites=单个账号总邀请上限')}
     ${testSection('admin-invite-rewards', '邀请记录查询', 'GET', 'get', '/api/admin/invite/rewards?userId=1', '需认证', 'auth-required', null, true, 'inviteReward', '查看指定用户的邀请奖励记录')}
     ${testSection('admin-invite-conversion', '邀请转化统计', 'GET', 'get', '/api/admin/invite/conversion-stats?userId=1', '需认证', 'auth-required', null, true, 'conversionStats', '查看用户邀请点击-注册转化率')}`;
 }
@@ -1572,7 +1600,7 @@ function validate_adminAuditTarget(result) { return result.data?.code === 200 &&
 function validate_adminAuditStats(result) { return result.data?.code === 200; }
 
 // 支付模块验证器
-function validate_paymentPlans(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length >= 3; }
+function validate_paymentPlans(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length >= 2; }
 function validate_paymentOrders(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
 function validate_searchQuery(result) { return result.data?.code === 200 && result.data?.data?.prices !== undefined; }
 function validate_searchSuggestions(result) { return result.data?.code === 200 && Array.isArray(result.data?.data) && result.data?.data?.length > 0; }

+ 2 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/config/SecurityConfig.java

@@ -74,7 +74,8 @@ public class SecurityConfig {
                         .requestMatchers("/api/level/user/**").hasAnyRole("ADMIN", "SUPER_ADMIN")
                         // 用户端邀请接口需登录(必须在 /api/invite/{code} 公开匹配之前)
                         .requestMatchers("/api/invite/code", "/api/invite/stats",
-                                "/api/invite/rewards", "/api/invite/extra-quota").authenticated()
+                                "/api/invite/rewards", "/api/invite/extra-quota",
+                                "/api/invite/bind").authenticated()
                         // 普通用户接口
                         .requestMatchers("/api/level/my").authenticated()
                         .requestMatchers(PUBLIC_URLS).permitAll()

+ 5 - 7
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -73,11 +73,10 @@ public class AuthService {
                     .build();
             userMapper.insert(user);
 
-            // 发布用户注册事件(邀请模块监听处理邀请码)
+            // 发布用户注册事件(邀请模块监听处理邀请码,赠送会员奖励
             eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
 
-            // 新用户赠送1个月普通会员
-            membershipService.grantNewUserMembership(user.getId(), request.getInviteCode());
+            // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
         }
 
         // 3. 更新登录信息
@@ -212,8 +211,7 @@ public class AuthService {
             // 发布用户注册事件,邀请模块可据此建立邀请关系并发放奖励
             eventPublisher.publishEvent(new UserRegisteredEvent(this, wechatUser.getId(), request.getInviteCode()));
 
-            // 新用户赠送普通会员:无邀请码1个月,有邀请码2个月
-            membershipService.grantNewUserMembership(wechatUser.getId(), request.getInviteCode());
+            // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
         }
 
         // 4. 生成正式Token
@@ -313,8 +311,8 @@ public class AuthService {
                 .city(user.getCity())
                 .district(user.getDistrict())
                 .role(user.getRole() != null ? user.getRole().getCode() : "USER")
-                .membershipLevel(user.getMembershipLevel() != null ? user.getMembershipLevel().getCode() : null)
-                .membershipLevelName(user.getMembershipLevel() != null ? user.getMembershipLevel().getDescription() : null)
+                .membershipLevel(levelInfo != null ? levelInfo.getLevelCode() : "PLUS")
+                .membershipLevelName(levelInfo != null ? levelInfo.getLevelName() : "普通会员")
                 .build();
     }
 

+ 118 - 64
zhijiayun-user/src/main/java/com/xuekairui/user/service/MembershipService.java

@@ -17,11 +17,14 @@ import com.xuekairui.user.mapper.UserMapper;
 import com.xuekairui.user.mapper.UserMembershipMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -39,10 +42,24 @@ public class MembershipService {
 
     /**
      * 获取用户当前生效的会员信息
+     * <p>无活跃会员记录时,回退到用户表的当前等级(默认PLUS普通会员)
      */
     public MembershipResponse getMyMembership(Long userId) {
         UserMembership membership = userMembershipMapper.selectActiveByUserId(userId);
-        return toResponse(membership);
+        if (membership != null) {
+            return toResponse(membership);
+        }
+        // 无活跃会员记录,默认为免费普通会员(PLUS)
+        return MembershipResponse.builder()
+                .userId(userId)
+                .level(MembershipLevel.PLUS.getCode())
+                .levelName(MembershipLevel.PLUS.getDescription())
+                .source("SYSTEM")
+                .effectiveFrom(null)
+                .effectiveTo(null)
+                .status(1)
+                .remark("免费会员")
+                .build();
     }
 
     /**
@@ -120,70 +137,78 @@ public class MembershipService {
     }
 
     /**
-     * 新用户注册赠送会员
-     * 有邀请码:赠送2个月普通会员;无邀请码:赠送1个月普通会员
+     * 统一发放或续期会员(互斥模式)
+     * <p>
+     * 如果用户已有同等级活跃会员,续期(叠加天数);
+     * 否则停用所有旧会员,创建新记录。
      */
     @Transactional
-    public void grantNewUserMembership(Long userId, String inviteCode) {
+    public void grantOrExtendMembership(Long userId, MembershipLevel level, int days,
+                                         String source, String remark) {
         User user = userMapper.selectById(userId);
         if (user == null) {
             return;
         }
 
-        int months = (inviteCode != null && !inviteCode.isBlank()) ? 2 : 1;
-        LocalDateTime now = LocalDateTime.now();
-        UserMembership membership = UserMembership.builder()
-                .userId(userId)
-                .level(MembershipLevel.PLUS)
-                .source("NEW_USER")
-                .effectiveFrom(now)
-                .effectiveTo(now.plusMonths(months))
-                .status(1)
-                .remark("新用户注册赠送" + months + "个月" + (inviteCode != null ? "(邀请码:" + inviteCode + ")" : ""))
-                .build();
-        userMembershipMapper.insert(membership);
-        user.setMembershipLevel(MembershipLevel.PLUS);
-        userMapper.updateById(user);
-        log.info("新用户{}赠送{}个月普通会员", userId, months);
+        UserMembership existing = userMembershipMapper.selectActiveByUserId(userId);
+        if (existing != null && existing.getLevel() == level) {
+            // 同等级续期:在原到期时间基础上追加
+            LocalDateTime base = existing.getEffectiveTo() != null
+                    && existing.getEffectiveTo().isAfter(LocalDateTime.now())
+                    ? existing.getEffectiveTo()
+                    : LocalDateTime.now();
+            existing.setEffectiveTo(base.plusDays(days));
+            userMembershipMapper.updateById(existing);
+        } else {
+            // 互斥:停用所有旧会员
+            List<UserMembership> activeList = userMembershipMapper.selectList(
+                    new LambdaQueryWrapper<UserMembership>()
+                            .eq(UserMembership::getUserId, userId)
+                            .eq(UserMembership::getStatus, 1));
+            for (UserMembership active : activeList) {
+                active.setStatus(0);
+                userMembershipMapper.updateById(active);
+            }
+
+            LocalDateTime now = LocalDateTime.now();
+            UserMembership membership = UserMembership.builder()
+                    .userId(userId)
+                    .level(level)
+                    .source(source)
+                    .effectiveFrom(now)
+                    .effectiveTo(now.plusDays(days))
+                    .status(1)
+                    .remark(remark)
+                    .build();
+            userMembershipMapper.insert(membership);
+        }
+
+        // 更新用户当前等级(仅当新等级更高时)
+        if (user.getMembershipLevel() == null
+                || user.getMembershipLevel().getWeight() < level.getWeight()) {
+            user.setMembershipLevel(level);
+            userMapper.updateById(user);
+        }
     }
 
     /**
-     * 邀请奖励:邀请人获赠1个月普通会员
+     * 邀请人奖励:赠送1个月(30天)高级会员(PRO)
+     * 无论邀请人当前会员身份,统一赠送高级会员时长
      */
     @Transactional
     public void grantInviteRewardMembership(Long inviterId, Long inviteeId) {
-        User inviter = userMapper.selectById(inviterId);
-        if (inviter == null) {
-            return;
-        }
-
-        UserMembership membership = UserMembership.builder()
-                .userId(inviterId)
-                .level(MembershipLevel.PLUS)
-                .source("INVITE")
-                .effectiveFrom(LocalDateTime.now())
-                .effectiveTo(LocalDateTime.now().plusMonths(1))
-                .status(1)
-                .remark("邀请奖励:邀请用户" + inviteeId + "注册")
-                .build();
-        userMembershipMapper.insert(membership);
-
-        // 更新用户当前等级为PLUS(如果当前低于PLUS)
-        if (inviter.getMembershipLevel() == null
-                || inviter.getMembershipLevel().getWeight() < MembershipLevel.PLUS.getWeight()) {
-            inviter.setMembershipLevel(MembershipLevel.PLUS);
-            userMapper.updateById(inviter);
-        }
-        log.info("邀请奖励:用户{}获得1个月普通会员", inviterId);
+        grantOrExtendMembership(inviterId, MembershipLevel.PRO, 30,
+                "INVITE", "邀请奖励:邀请用户" + inviteeId + "注册");
+        log.info("邀请奖励:用户{}获得30天高级会员", inviterId);
     }
 
     /**
-     * 支付购买激活会员(叠加模式:不覆盖低级别会员
+     * 支付购买激活会员(互斥模式:同等级续期,不同等级替换)
      * @param userId       用户ID
      * @param level        会员等级
-     * @param durationDays 有效期天数
+     * @param durationDays 有效期天数(月卡30天,年卡365天)
      * @param orderNo      关联支付订单号
-     * @return 创建的会员记录
+     * @return 创建或续期的会员记录
      */
     @Transactional
     public UserMembership activateFromPayment(Long userId, MembershipLevel level,
@@ -193,23 +218,7 @@ public class MembershipService {
             throw new BusinessException(ErrorCode.USER_NOT_FOUND);
         }
 
-        // 叠加模式:保留低级别会员,只停用同级别或更低级别的旧会员
-        // 高级别的会员总是优先使用,到期后自动回退到低级别
-        LocalDateTime now = LocalDateTime.now();
-        UserMembership membership = UserMembership.builder()
-                .userId(userId)
-                .level(level)
-                .source("RECHARGE")
-                .effectiveFrom(now)
-                .effectiveTo(now.plusDays(durationDays))
-                .status(1)
-                .remark("支付购买: " + orderNo)
-                .build();
-        userMembershipMapper.insert(membership);
-
-        // 同步当前最高等级到用户表
-        user.setMembershipLevel(level);
-        userMapper.updateById(user);
+        grantOrExtendMembership(userId, level, durationDays, "RECHARGE", "支付购买: " + orderNo);
 
         // 审计日志
         auditLogService.logSuccess(userId, user.getNickname(), "USER",
@@ -217,7 +226,9 @@ public class MembershipService {
                 "PAY:" + orderNo + "->" + level.getCode(), userId);
 
         log.info("用户{}通过支付{}激活会员等级{}, 有效期{}天", userId, orderNo, level.getCode(), durationDays);
-        return membership;
+
+        // 返回当前活跃会员记录
+        return userMembershipMapper.selectActiveByUserId(userId);
     }
 
     /**
@@ -281,4 +292,47 @@ public class MembershipService {
             default -> type;
         };
     }
+
+    /**
+     * 定时任务:每5分钟检查过期会员,自动降级
+     * <p>
+     * 高级会员全部时长到期后,账号降级为免费普通会员(PLUS)。
+     */
+    @Scheduled(cron = "0 */5 * * * ?")
+    @Transactional
+    public void expireMemberships() {
+        // 查找所有已过期但仍标记为有效的会员记录
+        List<UserMembership> expired = userMembershipMapper.selectList(
+                new LambdaQueryWrapper<UserMembership>()
+                        .eq(UserMembership::getStatus, 1)
+                        .isNotNull(UserMembership::getEffectiveTo)
+                        .lt(UserMembership::getEffectiveTo, LocalDateTime.now()));
+
+        if (expired.isEmpty()) {
+            return;
+        }
+
+        Set<Long> affectedUserIds = new HashSet<>();
+        for (UserMembership m : expired) {
+            m.setStatus(0);
+            userMembershipMapper.updateById(m);
+            affectedUserIds.add(m.getUserId());
+        }
+
+        // 对每个受影响的用户,检查是否还有其他有效会员
+        for (Long userId : affectedUserIds) {
+            UserMembership active = userMembershipMapper.selectActiveByUserId(userId);
+            if (active == null) {
+                // 无有效会员,降级为普通会员
+                User user = userMapper.selectById(userId);
+                if (user != null && user.getMembershipLevel() != MembershipLevel.PLUS) {
+                    user.setMembershipLevel(MembershipLevel.PLUS);
+                    userMapper.updateById(user);
+                    log.info("用户{}会员已到期,降级为普通会员", userId);
+                }
+            }
+        }
+
+        log.info("处理过期会员记录: {}条,涉及用户: {}个", expired.size(), affectedUserIds.size());
+    }
 }

+ 19 - 19
zhijiayun-user/src/main/java/com/xuekairui/user/service/UserLevelService.java

@@ -8,9 +8,11 @@ import com.xuekairui.user.dto.UserLevelResponse;
 import com.xuekairui.user.entity.MembershipBenefit;
 import com.xuekairui.user.entity.User;
 import com.xuekairui.user.entity.UserLevel;
+import com.xuekairui.user.entity.UserMembership;
 import com.xuekairui.user.enums.MembershipLevel;
 import com.xuekairui.user.mapper.MembershipBenefitMapper;
 import com.xuekairui.user.mapper.UserMapper;
+import com.xuekairui.user.mapper.UserMembershipMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -35,6 +37,7 @@ public class UserLevelService {
     private final UserMapper userMapper;
     private final MembershipService membershipService;
     private final MembershipBenefitMapper membershipBenefitMapper;
+    private final UserMembershipMapper userMembershipMapper;
 
     /**
      * 会员等级与兼容等级对象的映射(id 仅用于兼容旧接口)
@@ -164,14 +167,21 @@ public class UserLevelService {
     /**
      * 解析用户对应的兼容等级对象
      * <p>
-     * 静态元数据来自 LEVEL_MAP,具体配额从 t_membership_benefit 动态读取;
-     * 若数据库中未配置权益,则使用硬编码兜底值,保证系统可运行。
+     * 优先从数据库查询用户当前有效的会员记录;
+     * 如果没有有效会员(含邀请、付费等记录),默认为普通会员(PLUS)。
+     * 普通会员免费开放,注册即拥有。
      */
     private UserLevel resolveUserLevel(User user) {
-        MembershipLevel level = (user != null && user.getMembershipLevel() != null)
-                ? user.getMembershipLevel()
-                : MembershipLevel.PLUS;
-        return buildUserLevelWithBenefits(level);
+        if (user == null) {
+            return buildUserLevelWithBenefits(MembershipLevel.PLUS);
+        }
+        // 查询用户当前有效的会员记录(PRO/ULTRA 等付费或赠送的会员)
+        UserMembership activeMembership = userMembershipMapper.selectActiveByUserId(user.getId());
+        if (activeMembership != null && activeMembership.getLevel() != null) {
+            return buildUserLevelWithBenefits(activeMembership.getLevel());
+        }
+        // 无有效会员记录,默认为普通会员(PLUS)
+        return buildUserLevelWithBenefits(MembershipLevel.PLUS);
     }
 
     /**
@@ -234,22 +244,12 @@ public class UserLevelService {
     /**
      * 构建指定等级的定价方案列表
      * <p>
-     * TODO: 后续从 t_payment_plan 表动态读取,当前硬编码与 schema.sql 保持一致
+     * 普通会员(PLUS)免费开放,无付费套餐;
+     * 高级会员(PRO)提供月卡(30天)和年卡(365天)。
      */
     private List<LevelPricingPlan> buildPricingPlans(String levelCode) {
         return switch (levelCode) {
-            case "PLUS" -> List.of(
-                    LevelPricingPlan.builder()
-                            .planCode("MONTHLY_PLUS").planName("普通会员·月卡")
-                            .price(new BigDecimal("9.90")).originalPrice(new BigDecimal("19.90"))
-                            .durationDays(30).description("每天有效查询10次 · 1个月9.9(原价19.9)")
-                            .build(),
-                    LevelPricingPlan.builder()
-                            .planCode("YEARLY_PLUS").planName("普通会员·年卡")
-                            .price(new BigDecimal("99.00")).originalPrice(new BigDecimal("199.90"))
-                            .durationDays(365).description("每天有效查询10次 · 1年99(原价199.9)")
-                            .build()
-            );
+            case "PLUS" -> Collections.emptyList();
             case "PRO" -> List.of(
                     LevelPricingPlan.builder()
                             .planCode("MONTHLY_PRO").planName("高级会员·月卡")