Przeglądaj źródła

优化代码,添加接口

liuchengsen 1 miesiąc temu
rodzic
commit
8f8ca45582

+ 38 - 13
docs/01-产品文档.md

@@ -6,7 +6,7 @@
 > **产品形态**:**多端交付策略** - MVP首选Windows独立桌面应用(.exe),后续扩展微信小程序、APP、公众号等多渠道。  
 > **目标用户**:药店采购人员、药师,目标日活20万+。
 >
-> 📅 版本:v1.1-MVP | 📆 更新日期:2026-06-25 | ✅ 状态:开发中
+> 📅 版本:v1.1-MVP | 📆 更新日期:2026-07-01 | ✅ 状态:开发中
 
 ---
 
@@ -237,11 +237,7 @@
 #### 分享文案示例
 
 ```
-邀请你加入智价云药店
-高效药品采购比价工具
-邀请码:ABC123
-点击链接下载:
-https://app.zhijiayun.com/invite/ABC123
+我是用户6917,在这里发现了一个药店采购神器——智价云(药店端)!它聚合比价功能特别方便,能快速查到最低价,帮你节省采购成本。下载链接https://priceapi.kailin.com.cn/api/invite/2XKADABS填我的邀请码 2XKADABS 完成注册,你也会获得会员权益!
 ```
 
 #### 邀请统计
@@ -258,6 +254,8 @@ https://app.zhijiayun.com/invite/ABC123
 | GET /api/invite/stats | 获取邀请统计(邀请人数、注册人数、累计奖励) |
 | GET /api/invite/rewards | 获取邀请记录列表(含注册状态) |
 | GET /api/invite/extra-quota | 获取可用额外爬虫次数 |
+| GET /api/invite/my-inviter | 查询我的邀请人 |
+| POST /api/invite/bind | 补填邀请码 |
 | GET /api/invite/page/{code}?channel=windows | 邀请落地页数据(公开,无需登录) |
 | GET /invite/{code}?channel=wechat | 邀请链接直接访问入口(公开,自动计入点击数) |
 | POST /api/invite/click/{code} | 追踪邀请链接被打开(公开) |
@@ -319,11 +317,14 @@ https://app.zhijiayun.com/invite/ABC123
 | 查询订单 | GET /api/payment/order/{orderNo} | 查询单个订单状态 |
 | 我的订单 | GET /api/payment/orders | 查询用户所有订单 |
 | 模拟支付 | POST /api/payment/order/{orderNo}/pay | 开发测试用模拟支付(当前仅支持此模式) |
+| 切换渠道 | POST /api/payment/order/{orderNo}/switch-channel | 切换支付渠道并重新生成二维码 |
 | 微信回调 | POST /api/payment/callback/wechat | 微信支付异步通知(公开,当前仅模拟模式) |
 | 支付宝回调 | POST /api/payment/callback/alipay | 支付宝异步通知(公开,当前仅模拟模式) |
 
 **支付渠道**:微信支付(Native扫码)、支付宝(当面付/扫码)
 
+**重复支付自动退款**:用户切换支付渠道后,多个渠道都可能支付成功。系统以第一次支付成功为准激活会员,后续重复支付自动记录到 `t_payment_order_extra_payment` 表,并原路退款(微信退款API v3 / 支付宝退款接口)。退款状态记录为 PENDING → SUCCESS/FAILED,支持退款失败原因追踪。
+
 > **⚠️ 当前状态**:生产环境微信/支付宝回调接口为占位实现(含TODO),当前仅模拟支付模式可用。上线前需接入微信支付SDK和支付宝SDK完成验签与解密。
 
 **支付方案(初始套餐)**:
@@ -344,15 +345,23 @@ https://app.zhijiayun.com/invite/ABC123
 | PUT /api/admin/payment/plans/{id} | 更新支付方案 |
 | PUT /api/admin/payment/plans/{id}/status | 上架/下架方案 |
 
-### 2.10 搜索历史与关注列表
+### 2.10 平台会话管理
+
+| 接口 | 说明 |
+|------|------|
+| PUT /api/platform-session/{platformCode} | 更新平台会话 |
+| GET /api/platform-session/list | 查询平台会话列表 |
+| DELETE /api/platform-session/{platformCode} | 删除平台会话 |
+
+### 2.11 搜索历史与关注列表
 
 #### 搜索历史
 
 | 接口 | 说明 |
 |------|------|
-| GET /api/search/history | 获取用户搜索历史 |
-| DELETE /api/search/history/{id} | 删除指定搜索记录 |
-| DELETE /api/search/history | 清空搜索历史 |
+| GET /api/search-record/history | 获取用户搜索历史 |
+| DELETE /api/search-record/history/{id} | 删除指定搜索记录 |
+| DELETE /api/search-record/history | 清空搜索历史 |
 
 #### 关注/收藏列表
 
@@ -365,7 +374,7 @@ https://app.zhijiayun.com/invite/ABC123
 
 **关注信息**:药品名称、规格、厂家、关注时最低价、最低价平台、上次查询价格、价格变动、最后检查时间。
 
-### 2.11 运营管理后台
+### 2.12 运营管理后台
 
 | 功能 | 接口 |
 |------|------|
@@ -387,7 +396,7 @@ https://app.zhijiayun.com/invite/ABC123
 | 操作审计日志 | GET /api/admin/audit-log/*(详见下方) |
 | 邀请转化统计 | GET /api/admin/invite/conversion-stats?userId=1 |
 
-### 2.12 操作审计日志系统
+### 2.13 操作审计日志系统
 
 系统对所有关键操作进行审计记录,支持追溯:
 
@@ -470,6 +479,22 @@ https://app.zhijiayun.com/invite/ABC123
 → 自动发放对应会员等级 → 配额立即生效
 ```
 
+### 3.6 重复支付与自动退款流程
+
+```
+用户创建订单(渠道A)→ 切换渠道为B → 生成渠道B二维码
+→ 渠道A先支付成功 → 订单标记为PAID,激活会员
+→ 渠道B也支付成功 → 回调到达,发现订单已PAID
+→ 记录重复支付到 t_payment_order_extra_payment(幂等:同交易号不重复记录)
+→ 自动发起原路退款(渠道B原路退回)
+→ 退款状态更新:PENDING → SUCCESS/FAILED
+```
+
+**退款渠道**:
+- 微信:调用 WxPayService.refundV3()(API v3 退款接口)
+- 支付宝:调用 AlipayClient.execute(AlipayTradeRefundRequest)(统一收单交易退款)
+- 模拟模式:直接返回模拟退款单号,不调用真实SDK
+
 ---
 
 ## 4. 多端交付策略
@@ -630,7 +655,7 @@ https://app.zhijiayun.com/invite/ABC123
 - [x] 会员体系(PLUS/PRO/ULTRA三档,权益驱动配额)
 - [x] 爬虫配额管理(日/月/年三周期 + 额外配额 + 优惠券)
 - [x] 邀请裂变系统(多渠道落地页、URL Scheme自动填入邀请码、会员时长奖励)
-- [x] 支付系统(模拟支付、支付方案管理;微信/支付宝回调为占位实现,待接入SDK)
+- [x] 支付系统(模拟支付、支付方案管理、渠道切换、重复支付自动退款、签约协议、安心付;微信/支付宝回调为占位实现,待接入SDK)
 - [x] 体验配额管理(运营可配置天数、查询次数、关注品种数)
 - [x] 优惠券系统(批量发放、配额消耗优先使用)
 - [x] 运营管理后台(配置管理、执照审核、数据统计、会员管理、支付方案管理)

+ 91 - 34
docs/02-架构文档.md

@@ -4,7 +4,7 @@
 > 本项目为**全新项目,尚未上线**,当前处于 **MVP(最小可行产品)阶段**。  
 > 本文档描述的是 MVP 版本的架构设计,后续将根据业务发展和技术演进持续优化。
 >
-> 版本:v1.1-MVP | 更新日期:2026-06-25 | 状态:开发中
+> 版本:v1.1-MVP | 更新日期:2026-07-01 | 状态:开发中
 
 ---
 
@@ -35,7 +35,7 @@ zhijiayun-pharmacy (父工程)
 ├── zhijiayun-common    # 公共模块:Result、ErrorCode、BusinessException、TraceIdGenerator、ValidateUtil
 ├── zhijiayun-user      # 用户模块:认证、会员、优惠券、爬虫配额、搜索、关注、营业执照、审计日志
 ├── zhijiayun-invite    # 邀请模块:邀请码、裂变、会员奖励、运营配置
-├── zhijiayun-payment   # 支付模块:微信/支付宝支付、订单管理、套餐方案、二维码生成
+├── zhijiayun-payment   # 支付模块:微信/支付宝支付、订单管理、套餐方案、二维码生成、重复支付自动退款
 ├── zhijiayun-gateway   # 网关模块:聚合启动、请求日志、Redis限流、全局异常处理
 └── zhijiayun-test      # 测试模块:集成测试页面、端到端测试
 ```
@@ -65,21 +65,24 @@ com.xuekairui
 │   └── ValidateUtil.java      # 参数校验工具(IP提取等)
 ├── user/                      # zhijiayun-user
-│   ├── config/                # 配置类
+│   ├── config/                # 配置类(6个)
 │   │   ├── AliyunSmsProperties.java
+│   │   ├── AsyncConfig.java           # 异步任务线程池配置
 │   │   ├── MyBatisPlusMetaHandler.java
 │   │   ├── RedisConfig.java
-│   │   └── SecurityConfig.java  # Spring Security + RBAC
-│   ├── controller/            # 控制器(18个)
+│   │   ├── SecurityConfig.java  # Spring Security + RBAC
+│   │   └── SuperAdminInitializer.java # 超级管理员初始化
+│   ├── controller/            # 控制器(19个)
 │   │   ├── AuthController.java           # 认证(短信/密码/微信登录、Token刷新、用户信息)
 │   │   ├── CrawlerController.java        # 爬虫配额状态、消耗、日志
 │   │   ├── CouponController.java         # 优惠券发放、查询
 │   │   ├── UserLevelController.java      # 等级列表、用户等级设置
 │   │   ├── MembershipController.java     # 会员信息查询、权益查询
 │   │   ├── DrugSearchController.java     # 药品搜索比价、搜索建议
-│   │   ├── SearchHistoryController.java  # 搜索历史管理
+│   │   ├── SearchRecordController.java   # 搜索历史管理
 │   │   ├── WatchlistController.java      # 关注/收藏列表管理
 │   │   ├── PlatformAccountController.java # B2B平台账号绑定
+│   │   ├── PlatformSessionController.java # 平台会话管理
 │   │   ├── CrawlerPlatformConfigController.java # 平台配置查询
 │   │   ├── BusinessLicenseController.java # 营业执照上传
 │   │   ├── FileUploadController.java     # 文件上传
@@ -89,41 +92,56 @@ com.xuekairui
 │   │   ├── AdminLicenseController.java   # 营业执照审核
 │   │   ├── AdminTrialQuotaConfigController.java # 体验配额配置管理
 │   │   └── OperationAuditLogController.java # 审计日志查询
-│   ├── dto/                   # 数据传输对象(30+
-│   ├── entity/                # 数据库实体(16个)
+│   ├── dto/                   # 数据传输对象(34个
+│   ├── entity/                # 数据库实体(17个)
 │   ├── enums/                 # 枚举(MembershipLevel、UserRole、OperationType)
 │   ├── event/                 # 事件(UserRegisteredEvent)
-│   ├── mapper/                # MyBatis-Plus Mapper(15个)
+│   ├── mapper/                # MyBatis-Plus Mapper(16个)
 │   ├── security/              # JWT过滤器
-│   ├── service/               # 业务逻辑(18个服务)
+│   ├── service/               # 业务逻辑(19个服务)
 │   └── util/                  # 工具类
 ├── invite/                    # zhijiayun-invite
-│   ├── controller/
-│   │   ├── InviteController.java  # 邀请码、统计、记录、落地页
+│   ├── controller/            # 控制器(2个)
+│   │   ├── InviteController.java  # 邀请码、统计、记录、落地页、解析、下载
 │   │   └── AdminController.java   # 邀请配置、爬虫发放、记录查看
-│   ├── dto/
-│   ├── entity/
+│   ├── dto/                   # 数据传输对象(9个)
+│   ├── entity/                # 数据库实体(3个:InviteCode、InviteConfig、InviteRelation)
 │   ├── listener/              # UserRegisteredListener(事件驱动)
-│   ├── mapper/
+│   ├── mapper/                # MyBatis-Plus Mapper(3个)
 │   └── service/
 │       ├── InviteService.java       # 邀请核心服务
 │       └── InviteConfigService.java # 邀请配置服务
 ├── payment/                   # zhijiayun-payment
-│   ├── config/                # PaymentConfig、PaymentProperties
-│   ├── controller/
-│   │   ├── PaymentController.java        # 支付方案、创建订单、查询订单
+│   ├── config/                # PaymentConfig、PaymentProperties(2个)
+│   ├── controller/            # 控制器(6个)
+│   │   ├── PaymentController.java        # 支付方案、创建订单、查询订单、切换渠道
 │   │   ├── PaymentCallbackController.java # 微信/支付宝回调
-│   │   └── AdminPaymentController.java   # 支付方案管理
-│   ├── dto/
-│   ├── entity/                # PaymentOrder、PaymentPlan
-│   ├── enums/                 # PaymentChannel、PaymentStatus
-│   ├── mapper/
+│   │   ├── AdminPaymentController.java   # 支付方案管理
+│   │   ├── AgreementController.java     # 签约协议管理
+│   │   ├── AlipayAnxinPayController.java # 安心付管理
+│   │   └── AlipayOpenController.java     # 支付宝开放能力回调
+│   ├── dto/                   # 数据传输对象(11个)
+│   ├── entity/                # 数据库实体(7个)
+│   ├── enums/                 # 枚举(5个:PaymentChannel、PaymentStatus、AgreementStatus、OrderType、PeriodType)
+│   ├── mapper/                # MyBatis-Plus Mapper(8个)
+│   │   ├── PaymentOrderMapper.java
+│   │   ├── PaymentOrderExtraPaymentMapper.java  # 重复支付记录
+│   │   ├── PaymentAgreementMapper.java   # 签约协议
+│   │   ├── PaymentPlanMapper.java        # 支付方案
+│   │   ├── AlipayAnxinCardMapper.java    # 安心付卡模板
+│   │   ├── AlipayAnxinOrderRecordMapper.java # 安心付订单
+│   │   └── AlipayCallbackRecordMapper.java # 支付宝回调记录
 │   └── service/
-│       ├── PaymentOrderService.java  # 订单管理
-│       ├── PaymentPlanService.java   # 方案管理
-│       └── QRCodeService.java        # 二维码生成
+│       ├── PaymentOrderService.java    # 订单管理(含重复支付处理)
+│       ├── PaymentPlanService.java     # 方案管理
+│       ├── PaymentRefundService.java   # 退款服务(微信/支付宝原路退款)
+│       ├── PaymentAgreementService.java # 签约协议服务
+│       ├── AlipayAnxinPayService.java   # 安心付服务
+│       ├── AlipayCallbackRecordService.java # 支付宝回调记录服务
+│       ├── AutoDeductScheduler.java    # 自动续费定时扣款任务(每10分钟)
+│       └── QRCodeService.java          # 二维码生成
 └── gateway/                   # zhijiayun-gateway
     ├── Application.java       # 启动类
@@ -156,13 +174,20 @@ com.xuekairui
 | 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_payment_order | 支付订单表 | order_no, user_id, plan_id, amount(DECIMAL), channel(WECHAT/ALIPAY), status(PENDING/PAID/EXPIRED/REFUNDED), qr_code_url, trade_no, paid_channel |
+| t_payment_order_extra_payment | 重复支付记录表 | order_no, channel, trade_no, amount(DECIMAL), paid_time, refund_status(PENDING/SUCCESS/FAILED), refund_trade_no, refund_time, refund_fail_reason |
+| t_payment_agreement | 签约协议表 | user_id, plan_id, channel, period_type(MONTH/YEAR), period_amount, status(PENDING/SIGNED/UNSIGNED/FAILED), agreement_no, next_deduct_time |
+| t_alipay_anxin_card | 安心付卡模板表 | plan_id, card_id, card_name, card_type, period_amount, period_days, total_periods, status, appointment_url |
+| t_alipay_anxin_order_record | 安心付订单记录表 | user_id, plan_id, card_id, order_id, sub_order_id, out_biz_no, deduct_amount, deduct_status, deduct_time, membership_activated, order_no |
+| t_alipay_callback_record | 支付宝开放能力回调记录表 | type, app_id, msg_method, auth_code, source, state, params, status, error_msg |
+| t_platform_session | 平台会话快照表 | user_id, platform_code, platform_name, login_url, account_label, cookies_encrypted, version, expires_at |
+| t_platform_account | B2B平台账号绑定表 | user_id, platform_code, platform_name, account_encrypted, password_encrypted, enabled, verify_status |
+| t_search_record | 搜索记录表(完整比价快照) | user_id, drug_name, searched_at, status_text, platform_results, search_log |
 | 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_search_history | 搜索历史表(轻量关键词日志,无Java实体) | 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关系
@@ -176,13 +201,19 @@ t_user ──1:N──> t_crawler_usage_log (使用记录)
 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_search_history (搜索历史/关键词日志)
+t_user ──1:N──> t_search_record (搜索记录/比价快照)
 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 (方案-订单)
+t_payment_order ──1:N──> t_payment_order_extra_payment (订单-重复支付记录)
+t_payment_plan ──1:N──> t_payment_agreement (方案-签约协议)
+t_user ──1:N──> t_payment_agreement (用户-签约协议)
+t_user ──1:N──> t_platform_session (用户-平台会话)
+t_payment_plan ──1:N──> t_alipay_anxin_card (方案-安心付卡模板)
 ```
 
 ---
@@ -216,19 +247,27 @@ t_payment_plan ──1:N──> t_payment_order (方案-订单)
 | 公开 | /api/auth/wechat/** | 微信登录相关 |
 | 公开 | /api/invite/page/**, /api/invite/click/**, /api/invite/{code} | 邀请落地页 |
 | 公开 | /api/invite/resolve-link, /api/invite/validate | 邀请码解析/验证 |
+| 公开 | /invite/** | 浏览器直接访问邀请链接(公开) |
 | 公开 | /api/platform-config/** | 平台配置查询 |
 | 公开 | /api/trial-quota/** | 体验配额查询 |
 | 公开 | /api/search/suggestions | 搜索建议 |
 | 公开 | /api/payment/plans | 支付方案列表 |
 | 公开 | /api/payment/callback/** | 支付回调 |
+| 公开 | /api/payment/anxin/notify/** | 安心付通知 |
+| 公开 | /api/payment/agreement/notify/** | 签约回调通知 |
+| 公开 | /api/payment/alipay/** | 支付宝开放能力回调 |
 | 公开 | /api/level/list, /api/level/{id} | 等级列表 |
 | 认证 | /api/auth/user/**, /api/auth/password/set, /api/auth/phone/change | 用户信息 |
 | 认证 | /api/crawler/**, /api/search/query | 爬虫/搜索 |
+| 认证 | /api/platform-session/** | 平台会话管理 |
+| 认证 | /api/search-record/** | 搜索历史 |
 | 认证 | /api/invite/code, /api/invite/stats, /api/invite/rewards | 邀请操作 |
+| 认证 | /api/invite/extra-quota, /api/invite/bind, /api/invite/my-inviter | 邀请操作 |
 | 认证 | /api/coupon/**, /api/membership/** | 优惠券/会员 |
 | 认证 | /api/platform-account/**, /api/watchlist/** | 账号绑定/关注 |
-| 认证 | /api/payment/order/** | 支付订单 |
+| 认证 | /api/payment/order/**, /api/payment/agreement/** | 支付订单/签约 |
 | 管理 | /api/admin/** (ADMIN + SUPER_ADMIN) | 运营管理 |
+| 管理 | /api/admin/payment/anxin/** (ADMIN + SUPER_ADMIN) | 安心付管理 |
 | 超管 | /api/admin/users/** (仅SUPER_ADMIN) | 用户角色管理 |
 | 管理 | /api/coupon/issue, /api/level/user/** (ADMIN + SUPER_ADMIN) | 散落管理操作 |
 
@@ -309,20 +348,37 @@ t_payment_plan ──1:N──> t_payment_order (方案-订单)
 → 订单状态更新为PAID → 自动发放会员等级 → 配额生效
 ```
 
-### 6.2 支付渠道
+### 6.2 渠道切换与重复支付处理
+
+```
+用户创建订单(渠道A)→ 切换为渠道B → 生成渠道B二维码
+→ 渠道A先回调 → handlePaymentSuccess → 订单PAID,激活会员
+→ 渠道B也回调 → processPaidCallback 检查到已PAID
+→ handleDuplicatePayment(幂等:同交易号不重复处理)
+→ 记录到 t_payment_order_extra_payment(refund_status=PENDING)
+→ doRefund() → PaymentRefundService.refund()
+  ├── 微信 → WxPayService.refundV3()
+  └── 支付宝 → AlipayClient.execute(AlipayTradeRefundRequest)
+→ 退款成功:refund_status=SUCCESS, refund_trade_no=xxx
+→ 退款失败:refund_status=FAILED, refund_fail_reason=xxx
+```
+
+**退款服务**:`PaymentRefundService` 封装微信/支付宝原路退款逻辑,mock模式下模拟退款成功。
+
+### 6.3 支付渠道
 
 | 渠道 | 编码 | 回调地址 | 说明 |
 |------|------|----------|------|
 | 微信支付 | WECHAT | /api/payment/callback/wechat | Native扫码支付(API v3) |
 | 支付宝 | ALIPAY | /api/payment/callback/alipay | 当面付/扫码支付(RSA2验签) |
 
-### 6.3 模拟支付
+### 6.4 模拟支付
 
 开发环境(`payment.mock=true`)支持模拟支付:
 - POST /api/payment/order/{orderNo}/pay 直接标记订单为已支付
 - 回调接口接收模拟通知,跳过验签
 
-### 6.4 生产环境接入
+### 6.5 生产环境接入
 
 生产环境需配置:
 - 微信支付:app-id、mch-id、api-v3-key、private-key-path、serial-no、notify-url
@@ -346,6 +402,7 @@ t_payment_plan ──1:N──> t_payment_order (方案-订单)
 ### 7.3 支付与会员联动
 
 - 支付成功回调 → `PaymentOrderService.processPaidCallback` → 调用 `MembershipService` 发放对应会员等级
+- 重复支付回调 → `handleDuplicatePayment` → `PaymentRefundService.refund()` → 原路退款(微信/支付宝)
 
 ---
 

Plik diff jest za duży
+ 721 - 253
docs/03-接口文档.md


+ 49 - 6
docs/05-测试文档.md

@@ -4,7 +4,7 @@
 > 本项目为**全新项目,尚未上线**,当前处于 **MVP(最小可行产品)阶段**。  
 > 本文档记录的是 MVP 版本的测试用例和覆盖率目标,后续会持续补充和完善。
 >
-> 版本:v1.0-MVP | 更新日期:2026-06-16 | 状态:开发中
+> 版本:v1.1-MVP | 更新日期:2026-07-01 | 状态:开发中
 
 ---
 
@@ -88,6 +88,24 @@
 | LVL-02 | GET /api/level/my - 获取当前用户等级 | 200 |
 | LVL-03 | GET /api/level/{id} - 获取等级详情 | 200 |
 
+### 2.5 支付模块测试 (PaymentControllerTest)
+
+| 编号 | 用例 | 预期结果 |
+|------|------|----------|
+| PAY-01 | GET /api/payment/plans - 无需认证返回方案列表 | 200, 方案数组 |
+| PAY-02 | GET /api/payment/plans - 方案含价格、会员等级等字段 | 200, price/membershipLevel存在 |
+| PAY-03 | POST /api/payment/order/create - 微信支付返回二维码 | 200, channel=WECHAT, qrCodeBase64存在 |
+| PAY-04 | POST /api/payment/order/create - 支付宝返回二维码 | 200, channel=ALIPAY |
+| PAY-05 | POST /api/payment/order/create - 无效渠道返回错误 | 200, code=2004 |
+| PAY-06 | POST /api/payment/order/create - 无Token返回401 | 401 |
+| PAY-07 | GET /api/payment/order/{orderNo} - 查询存在订单 | 200, 返回详情 |
+| PAY-08 | GET /api/payment/order/{orderNo} - 不存在订单返回错误 | 200, code=2001 |
+| PAY-09 | POST /api/payment/order/{orderNo}/pay - 模拟支付成功 | 200, status=PAID, paidTime存在 |
+| PAY-10 | POST /api/payment/order/{orderNo}/pay - 已支付订单再次支付 | 200, code=2005 |
+| PAY-11 | GET /api/payment/orders - 查询用户订单列表 | 200, data为数组 |
+| PAY-12 | POST /api/payment/callback/wechat - 缺少订单号 | 200, 返回FAIL |
+| PAY-13 | POST /api/payment/callback/alipay - 缺少订单号 | 200, 返回fail |
+
 ---
 
 ## 3. 待补充测试
@@ -103,6 +121,12 @@
 | INV-05 | GET /api/invite/rewards - 邀请记录 | P1 |
 | INV-06 | 邀请注册 → 自动发放奖励 → 验证配额增加 | P1 |
 | INV-07 | 多渠道落地页(channel=app/wechat/miniapp) | P2 |
+| INV-08 | GET /api/invite/my-inviter - 已注册用户查询邀请人信息 | P1 |
+| INV-09 | GET /api/invite/my-inviter - 未被邀请的用户返回invited=false | P1 |
+| INV-10 | POST /api/invite/bind - 补填邀请码成功 | P1 |
+| INV-11 | POST /api/invite/bind - 无效邀请码返回错误 | P1 |
+| INV-12 | POST /api/invite/bind - 已有邀请关系时拒绝重复绑定 | P2 |
+| INV-13 | 补填邀请码后 → my-inviter 返回对应邀请人信息 | P1 |
 
 ### 3.2 爬虫配额测试
 
@@ -135,6 +159,21 @@
 | CPN-03 | GET /api/coupon/my/available - 可用优惠券 | P2 |
 | CPN-04 | 爬虫消耗优先使用优惠券 | P1 |
 
+### 3.5 重复支付与退款测试
+
+| 编号 | 用例 | 优先级 |
+|------|------|--------|
+| DUP-01 | 渠道切换 - PENDING订单切换渠道成功 | P0 |
+| DUP-02 | 渠道切换 - 已支付订单切换渠道拒绝 | P1 |
+| DUP-03 | 渠道切换 - 已过期订单切换渠道拒绝 | P1 |
+| DUP-04 | 重复支付回调 - 订单已PAID时第二笔回调记录重复支付 | P0 |
+| DUP-05 | 重复支付回调 - 同一tradeNo幂等不重复记录 | P0 |
+| DUP-06 | 自动退款 - 微信渠道重复支付触发退款(mock模式) | P0 |
+| DUP-07 | 自动退款 - 支付宝渠道重复支付触发退款(mock模式) | P0 |
+| DUP-08 | 退款状态 - 退款成功后refund_status=SUCCESS | P0 |
+| DUP-09 | 退款失败 - 记录refund_fail_reason,refund_status=FAILED | P1 |
+| DUP-10 | 退款幂等 - 同一重复支付记录不重复退款 | P1 |
+
 ---
 
 ## 4. 运行测试
@@ -169,11 +208,14 @@ mvn test -pl zhijiayun-gateway -Dtest="AuthControllerTest#sendCode_invalidPhone_
 
 ### 5.1 默认等级数据
 
-| level_code | crawler_quota | monthly_quota | max_concurrent |
-|------------|---------------|---------------|----------------|
-| NORMAL | 5 | 100 | 1 |
-| VIP | 20 | 500 | 3 |
-| SVIP | 100 | 3000 | 5 |
+> **注**:旧的等级版本体系(NORMAL/VIP/SVIP)已下线,统一迁移至会员体系(PLUS/PRO/ULTRA)。
+> 爬虫配额现在由会员权益配置 + 日/月/年三周期 + 额外配额组成。
+
+| membership_level | daily_quota | monthly_quota | yearly_quota | max_concurrent |
+|------------------|-------------|---------------|--------------|----------------|
+| PLUS | 10 | 200 | 2000 | 2 |
+| PRO | 50 | 1000 | 10000 | 5 |
+| ULTRA | 200 | 5000 | 50000 | 10 |
 
 ### 5.2 测试用户
 
@@ -187,5 +229,6 @@ mvn test -pl zhijiayun-gateway -Dtest="AuthControllerTest#sendCode_invalidPhone_
 |------|-------------|---------------|
 | zhijiayun-user | 70% | 60% |
 | zhijiayun-invite | 70% | 60% |
+| zhijiayun-payment | 70% | 60% |
 | zhijiayun-gateway | 80% | 70% |
 | 整体 | 70% | 60% |

+ 20 - 3
docs/06-验收流程与标准.md

@@ -4,7 +4,7 @@
 > 本项目为**全新项目,尚未上线**,当前处于 **MVP(最小可行产品)阶段**。  
 > 本文档定义的是 MVP 版本的验收标准,正式上线前需全部通过。
 >
-> 版本:v1.1-MVP | 更新日期:2026-06-25 | 状态:开发中
+> 版本:v1.1-MVP | 更新日期:2026-07-01 | 状态:开发中
 
 ---
 
@@ -138,6 +138,20 @@
 | FA-83 | 订单过期 | 超过15分钟未支付订单自动过期 | P1 |
 | FA-84 | 创建支付方案(运营) | 运营创建套餐方案成功 | P1 |
 | FA-85 | 上下架方案(运营) | 运营上架/下架方案成功 | P1 |
+| FA-86 | 渠道切换 | PENDING订单可切换支付渠道,重新生成二维码 | P1 |
+| FA-87 | 重复支付记录 | 订单已PAID时第二笔回调记录到 t_payment_order_extra_payment | P0 |
+| FA-88 | 重复支付幂等 | 同一tradeNo不重复记录,不重复退款 | P0 |
+| FA-89 | 自动退款-微信 | 重复支付自动调用微信退款API,refund_status=SUCCESS | P0 |
+| FA-90 | 自动退款-支付宝 | 重复支付自动调用支付宝退款API,refund_status=SUCCESS | P0 |
+| FA-91 | 退款失败处理 | 退款失败时 refund_status=FAILED,记录 refund_fail_reason | P1 |
+| FA-92 | 模拟模式退款 | mock模式下重复支付模拟退款成功,不调用真实SDK | P1 |
+| FA-93 | 签约协议-发起签约 | 用户可发起微信/支付宝签约协议 | P1 |
+| FA-94 | 签约协议-查询列表 | 返回用户签约列表,含协议状态 | P1 |
+| FA-95 | 签约协议-解约 | 用户可解约,协议状态更新为UNSIGNED | P1 |
+| FA-96 | 安心付-卡模板创建 | 运营可创建安心付卡模板 | P2 |
+| FA-97 | 安心付-扣款通知 | 接收支付宝安心付扣款通知,正确匹配用户 | P1 |
+| FA-98 | 平台会话管理 | 用户可更新/查询/删除平台会话 | P2 |
+| FA-99 | 搜索历史记录 | 搜索记录保存到 /api/search-record/history | P2 |
 
 ### 2.9 运营管理模块
 
@@ -214,7 +228,8 @@
 | SA-10 | HTTPS | 生产环境启用HTTPS | 配置检查 |
 | SA-11 | 短信限流 | IP维度1分钟1次限制有效 | 接口测试 |
 | SA-12 | 支付回调验签 | 生产环境微信/支付宝回调验签通过 | 代码审查 |
-| SA-13 | CORS配置 | 跨域配置正确,不暴露敏感头 | 配置检查 |
+| SA-13 | 重复支付退款安全 | 退款仅触发一次(幂等),退款失败不影响主流程 | 代码审查 |
+| SA-14 | CORS配置 | 跨域配置正确,不暴露敏感头 | 配置检查 |
 
 ---
 
@@ -261,10 +276,12 @@
 - [ ] 平台账号绑定(绑定/解绑/验证/加密存储)
 - [ ] 搜索历史与关注列表
 - [ ] 邀请裂变系统(多渠道 + 会员时长奖励 + 智能解析)
-- [ ] 支付系统(微信/支付宝 + 套餐管理 + 模拟支付)
+- [ ] 支付系统(微信/支付宝 + 套餐管理 + 模拟支付 + 渠道切换 + 重复支付自动退款 + 签约协议 + 安心付
 - [ ] 优惠券系统
 - [ ] 运营管理后台(用户管理 + 配置管理 + 审核管理)
 - [ ] 操作审计日志系统
+- [ ] 平台会话管理(会话更新/查询/删除)
+- [ ] 搜索历史(/api/search-record/history 增删查)
 - [ ] RBAC三级角色权限控制
 - [ ] 网关层请求日志与Redis限流
 

+ 12 - 7
zhijiayun-gateway/src/main/resources/db/migration-v5.sql

@@ -15,13 +15,18 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
 
 -- 3. 支付订单重复支付记录表(切换渠道后多个渠道都支付成功时保留证据)
 CREATE TABLE IF NOT EXISTS `t_payment_order_extra_payment` (
-    `id`          BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `order_no`    VARCHAR(32)   NOT NULL COMMENT '订单号',
-    `channel`     VARCHAR(20)   NOT NULL COMMENT '实际支付渠道:WECHAT/ALIPAY',
-    `trade_no`    VARCHAR(64)   NOT NULL COMMENT '第三方交易号',
-    `amount`      DECIMAL(10,2) NOT NULL COMMENT '支付金额(元)',
-    `paid_time`   DATETIME      NOT NULL COMMENT '支付时间',
-    `create_time` DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `id`                 BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `order_no`           VARCHAR(32)   NOT NULL COMMENT '订单号',
+    `channel`           VARCHAR(20)   NOT NULL COMMENT '实际支付渠道:WECHAT/ALIPAY',
+    `trade_no`          VARCHAR(64)   NOT NULL COMMENT '第三方交易号',
+    `amount`             DECIMAL(10,2) NOT NULL COMMENT '支付金额(元)',
+    `paid_time`          DATETIME      NOT NULL COMMENT '支付时间',
+    `refund_status`     VARCHAR(20)   DEFAULT 'PENDING' COMMENT '退款状态:PENDING/SUCCESS/FAILED',
+    `refund_trade_no`   VARCHAR(64)   DEFAULT NULL COMMENT '退款交易号(第三方退款单号)',
+    `refund_time`       DATETIME      DEFAULT NULL COMMENT '退款时间',
+    `refund_fail_reason` VARCHAR(500) DEFAULT NULL COMMENT '退款失败原因',
+    `create_time`       DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time`       DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     PRIMARY KEY (`id`),
     KEY `idx_order_no` (`order_no`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单重复支付记录表';

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

@@ -302,13 +302,18 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
 
 -- 支付订单重复支付记录表(切换渠道后多个渠道都支付成功时保留证据)
 CREATE TABLE IF NOT EXISTS `t_payment_order_extra_payment` (
-    `id`          BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `order_no`    VARCHAR(32)   NOT NULL COMMENT '订单号',
-    `channel`     VARCHAR(20)   NOT NULL COMMENT '实际支付渠道:WECHAT/ALIPAY',
-    `trade_no`    VARCHAR(64)   NOT NULL COMMENT '第三方交易号',
-    `amount`      DECIMAL(10,2) NOT NULL COMMENT '支付金额(元)',
-    `paid_time`   DATETIME      NOT NULL COMMENT '支付时间',
-    `create_time` DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `id`                 BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `order_no`           VARCHAR(32)   NOT NULL COMMENT '订单号',
+    `channel`           VARCHAR(20)   NOT NULL COMMENT '实际支付渠道:WECHAT/ALIPAY',
+    `trade_no`          VARCHAR(64)   NOT NULL COMMENT '第三方交易号',
+    `amount`             DECIMAL(10,2) NOT NULL COMMENT '支付金额(元)',
+    `paid_time`          DATETIME      NOT NULL COMMENT '支付时间',
+    `refund_status`     VARCHAR(20)   DEFAULT 'PENDING' COMMENT '退款状态:PENDING/SUCCESS/FAILED',
+    `refund_trade_no`   VARCHAR(64)   DEFAULT NULL COMMENT '退款交易号(第三方退款单号)',
+    `refund_time`       DATETIME      DEFAULT NULL COMMENT '退款时间',
+    `refund_fail_reason` VARCHAR(500) DEFAULT NULL COMMENT '退款失败原因',
+    `create_time`       DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time`       DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     PRIMARY KEY (`id`),
     KEY `idx_order_no` (`order_no`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单重复支付记录表';

+ 12 - 7
zhijiayun-gateway/src/test/resources/test-schema.sql

@@ -480,13 +480,18 @@ CREATE TABLE IF NOT EXISTS t_payment_order (
 
 -- 支付订单重复支付记录表
 CREATE TABLE IF NOT EXISTS t_payment_order_extra_payment (
-    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
-    order_no    VARCHAR(32)   NOT NULL,
-    channel     VARCHAR(20)   NOT NULL,
-    trade_no    VARCHAR(64)   NOT NULL,
-    amount      DECIMAL(10,2) NOT NULL,
-    paid_time   TIMESTAMP     NOT NULL,
-    create_time TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
+    id                  BIGINT AUTO_INCREMENT PRIMARY KEY,
+    order_no            VARCHAR(32)   NOT NULL,
+    channel             VARCHAR(20)   NOT NULL,
+    trade_no            VARCHAR(64)   NOT NULL,
+    amount              DECIMAL(10,2) NOT NULL,
+    paid_time           TIMESTAMP     NOT NULL,
+    refund_status       VARCHAR(20)   DEFAULT 'PENDING',
+    refund_trade_no     VARCHAR(64)   DEFAULT NULL,
+    refund_time         TIMESTAMP    DEFAULT NULL,
+    refund_fail_reason  VARCHAR(500) DEFAULT NULL,
+    create_time         TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
+    update_time         TIMESTAMP     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_order_no (order_no)
 );
 

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

@@ -893,11 +893,11 @@ public class InviteService {
 
         // 复制文本:与示例文案保持一致,便于用户一键转发
         String copyText = String.format(
-            "我是%s,在这里发现了一个药店采购神器——%s!它聚合比价功能特别方便,能快速查到最低价,帮你节省采购成本。填我的邀请码 %s 完成注册,你也会获得会员权益!下载链接%s",
+            "我是%s,在这里发现了一个药店采购神器——%s!它聚合比价功能特别方便,能快速查到最低价,帮你节省采购成本。下载链接%s填我的邀请码 %s 完成注册,你也会获得会员权益!",
             inviterNickname,
             config.getAppName(),
-            code.getCode(),
-            downloadLink
+            downloadLink,
+            code.getCode()
         );
 
         return InviteCodeResponse.builder()

+ 17 - 1
zhijiayun-payment/src/main/java/com/xuekairui/payment/entity/PaymentOrderExtraPayment.java

@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
  * 支付订单重复支付记录
  *
  * <p>同一订单在有效期内发生多次支付时(用户切换了支付渠道),
- * 第一次支付用于激活会员并作为订单主交易,后续支付记录到本表用于后续追溯和对账。</p>
+ * 第一次支付用于激活会员并作为订单主交易,后续支付记录到本表并自动发起原路退款。</p>
  */
 @Data
 @NoArgsConstructor
@@ -40,6 +40,22 @@ public class PaymentOrderExtraPayment {
     /** 支付时间 */
     private LocalDateTime paidTime;
 
+    /** 退款状态:PENDING(待退款) / SUCCESS(退款成功) / FAILED(退款失败) */
+    @Builder.Default
+    private String refundStatus = "PENDING";
+
+    /** 退款交易号(第三方退款单号) */
+    private String refundTradeNo;
+
+    /** 退款时间 */
+    private LocalDateTime refundTime;
+
+    /** 退款失败原因 */
+    private String refundFailReason;
+
     @TableField(fill = FieldFill.INSERT)
     private LocalDateTime createTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
 }

+ 27 - 1
zhijiayun-payment/src/main/java/com/xuekairui/payment/service/PaymentOrderService.java

@@ -56,6 +56,7 @@ public class PaymentOrderService {
     private final ObjectProvider<AlipayClient> alipayClientProvider;
     private final ObjectProvider<WxPayService> wxPayServiceProvider;
     private final PaymentAgreementService agreementService;
+    private final PaymentRefundService refundService;
 
     /**
      * 创建支付订单
@@ -299,7 +300,7 @@ public class PaymentOrderService {
      * 处理重复支付(切换渠道后多个渠道都支付成功)
      * <p>
      * 以第一次支付为准,后续支付记录到 {@code t_payment_order_extra_payment} 表,
-     * 不重复激活会员,但保留交易记录用于后续追溯和对账
+     * 不重复激活会员,并自动发起原路退款
      */
     private PaymentResultResponse handleDuplicatePayment(PaymentOrder order, String tradeNo, String paidChannel) {
         PaymentPlan plan = paymentPlanService.getPlanById(order.getPlanId());
@@ -320,11 +321,36 @@ public class PaymentOrderService {
             extraPaymentMapper.insert(extra);
             log.warn("订单重复支付已记录: orderNo={}, paidChannel={}, tradeNo={}, 主交易渠道={}",
                     order.getOrderNo(), paidChannel, tradeNo, order.getPaidChannel());
+
+            // 自动发起原路退款
+            doRefund(extra);
         }
 
         return buildResultResponse(order, plan);
     }
 
+    /**
+     * 对重复支付记录发起原路退款,并更新退款状态
+     */
+    private void doRefund(PaymentOrderExtraPayment extra) {
+        try {
+            String refundTradeNo = refundService.refund(
+                    extra.getChannel(), extra.getOrderNo(), extra.getTradeNo(), extra.getAmount());
+            extra.setRefundStatus("SUCCESS");
+            extra.setRefundTradeNo(refundTradeNo);
+            extra.setRefundTime(LocalDateTime.now());
+            extraPaymentMapper.updateById(extra);
+            log.info("重复支付已自动退款: orderNo={}, channel={}, tradeNo={}, refundTradeNo={}",
+                    extra.getOrderNo(), extra.getChannel(), extra.getTradeNo(), refundTradeNo);
+        } catch (Exception e) {
+            extra.setRefundStatus("FAILED");
+            extra.setRefundFailReason(e.getMessage());
+            extraPaymentMapper.updateById(extra);
+            log.error("重复支付自动退款失败: orderNo={}, channel={}, tradeNo={}, error={}",
+                    extra.getOrderNo(), extra.getChannel(), extra.getTradeNo(), e.getMessage(), e);
+        }
+    }
+
     /**
      * 检查并自动过期订单
      */

+ 147 - 0
zhijiayun-payment/src/main/java/com/xuekairui/payment/service/PaymentRefundService.java

@@ -0,0 +1,147 @@
+package com.xuekairui.payment.service;
+
+import cn.hutool.core.util.IdUtil;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.domain.AlipayTradeRefundModel;
+import com.alipay.api.request.AlipayTradeRefundRequest;
+import com.alipay.api.response.AlipayTradeRefundResponse;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.xuekairui.common.BusinessException;
+import com.xuekairui.common.ErrorCode;
+import com.xuekairui.payment.config.PaymentProperties;
+import com.xuekairui.payment.enums.PaymentChannel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+/**
+ * 支付退款服务
+ *
+ * <p>封装微信支付和支付宝的原路退款逻辑。
+ * 用于重复支付场景:同一订单多渠道支付成功后,对第二笔及之后的支付自动发起退款。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PaymentRefundService {
+
+    private final PaymentProperties paymentProperties;
+    @Qualifier("alipayClient")
+    private final ObjectProvider<AlipayClient> alipayClientProvider;
+    private final ObjectProvider<WxPayService> wxPayServiceProvider;
+
+    /**
+     * 发起原路退款
+     *
+     * @param channel 支付渠道:WECHAT/ALIPAY
+     * @param orderNo  商户订单号
+     * @param tradeNo  第三方交易号(微信 transaction_id / 支付宝 trade_no)
+     * @param amount   退款金额(元)
+     * @return 退款交易号(第三方退款单号)
+     */
+    public String refund(String channel, String orderNo, String tradeNo, BigDecimal amount) {
+        String refundNo = "RF" + IdUtil.getSnowflake(1, 1).nextIdStr();
+
+        if (paymentProperties.isMock()) {
+            log.info("【模拟】重复支付退款成功: orderNo={}, tradeNo={}, refundNo={}, amount={}元",
+                    orderNo, tradeNo, refundNo, amount);
+            return refundNo;
+        }
+
+        PaymentChannel ch = PaymentChannel.fromCode(channel);
+        if (ch == null) {
+            throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_SUPPORT, "不支持的退款渠道: " + channel);
+        }
+
+        return switch (ch) {
+            case WECHAT -> wechatRefund(orderNo, tradeNo, amount, refundNo);
+            case ALIPAY -> alipayRefund(orderNo, tradeNo, amount, refundNo);
+        };
+    }
+
+    /**
+     * 微信支付退款(API v3)
+     *
+     * <p>调用微信支付 v3 退款接口,按原交易号原路退回。
+     * 需要商户已配置退款证书(apiclient_key.pem + apiclient_cert.pem)。
+     */
+    private String wechatRefund(String orderNo, String tradeNo, BigDecimal amount, String refundNo) {
+        WxPayService wxPayService = wxPayServiceProvider.getIfAvailable();
+        if (wxPayService == null) {
+            throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "WxPayService未初始化,无法退款");
+        }
+
+        try {
+            WxPayRefundV3Request request = new WxPayRefundV3Request();
+            request.setOutTradeNo(orderNo);
+            request.setOutRefundNo(refundNo);
+            request.setReason("重复支付自动退款");
+
+            int totalCents = amount.movePointRight(2).intValue();
+            WxPayRefundV3Request.Amount amountObj = new WxPayRefundV3Request.Amount();
+            amountObj.setRefund(totalCents);
+            amountObj.setTotal(totalCents);
+            amountObj.setCurrency("CNY");
+            request.setAmount(amountObj);
+
+            WxPayRefundV3Result result = wxPayService.refundV3(request);
+            log.info("微信退款成功: orderNo={}, tradeNo={}, refundNo={}, refundId={}",
+                    orderNo, tradeNo, refundNo, result.getRefundId());
+            return result.getRefundId();
+        } catch (Exception e) {
+            log.error("微信退款失败: orderNo={}, tradeNo={}, refundNo={}", orderNo, tradeNo, refundNo, e);
+            throw new BusinessException(ErrorCode.REFUND_FAILED, "微信退款失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 支付宝退款(alipay.trade.refund)
+     *
+     * <p>调用支付宝统一收单交易退款接口,按原交易号原路退回。
+     */
+    private String alipayRefund(String orderNo, String tradeNo, BigDecimal amount, String refundNo) {
+        AlipayClient alipayClient = alipayClientProvider.getIfAvailable();
+        if (alipayClient == null) {
+            throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "AlipayClient未初始化,无法退款");
+        }
+
+        try {
+            AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
+            AlipayTradeRefundModel model = new AlipayTradeRefundModel();
+            model.setOutTradeNo(orderNo);
+            model.setTradeNo(tradeNo);
+            model.setRefundAmount(amount.toPlainString());
+            model.setOutRequestNo(refundNo);
+            model.setRefundReason("重复支付自动退款");
+            request.setBizModel(model);
+
+            boolean publicKeyMode = paymentProperties.getAlipay().getAlipayPublicKey() != null
+                    && !paymentProperties.getAlipay().getAlipayPublicKey().isBlank();
+            AlipayTradeRefundResponse response = publicKeyMode
+                    ? alipayClient.execute(request)
+                    : alipayClient.certificateExecute(request);
+
+            if (!response.isSuccess()) {
+                log.error("支付宝退款失败: orderNo={}, tradeNo={}, refundNo={}, code={}, subCode={}, subMsg={}",
+                        orderNo, tradeNo, refundNo, response.getCode(), response.getSubCode(), response.getSubMsg());
+                throw new BusinessException(ErrorCode.REFUND_FAILED,
+                        "支付宝退款失败: " + response.getSubMsg());
+            }
+
+            log.info("支付宝退款成功: orderNo={}, tradeNo={}, refundNo={}, tradeNo={}",
+                    orderNo, tradeNo, refundNo, response.getTradeNo());
+            return response.getTradeNo();
+        } catch (BusinessException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("支付宝退款异常: orderNo={}, tradeNo={}, refundNo={}", orderNo, tradeNo, refundNo, e);
+            throw new BusinessException(ErrorCode.REFUND_FAILED, "支付宝退款异常: " + e.getMessage());
+        }
+    }
+}

+ 4 - 1
zhijiayun-test/src/main/resources/static/invite-test.html

@@ -1160,8 +1160,11 @@ function buildBody(id, fields) {
     fields.forEach(f => {
         const forceArray = f.type === 'array';
         const asJson = f.type === 'json';
+        const forceString = f.type === 'string';
         if (asJson) {
             parts.push(`&quot;${f.name}&quot;: safeJsonParse(document.getElementById('${id}-${f.name}').value)`);
+        } else if (forceString) {
+            parts.push(`&quot;${f.name}&quot;: document.getElementById('${id}-${f.name}').value || null`);
         } else {
             parts.push(`&quot;${f.name}&quot;: parseFieldValue(document.getElementById('${id}-${f.name}').value, ${forceArray})`);
         }
@@ -1251,7 +1254,7 @@ function renderCrawlerModule() {
     ${testSection('crawler-consume', '消耗配额', 'POST', 'post', '/api/crawler/consume', '需认证', 'auth-required',
         [{label:'次数(count)', name:'count', placeholder:'1', default:'1'},
          {label:'优先优惠券(useCouponFirst)', name:'useCouponFirst', placeholder:'true/false', default:'false'},
-         {label:'平台代码(platform)', name:'platform', placeholder:'yaoshibang', default:'yaoshibang'}], true, null, '前端启动爬虫前调用,扣减配额次数')}
+         {label:'平台代码(platform)', name:'platform', placeholder:'yaoshibang 或 yaoshibang,yiyaocheng', default:'yaoshibang', type:'string'}], true, null, '前端启动爬虫前调用,扣减配额次数。platform仅用于日志统计,不做限制,支持逗号分隔传入多平台')}
     ${testSection('crawler-logs', '使用记录', 'GET', 'get', '/api/crawler/logs?days=7', '需认证', 'auth-required', null, true, 'crawlerLog', '查询爬虫使用历史记录')}`;
 }
 

+ 4 - 0
zhijiayun-user/pom.xml

@@ -89,6 +89,10 @@
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk18on</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.xuekairui</groupId>
+            <artifactId>zhijiayun-common</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

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

@@ -51,6 +51,8 @@ public class SecurityConfig {
             "/api/payment/agreement/notify/**",  // 签约回调通知(公开,供微信调用)
             "/api/payment/alipay/**",  // 支付宝开放能力回调(公开)
             "/api/payment/plans",        // 支付方案列表(公开)
+            "/api/level/list",           // 等级列表(公开)
+            "/api/level/*",              // 等级详情(公开,/my和/user/**已由前置规则匹配)
             "/error",                    // 错误页面
             "/actuator/health"           // 健康检查
     };

+ 1 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/service/VerificationCodeService.java

@@ -153,7 +153,7 @@ public class VerificationCodeService {
     private void checkSendLimit(String phone, String ip) {
         // 1. 检查1分钟间隔锁
         String intervalKey = INTERVAL_PREFIX + phone;
-        if (Boolean.TRUE.equals(redisTemplate.hasKey(intervalKey))) {
+        if (redisTemplate.hasKey(intervalKey)) {
             throw new BusinessException(ErrorCode.CODE_SEND_TOO_FREQUENT,
                     "发送过于频繁,请60秒后再试");
         }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików