12
0

40 Коммиты 5c8f40a8e3 ... a629f3cab8

Автор SHA1 Сообщение Дата
  lutong a629f3cab8 更多推荐中 门店头图审核过滤 3 дней назад
  刘云鑫 023582bddb feat:支付宝支付 6 дней назад
  刘云鑫 e7ebbcb6df Merge remote-tracking branch 'origin/sit' into sit 6 дней назад
  刘云鑫 3e6e025bac feat:支付宝支付 6 дней назад
  lutong 501209b517 商户评论的时候 显示商户的商铺名称 6 дней назад
  qinxuyang 92e47a0c11 Merge remote-tracking branch 'origin/sit' into sit 6 дней назад
  qinxuyang e413c43127 律师 支付宝绑定前置功能优化 6 дней назад
  fcw 25aba3a150 fix(payment): 修复支付宝直付通订单ID设置问题 6 дней назад
  zhangchen 4bce71fb69 个人设置,调整通知发送代码 6 дней назад
  zhangchen ab798e184a 个人设置,调整通知发送代码 1 неделя назад
  zhangchen 0654b74abb 关注以及粉丝相关接口修改 1 неделя назад
  zhangchen e07d36fe8c 新增接口,用户个性化设置相关代码 1 неделя назад
  fcw e7ef6eba2b Merge remote-tracking branch 'origin/sit' into sit 6 дней назад
  fcw a47678b548 fix(payment): 修复支付宝支付策略和支付控制器逻辑 6 дней назад
  zhangchen 096cd431c2 第三方绑定(微信)相关代码 6 дней назад
  qinxuyang 97b2546fcf job服务 pom导入shop.alien依赖 6 дней назад
  刘云鑫 d7c2368955 feat:新增合并后缺少的方法 6 дней назад
  fcw 82f56a74f2 refactor(entity): 优化支付宝直付通创建记录实体类结构 6 дней назад
  fcw 12cbe6dc4c Merge remote-tracking branch 'origin/sit' into sit 6 дней назад
  qinxuyang f2f9a8d69f Revert "律所 绑定支付宝前置功能 效验" 6 дней назад
  qinxuyang cb9d790baa 律所 绑定支付宝前置功能 效验 6 дней назад
  qinxuyang 5436f33b30 律所 绑定支付宝前置功能 效验 6 дней назад
  lutong f93f7ebaab 添加审核时间字段 1 неделя назад
  fcw 279d7321d5 Merge remote-tracking branch 'origin/sit' into sit 1 неделя назад
  fcw 31bc7d9bb1 feat(payment): 新增支付宝直付通二级商户支付功能 1 неделя назад
  刘云鑫 21eb739532 feat:微信绑定 1 неделя назад
  刘云鑫 779ab60738 feat:小程序支付。 1 неделя назад
  刘云鑫 6f5cd4f7a2 feat:入驻business_code修改 1 неделя назад
  刘云鑫 cd3f25581c feat:微信入驻接口成功+定时查询任务 1 неделя назад
  刘云鑫 8ddb0c69a2 feat:微信入驻 2 недель назад
  qinxuyang 4c27ee5b40 发布二手商品 去掉AI审核 1 неделя назад
  lutong e9ee167bf3 开发长链路缩短的功能 1 неделя назад
  lutong c260c7822d 审核修改接口 1 неделя назад
  lutong 477f972af7 定时任务: 二手交易平台 - 约定交易时间满24小时未双方确认交易成功自动失败 1 неделя назад
  qinxuyang 98350c52c3 线上预订 商家端 信息设置 1 неделя назад
  qinxuyang df5fe39d3c 店铺AI审核增加回显字段 VO 1 неделя назад
  qinxuyang baad8405ae 店铺AI审核增加回显字段 1 неделя назад
  lutong cb17e630dd Merge branch 'release_lutong_bug' into sit 1 неделя назад
  liudongzhi 747285a9eb 修改bug (预生产环境)人员配置(提测0328):人员下线,但是教练上面还显示已下线的教练 1 неделя назад
  qinxuyang 1f91b93c2c 用户端登录 可登录多个设备同时在线 1 неделя назад
100 измененных файлов с 8289 добавлено и 168 удалено
  1. 55 4
      alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java
  2. 3 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  3. 1 1
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java
  4. 725 0
      alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPartnerPaymentMininProgramStrategyImpl.java
  5. 82 0
      alien-entity/src/main/java/shop/alien/entity/store/AlipayZftCreateRecord.java
  6. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/LawyerUser.java
  7. 97 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUserPersonalizationSetting.java
  8. 60 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUserPushDevice.java
  9. 63 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUserThirdBind.java
  10. 60 0
      alien-entity/src/main/java/shop/alien/entity/store/PushDeviceOwnerType.java
  11. 9 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java
  12. 101 0
      alien-entity/src/main/java/shop/alien/entity/store/WechatPartnerApplyment.java
  13. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AlipayZftBizRequestDto.java
  14. 81 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AlipayZftMerchantCreateDto.java
  15. 201 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/AlipayZftMerchantSimplecreateDto.java
  16. 16 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/AiApproveStoreInfoResultVo.java
  17. 2 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CommonCommentVo.java
  18. 57 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserThirdBindInfoVo.java
  19. 69 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/MyMergedReviewItemVo.java
  20. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderInfoVO.java
  21. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreClockInVo.java
  22. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreLicenseInfoVo.java
  23. 13 0
      alien-entity/src/main/java/shop/alien/mapper/AlipayZftCreateRecordMapper.java
  24. 2 0
      alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java
  25. 20 10
      alien-entity/src/main/java/shop/alien/mapper/LifeFansMapper.java
  26. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeUserPersonalizationSettingMapper.java
  27. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeUserPushDeviceMapper.java
  28. 9 0
      alien-entity/src/main/java/shop/alien/mapper/LifeUserThirdBindMapper.java
  29. 1 1
      alien-entity/src/main/java/shop/alien/mapper/StoreClockInMapper.java
  30. 1 0
      alien-entity/src/main/java/shop/alien/mapper/StoreInfoMapper.java
  31. 11 0
      alien-entity/src/main/java/shop/alien/mapper/WechatPartnerApplymentMapper.java
  32. 1 0
      alien-entity/src/main/resources/mapper/StoreInfoMapper.xml
  33. 8 0
      alien-gateway/src/main/java/shop/alien/gateway/config/BaseRedisService.java
  34. 22 11
      alien-gateway/src/main/java/shop/alien/gateway/config/JwtTokenFilter.java
  35. 22 0
      alien-gateway/src/main/java/shop/alien/gateway/config/LifeUserThirdBindProperties.java
  36. 96 0
      alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserThirdBindController.java
  37. 22 0
      alien-gateway/src/main/java/shop/alien/gateway/dto/LifeUserThirdBindWechatRequest.java
  38. 16 2
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java
  39. 207 0
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserThirdBindService.java
  40. 100 0
      alien-gateway/src/main/java/shop/alien/gateway/thirdparty/WeChatMobileOAuthClient.java
  41. 6 0
      alien-job/pom.xml
  42. 9 4
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  43. 193 0
      alien-job/src/main/java/shop/alien/job/jobhandler/WechatPartnerApplymentAuditSyncJobHandler.java
  44. 135 0
      alien-job/src/main/java/shop/alien/job/store/AlipayJob.java
  45. 32 0
      alien-lawyer/src/main/java/shop/alien/lawyer/controller/AlipayZftCreateRecordController.java
  46. 17 0
      alien-lawyer/src/main/java/shop/alien/lawyer/service/AlipayZftCreateRecordService.java
  47. 36 0
      alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/AlipayZftCreateRecordServiceImpl.java
  48. 2 2
      alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerConsultationOrderServiceImpl.java
  49. 21 21
      alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java
  50. 38 22
      alien-second/src/main/java/shop/alien/second/task/Task.java
  51. 104 0
      alien-store/docs/short-link.md
  52. 47 0
      alien-store/src/main/java/shop/alien/store/config/UniPushProperties.java
  53. 2 2
      alien-store/src/main/java/shop/alien/store/controller/AiAuditController.java
  54. 62 0
      alien-store/src/main/java/shop/alien/store/controller/AlipayPartnerPaymentController.java
  55. 111 0
      alien-store/src/main/java/shop/alien/store/controller/AlipayZftOnboardingController.java
  56. 38 0
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  57. 16 8
      alien-store/src/main/java/shop/alien/store/controller/LifeStoreController.java
  58. 84 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserPersonalizationSettingController.java
  59. 62 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserPushDeviceController.java
  60. 89 10
      alien-store/src/main/java/shop/alien/store/controller/PaymentController.java
  61. 68 0
      alien-store/src/main/java/shop/alien/store/controller/ShortLinkController.java
  62. 3 3
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  63. 117 0
      alien-store/src/main/java/shop/alien/store/controller/WeChatPartnerApplymentController.java
  64. 26 0
      alien-store/src/main/java/shop/alien/store/dto/LifeUserPushBindDto.java
  65. 20 0
      alien-store/src/main/java/shop/alien/store/dto/ShortLinkResolveResponse.java
  66. 16 0
      alien-store/src/main/java/shop/alien/store/dto/ShortLinkShortenRequest.java
  67. 23 0
      alien-store/src/main/java/shop/alien/store/dto/ShortLinkShortenResponse.java
  68. 28 0
      alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java
  69. 53 0
      alien-store/src/main/java/shop/alien/store/service/AlipayZftOnboardingService.java
  70. 8 0
      alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java
  71. 129 2
      alien-store/src/main/java/shop/alien/store/service/LifeCommentService.java
  72. 27 12
      alien-store/src/main/java/shop/alien/store/service/LifeStoreService.java
  73. 46 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserPersonalizationSettingService.java
  74. 34 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserPushDeviceService.java
  75. 106 1
      alien-store/src/main/java/shop/alien/store/service/LifeUserService.java
  76. 140 39
      alien-store/src/main/java/shop/alien/store/service/MerchantPaymentSyncScheduler.java
  77. 19 0
      alien-store/src/main/java/shop/alien/store/service/ShortLinkService.java
  78. 1 1
      alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java
  79. 8 0
      alien-store/src/main/java/shop/alien/store/service/StorePaymentConfigService.java
  80. 82 0
      alien-store/src/main/java/shop/alien/store/service/UniPushCloudInvokeService.java
  81. 1103 0
      alien-store/src/main/java/shop/alien/store/service/WeChatPartnerApplymentService.java
  82. 801 0
      alien-store/src/main/java/shop/alien/store/service/impl/AlipayZftOnboardingServiceImpl.java
  83. 119 0
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  84. 279 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPersonalizationSettingServiceImpl.java
  85. 194 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPushDeviceServiceImpl.java
  86. 105 0
      alien-store/src/main/java/shop/alien/store/service/impl/ShortLinkServiceImpl.java
  87. 10 10
      alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java
  88. 6 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java
  89. 9 1
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  90. 11 0
      alien-store/src/main/java/shop/alien/store/service/impl/StorePaymentConfigServiceImpl.java
  91. 1 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java
  92. 56 0
      alien-store/src/main/java/shop/alien/store/strategy/payment/PaymentStrategy.java
  93. 277 0
      alien-store/src/main/java/shop/alien/store/strategy/payment/impl/AlipayPartnerPaymentStrategyImpl.java
  94. 5 0
      alien-store/src/main/java/shop/alien/store/strategy/payment/impl/AlipayPaymentStrategyImpl.java
  95. 740 0
      alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPartnerPaymentStrategyImpl.java
  96. 5 0
      alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java
  97. 6 0
      alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPaymentStrategyImpl.java
  98. 53 0
      alien-store/src/main/java/shop/alien/store/util/Base62Util.java
  99. 133 0
      alien-store/src/main/java/shop/alien/store/util/ShortLinkUrlValidator.java
  100. 7 1
      alien-util/src/main/java/shop/alien/util/common/constant/PaymentEnum.java

+ 55 - 4
alien-dining/src/main/java/shop/alien/dining/controller/PaymentController.java

@@ -10,9 +10,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
+import shop.alien.dining.service.StoreOrderService;
 import shop.alien.dining.strategy.payment.PaymentStrategyFactory;
 import shop.alien.dining.strategy.payment.PaymentStrategyFactory;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 import shop.alien.util.common.constant.PaymentEnum;
 import shop.alien.util.common.constant.PaymentEnum;
@@ -35,6 +38,7 @@ import java.util.Map;
 public class PaymentController {
 public class PaymentController {
 
 
     private final PaymentStrategyFactory paymentStrategyFactory;
     private final PaymentStrategyFactory paymentStrategyFactory;
+    private final StoreOrderService storeOrderService;
 
 
 
 
     @ApiOperation("创建预支付订单")
     @ApiOperation("创建预支付订单")
@@ -47,15 +51,15 @@ public class PaymentController {
             @ApiImplicitParam(name = "storeId", value = "店铺ID,用于从 MySQL 获取该店铺支付配置(StorePaymentConfig)", required = true, paramType = "query", dataType = "Integer"),
             @ApiImplicitParam(name = "storeId", value = "店铺ID,用于从 MySQL 获取该店铺支付配置(StorePaymentConfig)", required = true, paramType = "query", dataType = "Integer"),
             @ApiImplicitParam(name ="couponId", value = "优惠券Id"),
             @ApiImplicitParam(name ="couponId", value = "优惠券Id"),
             @ApiImplicitParam(name = "payerId", value = "payerId"),
             @ApiImplicitParam(name = "payerId", value = "payerId"),
-            @ApiImplicitParam(name = "tablewareFee", value = "餐具费"),
+            @ApiImplicitParam(name = "serviceFee", value = "服务费"),
             @ApiImplicitParam(name = "discountAmount", value = "优惠金额"),
             @ApiImplicitParam(name = "discountAmount", value = "优惠金额"),
             @ApiImplicitParam(name = "payAmount", value = "支付金额")
             @ApiImplicitParam(name = "payAmount", value = "支付金额")
     })
     })
     @RequestMapping("/prePay")
     @RequestMapping("/prePay")
-    public R prePay(String price, String subject, String payType, String payer, String orderNo, Integer storeId,Integer couponId, Integer payerId,String tablewareFee,String discountAmount,String payAmount) {
-        log.info("PaymentController:prePay, price: {}, subject: {}, payType: {}, payer: {}, orderNo: {}, storeId: {},couponId:{},payerId: {},tablewareFee: {},discountAmount: {},payAmount:{}", price, subject, payType, payer, orderNo, storeId,couponId,payerId,tablewareFee,discountAmount,payAmount);
+    public R prePay(String price, String subject, String payType, String payer, String orderNo, Integer storeId,Integer couponId, Integer payerId,String serviceFee,String discountAmount,String payAmount) {
+        log.info("PaymentController:prePay, price: {}, subject: {}, payType: {}, payer: {}, orderNo: {}, storeId: {},couponId:{},payerId: {},serviceFee: {},discountAmount: {},payAmount:{}", price, subject, payType, payer, orderNo, storeId,couponId,payerId,serviceFee,discountAmount,payAmount);
         try {
         try {
-            return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(price, subject, payer, orderNo, storeId, couponId,payerId,tablewareFee,discountAmount,payAmount);
+            return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(price, subject, payer, orderNo, storeId, couponId,payerId,serviceFee,discountAmount,payAmount);
         } catch (Exception e) {
         } catch (Exception e) {
             log.info("createPrePayOrder, orderNo: {}, error: {}", orderNo, e.getMessage());
             log.info("createPrePayOrder, orderNo: {}, error: {}", orderNo, e.getMessage());
             return R.fail(e.getMessage());
             return R.fail(e.getMessage());
@@ -68,6 +72,33 @@ public class PaymentController {
      * @param notifyData 回调 JSON 报文
      * @param notifyData 回调 JSON 报文
      * @return 204 无 body 或 5XX + 失败报文
      * @return 204 无 body 或 5XX + 失败报文
      */
      */
+    /**
+     * 服务商模式小程序支付回调(与直连小程序回调分离,需在商户平台配置 notify_url 指向本地址)
+     */
+    @RequestMapping("/weChatPartnerMininNotify")
+    public ResponseEntity<?> weChatPartnerMininNotify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[微信服务商小程序回调] 收到请求, Content-Length={}, Wechatpay-Serial={}",
+                request.getContentLength(), request.getHeader("Wechatpay-Serial"));
+        try {
+            R result = paymentStrategyFactory.getStrategy(PaymentEnum.WECHAT_PAY_PARTNER_MININ_PROGRAM.getType()).handleNotify(notifyData, request);
+            if (R.isSuccess(result)) {
+                return ResponseEntity.noContent().build();
+            }
+            String message = result.getMsg() != null ? result.getMsg() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", message);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        } catch (Exception e) {
+            log.error("[微信服务商小程序回调] 处理异常", e);
+            String msg = e.getMessage() != null ? e.getMessage() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", msg);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        }
+    }
+
     @RequestMapping("/weChatMininNotify")
     @RequestMapping("/weChatMininNotify")
     public ResponseEntity<?> notify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
     public ResponseEntity<?> notify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
         log.info("[微信支付回调] 收到回调请求, Content-Length={}, Wechatpay-Serial={}", request.getContentLength(), request.getHeader("Wechatpay-Serial"));
         log.info("[微信支付回调] 收到回调请求, Content-Length={}, Wechatpay-Serial={}", request.getContentLength(), request.getHeader("Wechatpay-Serial"));
@@ -95,6 +126,26 @@ public class PaymentController {
     }
     }
 
 
     /**
     /**
+     * 支付成功后重置餐桌与购物车(内部接口,供 alien-store 微信 APP 服务商回调等调用,与 {@link StoreOrderService#resetTableAfterPayment} 一致)
+     */
+    @PostMapping("/internal/resetTableAfterPayment")
+    public R<Void> internalResetTableAfterPayment(
+            @RequestParam("tableId") Integer tableId,
+            @RequestParam(value = "menuType", required = false) Integer menuType) {
+        log.info("[PaymentController] internalResetTableAfterPayment tableId={}, menuType={}", tableId, menuType);
+        try {
+            if (tableId == null) {
+                return R.fail("tableId 不能为空");
+            }
+            storeOrderService.resetTableAfterPayment(tableId);
+            return R.success("OK");
+        } catch (Exception e) {
+            log.error("[PaymentController] internalResetTableAfterPayment 失败 tableId={}", tableId, e);
+            return R.fail(e.getMessage() != null ? e.getMessage() : "重置失败");
+        }
+    }
+
+    /**
      * 查询订单状态
      * 查询订单状态
      * @param transactionId 交易订单号(微信支付订单号/商户订单号)
      * @param transactionId 交易订单号(微信支付订单号/商户订单号)
      * @param payType 支付类型
      * @param payType 支付类型

+ 3 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -1432,6 +1432,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         String storeAddress = "";
         String storeAddress = "";
         String storeBlurb = "";
         String storeBlurb = "";
         String storeType = "";
         String storeType = "";
+        String smid = "";
         Integer businessStatus = null;
         Integer businessStatus = null;
         if (storeInfo != null) {
         if (storeInfo != null) {
             storeName = storeInfo.getStoreName();
             storeName = storeInfo.getStoreName();
@@ -1440,6 +1441,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             storeBlurb = storeInfo.getStoreBlurb();
             storeBlurb = storeInfo.getStoreBlurb();
             storeType = storeInfo.getStoreType();
             storeType = storeInfo.getStoreType();
             businessStatus = storeInfo.getBusinessStatus();
             businessStatus = storeInfo.getBusinessStatus();
+            smid = storeInfo.getAlipaySmid();
         }
         }
 
 
         // 4. 查询优惠券信息(如果有)
         // 4. 查询优惠券信息(如果有)
@@ -1490,6 +1492,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         vo.setPayType(order.getPayType());
         vo.setPayType(order.getPayType());
         vo.setCreatedTime(order.getCreatedTime());
         vo.setCreatedTime(order.getCreatedTime());
         vo.setPayTime(order.getPayTime());
         vo.setPayTime(order.getPayTime());
+        vo.setSmid(smid);
         
         
         log.info("查询订单信息完成, orderId={}, itemCount={}", orderId, items.size());
         log.info("查询订单信息完成, orderId={}, itemCount={}", orderId, items.size());
         return vo;
         return vo;

+ 1 - 1
alien-dining/src/main/java/shop/alien/dining/strategy/payment/PaymentStrategy.java

@@ -23,7 +23,7 @@ public interface PaymentStrategy {
      * @return 预支付订单信息
      * @return 预支付订单信息
      * @throws Exception 生成异常
      * @throws Exception 生成异常
      */
      */
-    R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId, Integer couponId, Integer payerId,String tablewareFee,String discountAmount,String payAmount) throws Exception;
+    R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId, Integer couponId, Integer payerId,String serviceFee,String discountAmount,String payAmount) throws Exception;
 
 
 
 
     /**
     /**

+ 725 - 0
alien-dining/src/main/java/shop/alien/dining/strategy/payment/impl/WeChatPartnerPaymentMininProgramStrategyImpl.java

@@ -0,0 +1,725 @@
+package shop.alien.dining.strategy.payment.impl;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.annotations.SerializedName;
+import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
+import com.wechat.pay.java.service.refund.model.Refund;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import shop.alien.dining.service.StoreOrderService;
+import shop.alien.dining.strategy.payment.PaymentStrategy;
+import shop.alien.dining.util.WXPayUtility;
+import shop.alien.dining.util.WeChatPayUtil;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeDiscountCouponUser;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.mapper.LifeDiscountCouponUserMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StorePaymentConfigMapper;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.DiscountCouponEnum;
+import shop.alien.util.common.constant.PaymentEnum;
+import shop.alien.util.system.OSUtil;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * 微信支付小程序 — 服务商模式(特约商户 JSAPI)
+ * <p>
+ * 下单走 {@code POST /v3/pay/partner/transactions/jsapi},使用服务商证书签名;
+ * {@code sub_mchid} 来自 {@link StoreInfo#getWechatSubMchid()};
+ * 子商户小程序 {@code sub_appid} 优先取门店 {@link StorePaymentConfig},否则取配置项 {@code payment.wechatPartnerPay.business.subAppId}。
+ * </p>
+ *
+ * @see shop.alien.store.strategy.payment.impl.WeChatPartnerPaymentStrategyImpl
+ * @see WeChatPaymentMininProgramStrategyImpl
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WeChatPartnerPaymentMininProgramStrategyImpl implements PaymentStrategy {
+
+    private final StoreOrderService storeOrderService;
+    private final StorePaymentConfigMapper storePaymentConfigMapper;
+    private final StoreInfoMapper storeInfoMapper;
+    private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
+    private final ObjectMapper objectMapper;
+
+    @Value("${payment.wechatPartnerPay.host:https://api.mch.weixin.qq.com}")
+    private String wechatPayApiHost;
+
+    /** 服务商 JSAPI(小程序)预下单路径 */
+    @Value("${payment.wechatPartnerPay.miniProgram.prePayPath:/v3/pay/partner/transactions/jsapi}")
+    private String prePayPath;
+
+    @Value("${payment.wechatPartnerPay.searchOrderByOutTradeNoPath:/v3/pay/partner/transactions/out-trade-no/{out_trade_no}}")
+    private String searchOrderByOutTradeNoPath;
+
+    @Value("${payment.wechatPartnerPay.refundPath:/v3/refund/domestic/refunds}")
+    private String refundPath;
+
+    @Value("${payment.wechatPartnerPay.miniProgram.searchRefundStatusByOutRefundNoPath:/v3/refund/domestic/refunds/{out_refund_no}}")
+    private String searchRefundStatusByOutRefundNoPath;
+
+    @Value("${wechat.miniprogram.appId}")
+    private String spAppId;
+
+    @Value("${payment.wechatPartnerPay.business.spMchId}")
+    private String spMchId;
+
+    /** 全局默认子商户小程序 AppID,门店未配置 StorePaymentConfig 时可兜底 */
+    @Value("${payment.wechatPartnerPay.business.subAppId:}")
+    private String defaultSubAppId;
+
+    @Value("${payment.wechatPartnerPay.business.win.privateKeyPath}")
+    private String privateWinKeyPath;
+
+    @Value("${payment.wechatPartnerPay.business.linux.privateKeyPath}")
+    private String privateLinuxKeyPath;
+
+    @Value("${payment.wechatPartnerPay.business.win.wechatPayPublicKeyFilePath}")
+    private String wechatWinPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPartnerPay.business.linux.wechatPayPublicKeyFilePath}")
+    private String wechatLinuxPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPartnerPay.business.merchantSerialNumber}")
+    private String merchantSerialNumber;
+
+    @Value("${payment.wechatPartnerPay.business.wechatPayPublicKeyId}")
+    private String wechatPayPublicKeyId;
+
+    @Value("${payment.wechatPartnerPay.miniProgram.prePayNotifyUrl}")
+    private String prePayNotifyUrl;
+
+    @Value("${payment.wechatPartnerPay.miniProgram.refundNotifyUrl}")
+    private String refundNotifyUrl;
+
+    /** 服务商 APIv3 密钥,用于支付回调 resource 解密(与直连服务商一致) */
+    @Value("${payment.wechatPartnerPay.business.apiV3Key:}")
+    private String apiV3Key;
+
+    private PrivateKey privateKey;
+    private PublicKey wechatPayPublicKey;
+
+    private static final String POST_METHOD = "POST";
+    private static final String GET_METHOD = "GET";
+
+    @PostConstruct
+    public void loadPartnerCertificates() {
+        String keyPath;
+        String pubPath;
+        if ("windows".equals(OSUtil.getOsName())) {
+            keyPath = privateWinKeyPath;
+            pubPath = wechatWinPayPublicKeyFilePath;
+        } else {
+            keyPath = privateLinuxKeyPath;
+            pubPath = wechatLinuxPayPublicKeyFilePath;
+        }
+        log.info("[WeChatPartnerMinin] 加载服务商证书,os={}, privateKeyPath={}", OSUtil.getOsName(), keyPath);
+        this.privateKey = WXPayUtility.loadPrivateKeyFromPath(keyPath);
+        this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(pubPath);
+    }
+
+    private StorePaymentConfig getConfigByStoreId(Integer storeId) {
+        if (storeId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StorePaymentConfig::getStoreId, storeId);
+        return storePaymentConfigMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 解析特约商户小程序 AppID:门店配置优先,否则全局 defaultSubAppId。
+     */
+    private String resolveSubAppId(StorePaymentConfig config) {
+        if (config != null) {
+            if (org.springframework.util.StringUtils.hasText(config.getWechatMiniAppId())) {
+                return config.getWechatMiniAppId().trim();
+            }
+            if (org.springframework.util.StringUtils.hasText(config.getWechatAppId())) {
+                return config.getWechatAppId().trim();
+            }
+        }
+        if (StringUtils.isNotBlank(defaultSubAppId)) {
+            return defaultSubAppId.trim();
+        }
+        return null;
+    }
+
+    private String resolveSubMchidFromStore(Integer storeId) {
+        if (storeId == null) {
+            log.warn("[WeChatPartnerMinin] storeId 为空,无法解析 wechat_sub_mchid");
+            return null;
+        }
+        StoreInfo info = storeInfoMapper.selectById(storeId);
+        if (info == null) {
+            log.warn("[WeChatPartnerMinin] 门店不存在 storeId={}", storeId);
+            return null;
+        }
+        if (StringUtils.isBlank(info.getWechatSubMchid())) {
+            log.warn("[WeChatPartnerMinin] 门店未配置 wechat_sub_mchid storeId={}", storeId);
+            return null;
+        }
+        return info.getWechatSubMchid().trim();
+    }
+
+    @Override
+    public R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId,
+                                 Integer couponId, Integer payerId, String serviceFee, String discountAmount,
+                                 String payAmount) throws Exception {
+        if (storeId == null) {
+            log.warn("[WeChatPartnerMinin] createPrePayOrder 缺少 storeId");
+            return R.fail("店铺ID不能为空");
+        }
+        String subMchid = resolveSubMchidFromStore(storeId);
+        if (subMchid == null) {
+            return R.fail("请维护门店特约商户号 store_info.wechat_sub_mchid");
+        }
+
+        PartnerJsapiPrepayRequest request = new PartnerJsapiPrepayRequest();
+        request.spAppid = spAppId;
+        request.spMchid = spMchId;
+        request.subMchid = subMchid;
+        request.description = subject;
+        request.notifyUrl = prePayNotifyUrl;
+        request.amount = new CommonAmountInfo();
+        request.amount.total = Long.parseLong(price);
+        request.amount.currency = "CNY";
+        request.payer = new JsapiReqPayerInfo();
+        request.payer.spOpenid = payer.trim();
+
+        String wechatOutTradeNo = orderNo;
+        StoreOrder storeOrder = storeOrderService.getOrderByOrderNo(orderNo);
+        log.info("[WeChatPartnerMinin] createPrePayOrder orderNo={}, storeOrder={}, subMchid={}", orderNo, storeOrder != null, subMchid);
+
+        if (storeOrder != null) {
+            if (storeOrder.getPayStatus() != null && storeOrder.getPayStatus() == 1) {
+                return R.fail("订单已支付");
+            }
+            if (storeOrder.getPayTradeNo() != null) {
+                try {
+                    DirectAPIv3QueryResponse wxOrder = partnerSearchOrderRun(storeOrder.getPayTradeNo(), subMchid);
+                    if (wxOrder != null && "SUCCESS".equals(wxOrder.tradeState)) {
+                        return R.fail("该支付单已在微信侧支付成功,请勿重复发起支付");
+                    }
+                } catch (WXPayUtility.ApiException e) {
+                    if (e.getStatusCode() != 404 && !"ORDER_NOT_EXIST".equals(e.getErrorCode())) {
+                        log.error("[WeChatPartnerMinin] 预支付前查单失败 payTradeNo={}, code={}, msg={}",
+                                storeOrder.getPayTradeNo(), e.getErrorCode(), e.getErrorMessage());
+                        return R.fail("查询微信支付订单失败:" + (e.getErrorMessage() != null ? e.getErrorMessage() : e.getMessage()));
+                    }
+                }
+            }
+            String newPayTradeNo = "WX" + storeOrder.getId() + "_" + System.currentTimeMillis();
+            storeOrder.setPayTradeNo(newPayTradeNo);
+            wechatOutTradeNo = newPayTradeNo;
+            log.info("[WeChatPartnerMinin] 换新商户单号 orderNo={}, payTradeNo={}", orderNo, newPayTradeNo);
+            storeOrder.setCouponId(couponId);
+            storeOrder.setPayUserId(payerId);
+//            storeOrder.setServiceFee(new BigDecimal(serviceFee));
+            storeOrder.setDiscountAmount(new BigDecimal(discountAmount));
+            storeOrder.setPayAmount(new BigDecimal(payAmount));
+            if (!storeOrderService.updateById(storeOrder)) {
+                log.error("[WeChatPartnerMinin] 更新订单失败 orderNo={}", orderNo);
+                return R.fail("更新订单失败");
+            }
+        }
+        request.outTradeNo = wechatOutTradeNo;
+
+        try {
+            DirectAPIv3JsapiPrepayResponse response = partnerPrePayOrderRun(request);
+            log.info("[WeChatPartnerMinin] 预下单成功 prepayId={}, outTradeNo={}", response.prepayId, request.outTradeNo);
+
+            Map<String, String> result = new HashMap<>();
+            String prepayForSign = "prepay_id=" + response.prepayId;
+            result.put("prepayId", prepayForSign);
+            result.put("appId", spAppId);
+            result.put("spAppId", spAppId);
+            result.put("spMchId", spMchId);
+            result.put("subMchId", subMchid);
+            result.put("mchId", spMchId);
+            result.put("orderNo", request.outTradeNo);
+
+            long timestamp = System.currentTimeMillis() / 1000;
+            String nonce = WXPayUtility.createNonce(32);
+            String message = String.format("%s\n%s\n%s\n%s\n", spAppId, timestamp, nonce, prepayForSign);
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initSign(privateKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            result.put("signType", "RSA");
+            result.put("sign", Base64.getEncoder().encodeToString(sign.sign()));
+            result.put("timestamp", String.valueOf(timestamp));
+            result.put("nonce", nonce);
+            result.put("transactionId", request.outTradeNo);
+            return R.data(result);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("[WeChatPartnerMinin] 预下单失败 code={}, body={}", e.getErrorCode(), e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+
+    private DirectAPIv3JsapiPrepayResponse partnerPrePayOrderRun(PartnerJsapiPrepayRequest request) {
+        String uri = prePayPath;
+        String reqBody = WXPayUtility.toJson(request);
+        log.debug("[WeChatPartnerMinin] POST {} bodyLen={}", uri, reqBody.length());
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, POST_METHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+        reqBuilder.method(POST_METHOD, body);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartnerMinin prepay failed: " + uri, e);
+        }
+    }
+
+    private DirectAPIv3QueryResponse partnerSearchOrderRun(String outTradeNo, String subMchid) {
+        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(outTradeNo));
+        Map<String, Object> args = new HashMap<>(4);
+        args.put("sp_mchid", spMchId);
+        args.put("sub_mchid", subMchid);
+        String queryString = WXPayUtility.urlEncode(args);
+        if (!queryString.isEmpty()) {
+            uri = uri + "?" + queryString;
+        }
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, GET_METHOD, uri, null));
+        reqBuilder.method(GET_METHOD, null);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, DirectAPIv3QueryResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartnerMinin query order failed: " + uri, e);
+        }
+    }
+
+    @Override
+    public R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[WeChatPartnerMinin回调] 进入 handleNotify, len={}", notifyData != null ? notifyData.length() : 0);
+        String serial = request.getHeader("Wechatpay-Serial");
+        String signature = request.getHeader("Wechatpay-Signature");
+        String timestamp = request.getHeader("Wechatpay-Timestamp");
+        String nonce = request.getHeader("Wechatpay-Nonce");
+
+        if (serial == null || signature == null || timestamp == null || nonce == null) {
+            log.warn("[WeChatPartnerMinin回调] 验签参数缺失 serial={}, signature={}, timestamp={}, nonce={}",
+                    serial, signature != null, timestamp, nonce);
+            return R.fail("验签参数缺失");
+        }
+
+        if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
+            log.info("[WeChatPartnerMinin回调] 签名探测请求,直接成功");
+            return R.success("OK");
+        }
+
+        if (!StringUtils.equals(serial, wechatPayPublicKeyId)) {
+            log.warn("[WeChatPartnerMinin回调] Wechatpay-Serial 与配置不符 serial={}, expected={}", serial, wechatPayPublicKeyId);
+            return R.fail("公钥序列号不匹配");
+        }
+        if (wechatPayPublicKey == null) {
+            log.error("[WeChatPartnerMinin回调] 平台公钥未加载");
+            return R.fail("平台公钥未就绪");
+        }
+
+        StringBuilder signStr = new StringBuilder();
+        signStr.append(timestamp).append("\n");
+        signStr.append(nonce).append("\n");
+        signStr.append(notifyData).append("\n");
+        Signature sign = Signature.getInstance("SHA256withRSA");
+        byte[] signatureBytes = Base64.getDecoder().decode(signature);
+        sign.initVerify(wechatPayPublicKey);
+        sign.update(signStr.toString().getBytes(StandardCharsets.UTF_8));
+
+        if (!sign.verify(signatureBytes)) {
+            log.warn("[WeChatPartnerMinin回调] 验签失败");
+            return R.fail("Verified error");
+        }
+
+        if (!org.springframework.util.StringUtils.hasText(apiV3Key)) {
+            log.error("[WeChatPartnerMinin回调] 未配置 payment.wechatPartnerPay.business.apiV3Key,无法解密");
+            return R.fail("APIv3 密钥未配置");
+        }
+
+        final String notifyDataCopy = notifyData;
+        CompletableFuture.runAsync(() -> processNotifyBusiness(notifyDataCopy));
+        return R.success("OK");
+    }
+
+    /**
+     * 异步处理支付成功:解密 resource、更新订单与优惠券(与 {@link WeChatPaymentMininProgramStrategyImpl} 行为对齐)
+     */
+    private void processNotifyBusiness(String notifyData) {
+        try {
+            JsonNode rootNode = objectMapper.readTree(notifyData);
+            JsonNode resourceNode = rootNode.get("resource");
+            if (resourceNode == null) {
+                log.warn("[WeChatPartnerMinin回调] 报文无 resource");
+                return;
+            }
+            String encryptAlgorithm = resourceNode.get("algorithm").asText();
+            String resourceNonce = resourceNode.get("nonce").asText();
+            String associatedData = resourceNode.has("associated_data") && !resourceNode.get("associated_data").isNull()
+                    ? resourceNode.get("associated_data").asText() : "";
+            String ciphertext = resourceNode.get("ciphertext").asText();
+            if (!"AEAD_AES_256_GCM".equals(encryptAlgorithm)) {
+                log.warn("[WeChatPartnerMinin回调] 不支持的算法 {}", encryptAlgorithm);
+                return;
+            }
+            String plainBusinessData = WeChatPayUtil.decrypt(apiV3Key, resourceNonce, associatedData, ciphertext);
+            log.info("[WeChatPartnerMinin回调] 解密成功,业务数据长度={}", plainBusinessData.length());
+            JSONObject jsonObject = JSONObject.parseObject(plainBusinessData);
+            String tradeState = jsonObject.getString("trade_state");
+            if (!"SUCCESS".equals(tradeState)) {
+                log.info("[WeChatPartnerMinin回调] trade_state 非 SUCCESS: {}", tradeState);
+                return;
+            }
+            String outTradeNo = jsonObject.getString("out_trade_no");
+            StoreOrder storeOrder = storeOrderService.getOne(new QueryWrapper<StoreOrder>().eq("order_no", outTradeNo));
+            if (storeOrder == null && org.springframework.util.StringUtils.hasText(outTradeNo)) {
+                storeOrder = storeOrderService.getOne(new QueryWrapper<StoreOrder>().eq("pay_trade_no", outTradeNo));
+            }
+            if (storeOrder != null && storeOrder.getPayStatus() != 1) {
+                storeOrder.setPayStatus(1);
+                storeOrder.setOrderStatus(1);
+                storeOrder.setPayType(1);
+                storeOrder.setPayTime(new Date());
+                if (storeOrderService.updateById(storeOrder)) {
+                    log.info("[WeChatPartnerMinin] 更新订单支付成功 outTradeNo={}", outTradeNo);
+                    if (storeOrder.getCouponId() != null && storeOrder.getPayUserId() != null) {
+                        LambdaQueryWrapper<LifeDiscountCouponUser> couponUserWrapper = new LambdaQueryWrapper<>();
+                        couponUserWrapper.eq(LifeDiscountCouponUser::getUserId, storeOrder.getPayUserId());
+                        couponUserWrapper.eq(LifeDiscountCouponUser::getCouponId, storeOrder.getCouponId());
+                        couponUserWrapper.eq(LifeDiscountCouponUser::getStatus,
+                                Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
+                        couponUserWrapper.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+                        couponUserWrapper.orderByDesc(LifeDiscountCouponUser::getCreatedTime);
+                        couponUserWrapper.last("LIMIT 1");
+                        LifeDiscountCouponUser couponUser = lifeDiscountCouponUserMapper.selectOne(couponUserWrapper);
+                        if (couponUser != null) {
+                            couponUser.setStatus(Integer.parseInt(DiscountCouponEnum.HAVE_BEEN_USED.getValue()));
+                            couponUser.setUseTime(new Date());
+                            lifeDiscountCouponUserMapper.updateById(couponUser);
+                            log.info("[WeChatPartnerMinin] 优惠券已使用 id={}", couponUser.getId());
+                        }
+                    }
+                    try {
+                        storeOrderService.resetTableAfterPayment(storeOrder.getTableId());
+                        log.info("[WeChatPartnerMinin] 支付后重置餐桌 tableId={}", storeOrder.getTableId());
+                    } catch (Exception e) {
+                        log.error("[WeChatPartnerMinin] 重置餐桌失败 tableId={}", storeOrder.getTableId(), e);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[WeChatPartnerMinin回调] 异步处理异常", e);
+        }
+    }
+
+    @Override
+    public R searchOrderByOutTradeNoPath(String transactionId, Integer storeId) throws Exception {
+        log.info("[WeChatPartnerMinin] 查单 transactionId={}, storeId={}", transactionId, storeId);
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        String subMchid = resolveSubMchidFromStore(storeId);
+        if (subMchid == null) {
+            return R.fail("请维护门店特约商户号 store_info.wechat_sub_mchid");
+        }
+        try {
+            DirectAPIv3QueryResponse response = partnerSearchOrderRun(transactionId, subMchid);
+            return R.data(response);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("[WeChatPartnerMinin] 查单失败 code={}, msg={}", e.getErrorCode(), e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @Override
+    public String handleRefund(Map<String, String> params) throws Exception {
+        Integer refundStoreId = parseStoreIdFromParams(params);
+        String subMchid = resolveSubMchidFromStore(refundStoreId);
+        log.info("[WeChatPartnerMinin] 退款 storeId={}, subMchid={}, outTradeNo={}",
+                refundStoreId, subMchid != null ? subMchid : "(空)", params != null ? params.get("outTradeNo") : null);
+        if (subMchid == null) {
+            return "退款失败:请在参数中传入 storeId,且门店需维护 wechat_sub_mchid";
+        }
+        if (params == null || params.isEmpty()) {
+            return "退款失败:参数为空";
+        }
+        PartnerRefundCreateRequest request = new PartnerRefundCreateRequest();
+        request.subMchid = subMchid;
+        request.outTradeNo = params.get("outTradeNo");
+        request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        request.reason = params.get("reason");
+        request.notifyUrl = refundNotifyUrl;
+        request.amount = new AmountReq();
+        request.amount.refund = new BigDecimal(params.get("refundAmount")).longValue();
+        request.amount.total = new BigDecimal(params.get("totalAmount")).longValue();
+        request.amount.currency = "CNY";
+
+        try {
+            PartnerRefundResponse response = partnerRefundRun(request);
+            String status = response.status != null ? response.status : "UNKNOWN";
+            if ("SUCCESS".equals(status) || "PROCESSING".equals(status)) {
+                log.info("[WeChatPartnerMinin] 退款受理成功 outTradeNo={}, outRefundNo={}, status={}",
+                        request.outTradeNo, request.outRefundNo, status);
+                return "调用成功";
+            }
+            log.error("[WeChatPartnerMinin] 退款未成功 status={}, outTradeNo={}", status, request.outTradeNo);
+            return "退款失败";
+        } catch (Exception e) {
+            log.error("[WeChatPartnerMinin] 退款异常 outTradeNo={}", request.outTradeNo, e);
+            return "退款处理异常:" + e.getMessage();
+        }
+    }
+
+    private PartnerRefundResponse partnerRefundRun(PartnerRefundCreateRequest request) {
+        String uri = refundPath;
+        String reqBody = WXPayUtility.toJson(request);
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, POST_METHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+        reqBuilder.method(POST_METHOD, body);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, PartnerRefundResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartnerMinin refund failed: " + uri, e);
+        }
+    }
+
+    private static Integer parseStoreIdFromParams(Map<String, String> params) {
+        if (params == null) {
+            return null;
+        }
+        String raw = params.get("storeId");
+        if (raw == null || raw.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(raw.trim());
+        } catch (NumberFormatException e) {
+            log.warn("[WeChatPartnerMinin] 退款参数 storeId 非法: {}", raw);
+            return null;
+        }
+    }
+
+    @Override
+    public R searchRefundRecordByOutRefundNo(String outRefundNo, Integer storeId) throws Exception {
+        if (outRefundNo == null || outRefundNo.trim().isEmpty()) {
+            log.error("[WeChatPartnerMinin] 查询退款失败:outRefundNo 为空");
+            return R.fail("外部退款单号不能为空");
+        }
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        String subMchid = resolveSubMchidFromStore(storeId);
+        if (subMchid == null) {
+            return R.fail("请维护门店特约商户号 store_info.wechat_sub_mchid");
+        }
+        QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
+        request.setOutRefundNo(outRefundNo);
+        try {
+            Refund response = doSearchRefundByOutRefundNo(request, subMchid);
+            if (response == null) {
+                return R.fail("微信支付返回为空");
+            }
+            String refundStatus = String.valueOf(response.getStatus());
+            log.info("[WeChatPartnerMinin] 退款查询 outRefundNo={}, status={}", outRefundNo, refundStatus);
+            switch (refundStatus) {
+                case "SUCCESS":
+                    return R.data(response);
+                case "REFUNDCLOSE":
+                    return R.fail("微信支付退款已关闭");
+                case "PROCESSING":
+                    return R.fail("微信支付退款处理中,请稍后再查");
+                case "CHANGE":
+                    return R.fail("微信支付退款异常");
+                default:
+                    return R.fail("未知退款状态: " + refundStatus);
+            }
+        } catch (WXPayUtility.ApiException e) {
+            log.error("[WeChatPartnerMinin] 查询退款 API 异常 outRefundNo={}", outRefundNo, e);
+            return R.fail("微信支付查询退款失败:" + e.getMessage());
+        }
+    }
+
+    private Refund doSearchRefundByOutRefundNo(QueryByOutRefundNoRequest request, String subMchid) {
+        String uri = searchRefundStatusByOutRefundNoPath.replace("{out_refund_no}", WXPayUtility.urlEncode(request.getOutRefundNo()));
+        Map<String, Object> args = new HashMap<>(4);
+        args.put("sub_mchid", subMchid);
+        String queryString = WXPayUtility.urlEncode(args);
+        if (!queryString.isEmpty()) {
+            uri = uri + "?" + queryString;
+        }
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, GET_METHOD, uri, null));
+        reqBuilder.method(GET_METHOD, null);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, Refund.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartnerMinin refund query failed: " + uri, e);
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.WECHAT_PAY_PARTNER_MININ_PROGRAM.getType();
+    }
+
+    // ———— 请求/响应模型(字段名与微信 APIv3 一致) ————
+
+    public static class PartnerJsapiPrepayRequest {
+        @SerializedName("sp_appid")
+        public String spAppid;
+        @SerializedName("sp_mchid")
+        public String spMchid;
+        @SerializedName("sub_appid")
+        public String subAppid;
+        @SerializedName("sub_mchid")
+        public String subMchid;
+        @SerializedName("description")
+        public String description;
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+        @SerializedName("notify_url")
+        public String notifyUrl;
+        @SerializedName("amount")
+        public CommonAmountInfo amount;
+        @SerializedName("payer")
+        public JsapiReqPayerInfo payer;
+    }
+
+    public static class CommonAmountInfo {
+        @SerializedName("total")
+        public Long total;
+        @SerializedName("currency")
+        public String currency;
+    }
+
+    public static class JsapiReqPayerInfo {
+        @SerializedName("sp_openid")
+        public String spOpenid;
+    }
+
+    public static class DirectAPIv3JsapiPrepayResponse {
+        @SerializedName("prepay_id")
+        public String prepayId;
+    }
+
+    public static class DirectAPIv3QueryResponse {
+        @SerializedName("appid")
+        public String appid;
+        @SerializedName("mchid")
+        public String mchid;
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+        @SerializedName("transaction_id")
+        public String transactionId;
+        @SerializedName("trade_type")
+        public String tradeType;
+        @SerializedName("trade_state")
+        public String tradeState;
+        @SerializedName("trade_state_desc")
+        public String tradeStateDesc;
+    }
+
+    public static class PartnerRefundCreateRequest {
+        @SerializedName("sub_mchid")
+        public String subMchid;
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+        @SerializedName("out_refund_no")
+        public String outRefundNo;
+        @SerializedName("reason")
+        public String reason;
+        @SerializedName("notify_url")
+        public String notifyUrl;
+        @SerializedName("amount")
+        public AmountReq amount;
+    }
+
+    public static class AmountReq {
+        @SerializedName("refund")
+        public Long refund;
+        @SerializedName("total")
+        public Long total;
+        @SerializedName("currency")
+        public String currency;
+    }
+
+    public static class PartnerRefundResponse {
+        @SerializedName("status")
+        public String status;
+        @SerializedName("out_refund_no")
+        public String outRefundNo;
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+    }
+}

+ 82 - 0
alien-entity/src/main/java/shop/alien/entity/store/AlipayZftCreateRecord.java

@@ -0,0 +1,82 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 支付宝直付通二级商户标准进件(ant.merchant.expand.indirect.zft.create)本地提交记录。
+ */
+@Data
+@ApiModel(value = "AlipayZftCreateRecord对象", description = "支付宝直付通二级商户标准进件提交记录")
+@TableName("alipay_zft_create_record")
+public class AlipayZftCreateRecord {
+
+    @ApiModelProperty("主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("门店ID(可选)")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty("外部商户号 external_id")
+    @TableField("external_id")
+    private String externalId;
+
+    @ApiModelProperty("商户名称快照")
+    @TableField("merchant_name")
+    private String merchantName;
+
+    @ApiModelProperty("提交请求JSON")
+    @TableField("request_json")
+    private String requestJson;
+
+    @ApiModelProperty("支付宝响应原文")
+    @TableField("response_body")
+    private String responseBody;
+
+    @ApiModelProperty("是否调用成功")
+    @TableField("invoke_success")
+    private Boolean invokeSuccess;
+
+    @ApiModelProperty("支付宝子错误码")
+    @TableField("sub_code")
+    private String subCode;
+
+    @ApiModelProperty("支付宝子错误描述")
+    @TableField("sub_msg")
+    private String subMsg;
+
+    @ApiModelProperty("订单ID")
+    @TableField("order_id")
+    private String orderId;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField(value = "created_user_id", fill = FieldFill.INSERT)
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "更新人ID")
+    @TableField(value = "updated_user_id", fill = FieldFill.INSERT_UPDATE)
+    private Integer updatedUserId;
+
+}

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/LawyerUser.java

@@ -381,5 +381,9 @@ public class LawyerUser extends Model<LawyerUser> {
     @TableField("time_num")
     @TableField("time_num")
     private Integer timeNum;
     private Integer timeNum;
 
 
+    @ApiModelProperty(value = "支付宝二级商户是否绑定")
+    @TableField("zfb_secondary_merchant_account")
+    private String zfbSecondaryMerchantAccount;
+
 }
 }
 
 

+ 97 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUserPersonalizationSetting.java

@@ -0,0 +1,97 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * life_user 个性化设置
+ */
+@Data
+@JsonInclude
+@TableName("life_user_personalization_setting")
+@ApiModel(value = "LifeUserPersonalizationSetting", description = "用户个性化设置")
+public class LifeUserPersonalizationSetting implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "用户id,对应 life_user.id")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "个性化推荐 0不推荐 1推荐")
+    @TableField("personalized_recommendation")
+    private Integer personalizedRecommendation;
+
+    @ApiModelProperty(value = "允许关注的人评论我 0不允许 1允许")
+    @TableField("only_followers_comment")
+    private Integer onlyFollowersComment;
+
+    @ApiModelProperty(value = "粉丝列表 0不可见 1可见")
+    @TableField("hide_fans_list")
+    private Integer hideFansList;
+
+    @ApiModelProperty(value = "关注列表 0不可见 1可见")
+    @TableField("hide_follow_list")
+    private Integer hideFollowList;
+
+    @ApiModelProperty(value = "接收消息 0不接收 1可接收")
+    @TableField("notify_receive_message")
+    private Integer notifyReceiveMessage;
+
+    @ApiModelProperty(value = "点赞 0不可点赞 1可点赞")
+    @TableField("notify_like")
+    private Integer notifyLike;
+
+    @ApiModelProperty(value = "关注 0不可关注 1可关注")
+    @TableField("notify_follow")
+    private Integer notifyFollow;
+
+    @ApiModelProperty(value = "评论 0不可评论 1可评论")
+    @TableField("notify_comment")
+    private Integer notifyComment;
+
+    @ApiModelProperty(value = "是否跟随系统字体 0不跟随 1跟随;为1时不更新 chatFontLevel")
+    @TableField("follow_system_font")
+    private Integer followSystemFont;
+
+    @ApiModelProperty(value = "字体档位 0~4,仅 followSystemFont=0 时可调整")
+    @TableField("chat_font_level")
+    private Integer chatFontLevel;
+
+    @ApiModelProperty(value = "允许自动刷新 0不允许 1允许")
+    @TableField("auto_refresh")
+    private Integer autoRefresh;
+
+    @ApiModelProperty(value = "删除标记 0未删除 1已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 60 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUserPushDevice.java

@@ -0,0 +1,60 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 推送设备 cid 绑定;{@link #userId} 含义由 {@link #ownerType} 决定。
+ */
+@Data
+@JsonInclude
+@TableName("life_user_push_device")
+@ApiModel(value = "LifeUserPushDevice", description = "用户推送设备绑定")
+public class LifeUserPushDevice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "业务用户主键:owner_type=user 时为 life_user.id;store 为 store_user.id;lawyer 为 lawyer_user.id")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "归属类型:user / store / lawyer(与 JWT userType 一致)")
+    @TableField("owner_type")
+    private String ownerType;
+
+    @ApiModelProperty(value = "uni-push client id")
+    @TableField("push_client_id")
+    private String pushClientId;
+
+    @ApiModelProperty(value = "平台:ios、android 等")
+    @TableField("platform")
+    private String platform;
+
+    @ApiModelProperty(value = "DCloud appid,如 __UNI__xxxx")
+    @TableField("dcloud_app_id")
+    private String dcloudAppId;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField("created_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField("updated_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

+ 63 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUserThirdBind.java

@@ -0,0 +1,63 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@JsonInclude
+@TableName("life_user_third_bind")
+@ApiModel("LifeUserThirdBind")
+public class LifeUserThirdBind {
+
+    public static final String PLATFORM_WECHAT = "WECHAT";
+    public static final String PLATFORM_ALIPAY = "ALIPAY";
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("life_user_id")
+    @ApiModelProperty("life_user.id")
+    private Integer lifeUserId;
+
+    @TableField("platform")
+    @ApiModelProperty("WECHAT / ALIPAY")
+    private String platform;
+
+    @TableField("openid")
+    private String openid;
+
+    @TableField("unionid")
+    private String unionid;
+
+    @TableField("alipay_user_id")
+    private String alipayUserId;
+
+    @TableField("account_label")
+    @ApiModelProperty("昵称/展示名")
+    private String accountLabel;
+
+    @TableField("bind_phone")
+    @ApiModelProperty("渠道授权手机号")
+    private String bindPhone;
+
+    @TableField("remark")
+    private String remark;
+
+    @TableLogic
+    @TableField("delete_flag")
+    private Integer deleteFlag;
+
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

+ 60 - 0
alien-entity/src/main/java/shop/alien/entity/store/PushDeviceOwnerType.java

@@ -0,0 +1,60 @@
+package shop.alien.entity.store;
+
+/**
+ * 推送设备绑定归属类型,与 JWT 中 userType 及表 life_user_push_device.owner_type 一致。
+ * <ul>
+ *   <li>{@link #USER} — C 端 life_user.id</li>
+ *   <li>{@link #STORE} — 门店 store_user.id</li>
+ *   <li>{@link #LAWYER} — 律师 lawyer_user.id</li>
+ * </ul>
+ */
+public enum PushDeviceOwnerType {
+
+    USER("user"),
+    STORE("store"),
+    LAWYER("lawyer");
+
+    private final String code;
+
+    PushDeviceOwnerType(String code) {
+        this.code = code;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    /**
+     * 根据登录 token 中的 userType 解析;不支持的类型返回 null。
+     */
+    public static PushDeviceOwnerType fromJwtUserType(String jwtUserType) {
+        if (jwtUserType == null || jwtUserType.trim().isEmpty()) {
+            return USER;
+        }
+        String t = jwtUserType.trim().toLowerCase();
+        switch (t) {
+            case "user":
+                return USER;
+            case "store":
+            case "merchant":
+                return STORE;
+            case "lawyer":
+                return LAWYER;
+            default:
+                return null;
+        }
+    }
+
+    public static PushDeviceOwnerType fromCode(String code) {
+        if (code == null || code.trim().isEmpty()) {
+            return null;
+        }
+        String c = code.trim().toLowerCase();
+        for (PushDeviceOwnerType v : values()) {
+            if (v.code.equals(c)) {
+                return v;
+            }
+        }
+        return null;
+    }
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java

@@ -362,4 +362,13 @@ public class StoreInfo {
     @ApiModelProperty(value = "预约服务 0不提供 1提供")
     @ApiModelProperty(value = "预约服务 0不提供 1提供")
     @TableField("booking_service")
     @TableField("booking_service")
     private Integer bookingService;
     private Integer bookingService;
+
+    @ApiModelProperty(value = "微信支付特约商户号(服务商进件审核通过后回写 sub_mchid)")
+    @TableField("wechat_sub_mchid")
+    private String wechatSubMchid;
+
+    @ApiModelProperty(value = "支付宝支付特约商户号(服务商进件审核通过后回写 smid)")
+    @TableField("alipay_smid")
+    private String alipaySmid;
+
 }
 }

+ 101 - 0
alien-entity/src/main/java/shop/alien/entity/store/WechatPartnerApplyment.java

@@ -0,0 +1,101 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 微信支付服务商 - 特约商户进件申请单(本地落库主表)
+ */
+@Data
+@ApiModel(value = "WechatPartnerApplyment对象", description = "微信支付服务商-特约商户进件申请单(本地落库)")
+@TableName("wechat_partner_applyment")
+public class WechatPartnerApplyment {
+
+    @ApiModelProperty("主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("服务商商户号sp_mchid")
+    @TableField("sp_mchid")
+    private String spMchid;
+
+    @ApiModelProperty("业务申请编号business_code")
+    @TableField("business_code")
+    private String businessCode;
+
+    @ApiModelProperty("幂等键Idempotency-Key")
+    @TableField("idempotency_key")
+    private String idempotencyKey;
+
+    /**
+     * 关联本系统门店 {@link StoreInfo#getId()},审核通过后用于回写 {@code store_info.wechat_sub_mchid};
+     * 提交进件时通过 query 参数 {@code storeId} 传入并落库(非微信请求字段)。
+     */
+    @ApiModelProperty("关联门店ID store_info.id")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty("微信支付申请单号applyment_id")
+    @TableField("applyment_id")
+    private Long applymentId;
+
+    @ApiModelProperty("特约商户号sub_mchid")
+    @TableField("sub_mchid")
+    private String subMchid;
+
+    @ApiModelProperty("超级管理员签约链接sign_url")
+    @TableField("sign_url")
+    private String signUrl;
+
+    @ApiModelProperty("申请单状态applyment_state")
+    @TableField("applyment_state")
+    private String applymentState;
+
+    @ApiModelProperty("申请状态描述applyment_state_msg")
+    @TableField("applyment_state_msg")
+    private String applymentStateMsg;
+
+    @ApiModelProperty("是否通过:0未知/处理中 1通过 2驳回")
+    @TableField("is_approved")
+    private Integer isApproved;
+
+    @ApiModelProperty("不通过理由(聚合audit_detail.reject_reason等)")
+    @TableField("reject_reason")
+    private String rejectReason;
+
+    @ApiModelProperty("最后一次提交时间")
+    @TableField("last_submit_time")
+    private Date lastSubmitTime;
+
+    @ApiModelProperty("最后一次查询时间")
+    @TableField("last_query_time")
+    private Date lastQueryTime;
+
+    @ApiModelProperty("提交申请单请求体JSON(建议存最终提交给微信的那份)")
+    @TableField("request_json")
+    private String requestJson;
+
+    @ApiModelProperty("最后一次提交微信响应原文JSON")
+    @TableField("last_submit_resp_json")
+    private String lastSubmitRespJson;
+
+    @ApiModelProperty("最后一次查询微信响应原文JSON")
+    @TableField("last_query_resp_json")
+    private String lastQueryRespJson;
+
+    @ApiModelProperty("创建时间")
+    @TableField("created_at")
+    private Date createdAt;
+
+    @ApiModelProperty("更新时间")
+    @TableField("updated_at")
+    private Date updatedAt;
+}
+

+ 50 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AlipayZftBizRequestDto.java

@@ -0,0 +1,50 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 支付宝直付通 / 二级商户进件类接口统一请求体。
+ * <p>
+ * {@code bizContent} 需与官方接口的 {@code biz_content} 一致,字段以
+ * <a href="https://opendocs.alipay.com/solution/0dec7x">支付宝方案文档</a> 为准。
+ * <p>
+ * 公共参数中 {@code format}、{@code charset}、{@code sign_type}、签名、时间戳等由 SDK 自动处理。
+ * {@link #appId} 可显式传入;不传则使用门店支付配置中的应用 ID。第三方代调用时可传 {@link #appAuthToken}。
+ */
+@Data
+@ApiModel("支付宝直付通 biz_content 请求")
+public class AlipayZftBizRequestDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "门店ID(用于选取该门店已配置的支付宝应用与证书)", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty("异步通知地址,进件类接口按文档选填")
+    private String notifyUrl;
+
+    @ApiModelProperty("支付宝应用 app_id;不传则使用门店 store_payment_config.app_id(须与证书同属该应用)")
+    @JsonProperty("app_id")
+    @JsonAlias("appId")
+    private String appId;
+
+    @ApiModelProperty("应用授权令牌 app_auth_token(第三方代调用时选填,见开放平台应用授权)")
+    @JsonProperty("app_auth_token")
+    @JsonAlias("appAuthToken")
+    private String appAuthToken;
+
+    @ApiModelProperty("接口版本 api_version,一般无需传;需覆盖 SDK 默认时填写,如 1.0")
+    @JsonProperty("api_version")
+    @JsonAlias("apiVersion")
+    private String apiVersion;
+
+    @ApiModelProperty(value = "对应支付宝接口的 biz_content(JSON 对象);不传或为空时按 {} 提交")
+    private Map<String, Object> bizContent;
+}

+ 81 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AlipayZftMerchantCreateDto.java

@@ -0,0 +1,81 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ant.merchant.expand.indirect.zft.create(二级商户创建)请求。
+ * <p>
+ * 与官方网关 multipart 中 {@code biz_content} 一致:可将整段业务 JSON 放在 {@link #bizContent} 中;
+ * 也可仅用顶层 {@link #externalId}、{@link #merchantType} 等与 {@link #extraBiz} 组合。
+ * <p>
+ * 合并顺序:先 {@link #bizContent},再 {@link #extraBiz},最后非空的顶层字段覆盖同名键(便于局部纠偏)。
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@ApiModel("直付通二级商户创建(zft.create)")
+public class AlipayZftMerchantCreateDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "门店ID(选取该门店支付宝应用与证书)", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty("异步通知地址,按文档选填")
+    private String notifyUrl;
+
+    @ApiModelProperty("支付宝应用 app_id;不传则使用门店配置")
+    @JsonProperty("app_id")
+    @JsonAlias("appId")
+    private String appId;
+
+    @ApiModelProperty("应用授权令牌 app_auth_token(第三方代调用时选填)")
+    @JsonProperty("app_auth_token")
+    @JsonAlias("appAuthToken")
+    private String appAuthToken;
+
+    @ApiModelProperty("接口版本 api_version,一般无需传")
+    @JsonProperty("api_version")
+    @JsonAlias("apiVersion")
+    private String apiVersion;
+
+    @ApiModelProperty("商户编号 external_id;可与 biz_content 二选一(biz_content 内已含则可不传顶层)")
+    @JsonProperty("external_id")
+    @JsonAlias("externalId")
+    private String externalId;
+
+    @ApiModelProperty("商户类型 merchant_type;可与 biz_content 二选一")
+    @JsonProperty("merchant_type")
+    @JsonAlias("merchantType")
+    private String merchantType;
+
+    @ApiModelProperty("商户名称;可与 biz_content 二选一")
+    private String name;
+
+    @ApiModelProperty("商户别名 alias_name;可与 biz_content 二选一")
+    @JsonProperty("alias_name")
+    @JsonAlias("aliasName")
+    private String aliasName;
+
+    @ApiModelProperty("商户类目 MCC;可与 biz_content 二选一")
+    private String mcc;
+
+    @ApiModelProperty("开通服务 service;可与 biz_content 二选一")
+    private List<String> service;
+
+    @ApiModelProperty("与官方接口 biz_content 一致的整体对象(推荐)。与 extraBiz、顶层字段合并后作为最终 biz_content")
+    @JsonProperty("biz_content")
+    @JsonAlias("bizContent")
+    private Map<String, Object> bizContent;
+
+    @ApiModelProperty("其余 biz_content 字段(可选),在 biz_content 之后合并;同名字段以顶层显式字段为准")
+    private Map<String, Object> extraBiz;
+}

+ 201 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/AlipayZftMerchantSimplecreateDto.java

@@ -0,0 +1,201 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * ant.merchant.expand.indirect.zft.simplecreate(直付通二级商户免证照进件)请求体。
+ * <p>
+ * 已含联系人、结算卡、开票、站点、行业资质等;公共参数中 {@link #appId}、{@code app_auth_token}、{@code api_version} 可显式传入。
+ */
+@Data
+@ApiModel("直付通二级商户简化进件(zft.simplecreate)")
+public class AlipayZftMerchantSimplecreateDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @ApiModelProperty("异步通知地址")
+    private String notifyUrl;
+
+    @ApiModelProperty("支付宝应用 app_id;不传则使用门店配置")
+    @JsonProperty("app_id")
+    @JsonAlias("appId")
+    private String appId;
+
+    @ApiModelProperty("应用授权令牌 app_auth_token(第三方代调用时选填)")
+    @JsonProperty("app_auth_token")
+    @JsonAlias("appAuthToken")
+    private String appAuthToken;
+
+    @ApiModelProperty("接口版本 api_version,一般无需传")
+    @JsonProperty("api_version")
+    @JsonAlias("apiVersion")
+    private String apiVersion;
+
+    @ApiModelProperty(value = "商户编号 external_id", required = true)
+    @JsonProperty("external_id")
+    @JsonAlias("externalId")
+    private String externalId;
+
+    @ApiModelProperty(value = "签约支付宝账户 binding_alipay_logon_id", required = true)
+    @JsonProperty("binding_alipay_logon_id")
+    @JsonAlias("bindingAlipayLogonId")
+    private String bindingAlipayLogonId;
+
+    @ApiModelProperty(value = "商户别名 alias_name", required = true)
+    @JsonProperty("alias_name")
+    @JsonAlias("aliasName")
+    private String aliasName;
+
+    @ApiModelProperty("商户客服电话 service_phone")
+    @JsonProperty("service_phone")
+    @JsonAlias("servicePhone")
+    private String servicePhone;
+
+    @ApiModelProperty(value = "二级商户名称 name", required = true)
+    private String name;
+
+    @ApiModelProperty(value = "商户类目 mcc", required = true)
+    private String mcc;
+
+    @ApiModelProperty(value = "开通服务列表 service", required = true)
+    private List<String> service;
+
+    @ApiModelProperty(value = "结算支付宝账号 alipay_logon_id(结算到支付宝时与 default_settle_rule 配合使用)")
+    @JsonProperty("alipay_logon_id")
+    @JsonAlias("alipayLogonId")
+    private String alipayLogonId;
+
+    @ApiModelProperty(value = "联系人 contact_infos(姓名、手机号在业务上必填)")
+    @JsonProperty("contact_infos")
+    @JsonAlias("contactInfos")
+    private ContactPart contactInfos;
+
+    @ApiModelProperty("默认结算规则 default_settle_rule")
+    @JsonProperty("default_settle_rule")
+    @JsonAlias("defaultSettleRule")
+    private DefaultSettleRulePart defaultSettleRule;
+
+    @ApiModelProperty("结算银行卡 biz_cards(结算到银行卡时填写,与支付宝结算二选一按文档)")
+    @JsonProperty("biz_cards")
+    @JsonAlias("bizCards")
+    private SettleCardPart bizCards;
+
+    @ApiModelProperty("经营地址 business_address(使用当面付等时按文档必填省市区与详细地址)")
+    @JsonProperty("business_address")
+    @JsonAlias("businessAddress")
+    private AddressPart businessAddress;
+
+    @ApiModelProperty("门头照,图片上传接口返回的 oss key")
+    @JsonProperty("out_door_images")
+    @JsonAlias("outDoorImages")
+    private String outDoorImages;
+
+    @ApiModelProperty("内景照,图片上传接口返回的 oss key")
+    @JsonProperty("in_door_images")
+    @JsonAlias("inDoorImages")
+    private String inDoorImages;
+
+    @ApiModelProperty("授权函/说明函图片,图片上传接口返回的 oss key")
+    @JsonProperty("license_auth_letter_image")
+    @JsonAlias("licenseAuthLetterImage")
+    private String licenseAuthLetterImage;
+
+    @Data
+    @ApiModel("联系人")
+    public static class ContactPart implements Serializable {
+        private static final long serialVersionUID = 1L;
+        @ApiModelProperty("姓名")
+        private String name;
+        @ApiModelProperty("手机号")
+        private String mobile;
+        @JsonProperty("id_card_no")
+        @JsonAlias("idCardNo")
+        @ApiModelProperty("身份证号")
+        private String idCardNo;
+        @ApiModelProperty("电话")
+        private String phone;
+        @ApiModelProperty("邮箱")
+        private String email;
+    }
+
+    @Data
+    @ApiModel("默认结算规则")
+    public static class DefaultSettleRulePart implements Serializable {
+        private static final long serialVersionUID = 1L;
+        @JsonProperty("default_settle_type")
+        @JsonAlias("defaultSettleType")
+        @ApiModelProperty("alipayAccount / bankCard")
+        private String defaultSettleType;
+        @JsonProperty("default_settle_target")
+        @JsonAlias("defaultSettleTarget")
+        @ApiModelProperty("结算目标(支付宝登录号或银行卡号等)")
+        private String defaultSettleTarget;
+    }
+
+    @Data
+    @ApiModel("结算银行卡")
+    public static class SettleCardPart implements Serializable {
+        private static final long serialVersionUID = 1L;
+        @JsonProperty("account_inst_name")
+        @JsonAlias("accountInstName")
+        private String accountInstName;
+        @JsonProperty("bank_code")
+        @JsonAlias("bankCode")
+        private String bankCode;
+        @JsonProperty("account_type")
+        @JsonAlias("accountType")
+        private String accountType;
+        @JsonProperty("usage_type")
+        @JsonAlias("usageType")
+        private String usageType;
+        @JsonProperty("account_holder_name")
+        @JsonAlias("accountHolderName")
+        private String accountHolderName;
+        @JsonProperty("account_inst_city")
+        @JsonAlias("accountInstCity")
+        private String accountInstCity;
+        @JsonProperty("account_inst_id")
+        @JsonAlias("accountInstId")
+        private String accountInstId;
+        @JsonProperty("account_no")
+        @JsonAlias("accountNo")
+        private String accountNo;
+        @JsonProperty("account_branch_name")
+        @JsonAlias("accountBranchName")
+        private String accountBranchName;
+        @JsonProperty("account_inst_province")
+        @JsonAlias("accountInstProvince")
+        private String accountInstProvince;
+    }
+
+    @Data
+    @ApiModel("地址")
+    public static class AddressPart implements Serializable {
+        private static final long serialVersionUID = 1L;
+        private String address;
+        @JsonProperty("district_code")
+        @JsonAlias("districtCode")
+        private String districtCode;
+        private String latitude;
+        @JsonProperty("city_code")
+        @JsonAlias("cityCode")
+        private String cityCode;
+        private String poiid;
+        @JsonProperty("province_code")
+        @JsonAlias("provinceCode")
+        private String provinceCode;
+        private String longitude;
+        @ApiModelProperty("如 BUSINESS_ADDRESS")
+        private String type;
+    }
+}

+ 16 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/AiApproveStoreInfoResultVo.java

@@ -0,0 +1,16 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * AI 店铺审核接口响应(回显 AI 返回字段)
+ */
+@Data
+@ApiModel(description = "AI店铺审核结果")
+public class AiApproveStoreInfoResultVo {
+
+    @ApiModelProperty(value = "注册号/统一社会信用代码(AI 回显 registration_no)")
+    private String registrationNo;
+}

+ 2 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonCommentVo.java

@@ -24,4 +24,6 @@ public class CommonCommentVo extends CommonComment {
     private Integer commentCount;
     private Integer commentCount;
     @ApiModelProperty(value = "评论用户手机号")
     @ApiModelProperty(value = "评论用户手机号")
     private String headPhone;
     private String headPhone;
+    @ApiModelProperty(value = "商铺名称(评论主体为商户时有效,comment_type=2)")
+    private String storeName;
 }
 }

+ 57 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserThirdBindInfoVo.java

@@ -0,0 +1,57 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 当前用户微信/支付宝绑定信息:昵称、平台账号标识、渠道手机号等
+ */
+@Data
+@ApiModel("用户第三方绑定查询")
+public class LifeUserThirdBindInfoVo {
+
+    @ApiModelProperty("life_user.id")
+    private Integer lifeUserId;
+
+    @ApiModelProperty("用户账号手机号(life_user.user_phone)")
+    private String lifeUserPhone;
+
+    @ApiModelProperty("微信")
+    private WechatBindItem wechat;
+
+    @ApiModelProperty("支付宝")
+    private AlipayBindItem alipay;
+
+    @Data
+    @ApiModel("微信绑定项")
+    public static class WechatBindItem {
+        @ApiModelProperty("是否已绑定")
+        private Boolean bound;
+        @ApiModelProperty("微信昵称(与 accountLabel 同源,便于前端语义)")
+        private String nickName;
+        @ApiModelProperty("展示名(数据库 account_label)")
+        private String accountLabel;
+        @ApiModelProperty("平台账号标识:openid")
+        private String openid;
+        @ApiModelProperty("unionid")
+        private String unionid;
+        @ApiModelProperty("微信侧授权手机号")
+        private String bindPhone;
+    }
+
+    @Data
+    @ApiModel("支付宝绑定项")
+    public static class AlipayBindItem {
+        @ApiModelProperty("是否已绑定")
+        private Boolean bound;
+        @ApiModelProperty("支付宝昵称/展示名")
+        private String nickName;
+        @ApiModelProperty("展示名(数据库 account_label)")
+        private String accountLabel;
+        @ApiModelProperty("平台账号标识:支付宝 user_id")
+        private String alipayUserId;
+        @ApiModelProperty("支付宝侧授权手机号")
+        private String bindPhone;
+    }
+}

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/MyMergedReviewItemVo.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 「我的评价」融合列表单项:门店评价 + 律师订单评价
+ */
+@Data
+@ApiModel(value = "MyMergedReviewItemVo", description = "我的评价融合列表项")
+public class MyMergedReviewItemVo {
+
+    @ApiModelProperty(value = "类型:STORE-门店评价,LAWYER-律师订单评价", required = true)
+    private String itemType;
+
+    @ApiModelProperty(value = "门店评价主键(itemType=STORE 时有值)")
+    private Long storeRatingId;
+
+    @ApiModelProperty(value = "律师评价主键(itemType=LAWYER 时有值)")
+    private Integer lawyerReviewId;
+
+    @ApiModelProperty(value = "发布时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date publishTime;
+
+    @ApiModelProperty(value = "总评分/核心评分")
+    private Double score;
+
+    @ApiModelProperty(value = "文字内容")
+    private String content;
+
+    @ApiModelProperty(value = "图片/视频等媒体 URL 列表")
+    private List<String> mediaUrls;
+
+    @ApiModelProperty(value = "门店 id(STORE)")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "门店名称(STORE)")
+    private String storeName;
+
+    @ApiModelProperty(value = "门店图标/头图(STORE,可能为空)")
+    private String storeIcon;
+
+    @ApiModelProperty(value = "律师姓名(LAWYER)")
+    private String lawyerName;
+
+    @ApiModelProperty(value = "律师头像(LAWYER)")
+    private String lawyerAvatar;
+
+    @ApiModelProperty(value = "律所名称(LAWYER)")
+    private String lawyerFirm;
+
+    @ApiModelProperty(value = "评价标签文案,如「我给出超赞」(LAWYER)")
+    private String ratingTag;
+
+    @ApiModelProperty(value = "律师用户 id(LAWYER)")
+    private Integer lawyerUserId;
+
+    @ApiModelProperty(value = "关联订单 id(LAWYER)")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "门店评价业务类型 common_rating.business_type(STORE)")
+    private Integer businessType;
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderInfoVO.java

@@ -34,6 +34,9 @@ public class OrderInfoVO {
     @ApiModelProperty(value = "门店电话")
     @ApiModelProperty(value = "门店电话")
     private String storeTel;
     private String storeTel;
 
 
+    @ApiModelProperty(value = "smid")
+    private String smid;
+
     @ApiModelProperty(value = "门店地址")
     @ApiModelProperty(value = "门店地址")
     private String storeAddress;
     private String storeAddress;
 
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreClockInVo.java

@@ -15,6 +15,9 @@ public class StoreClockInVo extends StoreClockIn {
     @ApiModelProperty(value = "商家姓名")
     @ApiModelProperty(value = "商家姓名")
     private String storeName;
     private String storeName;
 
 
+    @ApiModelProperty(value = "门店简介")
+    private String storeBlurb;
+
     @ApiModelProperty(value = "商家位置")
     @ApiModelProperty(value = "商家位置")
     private String storePosition;
     private String storePosition;
 
 

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreLicenseInfoVo.java

@@ -47,6 +47,10 @@ public class StoreLicenseInfoVo {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date submitDate;
     private Date submitDate;
 
 
+    @ApiModelProperty("审核时间(门店 store_info.review_date)")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date reviewDate;
+
     @ApiModelProperty("证照到期时间")
     @ApiModelProperty("证照到期时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date expirationTime;
     private Date expirationTime;

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/AlipayZftCreateRecordMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.AlipayZftCreateRecord;
+
+/**
+ * 支付宝直付通二级商户标准进件提交记录 Mapper
+ */
+@Mapper
+public interface AlipayZftCreateRecordMapper extends BaseMapper<AlipayZftCreateRecord> {
+
+}

+ 2 - 0
alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java

@@ -36,9 +36,11 @@ public interface CommonCommentMapper extends BaseMapper<CommonComment> {
             "IF(cc.comment_type = 1, lu.user_image, su.head_img) AS headImg, " +
             "IF(cc.comment_type = 1, lu.user_image, su.head_img) AS headImg, " +
             "IF(cc.comment_type = 1, lu.user_name, su.nick_name) AS headName, " +
             "IF(cc.comment_type = 1, lu.user_name, su.nick_name) AS headName, " +
             "IF(cc.comment_type = 1, lu.user_phone, su.phone) AS headPhone, " +
             "IF(cc.comment_type = 1, lu.user_phone, su.phone) AS headPhone, " +
+            "IF(cc.comment_type = 2, si.store_name, NULL) AS storeName, " +
             "IF(llr.dianzan_id IS NULL, '0', '1') AS isLike " +
             "IF(llr.dianzan_id IS NULL, '0', '1') AS isLike " +
             "FROM common_comment cc " +
             "FROM common_comment cc " +
             "LEFT JOIN life_user lu ON cc.user_id = lu.id AND lu.delete_flag = 0 " +
             "LEFT JOIN life_user lu ON cc.user_id = lu.id AND lu.delete_flag = 0 " +
+            "LEFT JOIN store_info si ON cc.merchant_id = si.id AND si.delete_flag = 0 " +
             "LEFT JOIN store_user su ON cc.merchant_id = su.store_id AND su.delete_flag = 0 " +
             "LEFT JOIN store_user su ON cc.merchant_id = su.store_id AND su.delete_flag = 0 " +
             "AND su.id = (SELECT MIN(su2.id) FROM store_user su2 WHERE su2.store_id = cc.merchant_id AND su2.delete_flag = 0) " +
             "AND su.id = (SELECT MIN(su2.id) FROM store_user su2 WHERE su2.store_id = cc.merchant_id AND su2.delete_flag = 0) " +
             "LEFT JOIN life_like_record llr ON llr.huifu_id = cc.id " +
             "LEFT JOIN life_like_record llr ON llr.huifu_id = cc.id " +

+ 20 - 10
alien-entity/src/main/java/shop/alien/mapper/LifeFansMapper.java

@@ -15,8 +15,9 @@ import java.util.List;
 @Mapper
 @Mapper
 public interface LifeFansMapper extends BaseMapper<LifeFans> {
 public interface LifeFansMapper extends BaseMapper<LifeFans> {
 
 
-    @Select("select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.username) username, MAX(foll.accountBlurb) accountBlurb, MAX(foll.isMerchant) isMerchant, " +
-            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowMe, 1 as isFollowThis,  " +
+    @Select("<script>" +
+            "select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.username) username, MAX(foll.accountBlurb) accountBlurb, MAX(foll.isMerchant) isMerchant, " +
+            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowMe, MAX(if(isnull(fans_this.id), 0, 1)) isFollowThis, MAX(if(isnull(lb.id), '0', '1')) isBlocked,  " +
             "  (select count(1) from life_fans where fans_id= foll.phoneId and delete_flag =0) followNum, " +
             "  (select count(1) from life_fans where fans_id= foll.phoneId and delete_flag =0) followNum, " +
             "  (select count(1) from life_fans where followed_id= foll.phoneId and delete_flag =0) fansNum from ( " +
             "  (select count(1) from life_fans where followed_id= foll.phoneId and delete_flag =0) fansNum from ( " +
             "    with follow as (   " +
             "    with follow as (   " +
@@ -31,16 +32,20 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    join store_info info on info.id = user.store_id " +
             "    join store_info info on info.id = user.store_id " +
             "    left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "    left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "    where foll.flag = 'store' and user.delete_flag = 0 and info.delete_flag = 0 " +
             "    where foll.flag = 'store' and user.delete_flag = 0 and info.delete_flag = 0 " +
+            "<if test=\"onlyStoreFollowed == false\">" +
             "    union " +
             "    union " +
             "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId,'' username, '' accountBlurb, '0' AS isMerchant " +
             "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId,'' username, '' accountBlurb, '0' AS isMerchant " +
             "    from follow foll " +
             "    from follow foll " +
             "    join life_user user on foll.phone = user.user_phone   " +
             "    join life_user user on foll.phone = user.user_phone   " +
             "    where foll.flag = 'user' and user.delete_flag = 0   " +
             "    where foll.flag = 'user' and user.delete_flag = 0   " +
+            "</if>" +
             ") foll   " +
             ") foll   " +
-            "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{fansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{relationFansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans_this on fans_this.fans_id = #{relationFansId} and fans_this.followed_id = foll.phoneId and fans_this.delete_flag = 0 " +
             "left join life_blacklist lb on lb.blocked_type = foll.blockedType and lb.blocked_id = foll.blockedId and lb.blocker_type = #{blockerType} and lb.blocker_id = #{blockerId} and lb.delete_flag = 0 " +
             "left join life_blacklist lb on lb.blocked_type = foll.blockedType and lb.blocked_id = foll.blockedId and lb.blocker_type = #{blockerType} and lb.blocker_id = #{blockerId} and lb.delete_flag = 0 " +
-            "${ew.customSqlSegment} ")
-    IPage<LifeFansVo> getMyFollowed(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
+            "${ew.customSqlSegment} " +
+            "</script>")
+    IPage<LifeFansVo> getMyFollowed(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("relationFansId") String relationFansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param("onlyStoreFollowed") boolean onlyStoreFollowed, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
 
 
 
 
     @Select("select foll.*, if(isnull(fans.id), 0, 1) isFollowMe, 1 as isFollowThis from ( " +
     @Select("select foll.*, if(isnull(fans.id), 0, 1) isFollowMe, 1 as isFollowThis from ( " +
@@ -64,8 +69,9 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{fansId} and fans.delete_flag = 0 ")
             "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{fansId} and fans.delete_flag = 0 ")
     List<LifeFansVo> getMyFollowedAll(@Param("fansId") String fansId);
     List<LifeFansVo> getMyFollowedAll(@Param("fansId") String fansId);
 
 
-    @Select("select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.isMerchant) isMerchant, " +
-            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowThis, 1 as isFollowMe, " +
+    @Select("<script>" +
+            "select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.isMerchant) isMerchant, " +
+            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowThis, MAX(if(isnull(fans_me.id), 0, 1)) isFollowMe, MAX(if(isnull(lb.id), '0', '1')) isBlocked, " +
             "    (select count(1) from life_fans fans2 where fans2.followed_id = foll.phoneId and fans2.delete_flag = 0) fansNum, " +
             "    (select count(1) from life_fans fans2 where fans2.followed_id = foll.phoneId and fans2.delete_flag = 0) fansNum, " +
             "    (select count(1) from life_fans fans3 where fans3.fans_id = foll.phoneId and fans3.delete_flag = 0) followNum " +
             "    (select count(1) from life_fans fans3 where fans3.fans_id = foll.phoneId and fans3.delete_flag = 0) followNum " +
             "from ( " +
             "from ( " +
@@ -80,16 +86,20 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    join store_info info on info.id = user.store_id " +
             "    join store_info info on info.id = user.store_id " +
             "    left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "    left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "    where foll.flag = 'store' and user.delete_flag = 0 and info.delete_flag = 0" +
             "    where foll.flag = 'store' and user.delete_flag = 0 and info.delete_flag = 0" +
+            "<if test=\"onlyStoreFans == false\">" +
             "    union " +
             "    union " +
             "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId, '0' AS isMerchant" +
             "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId, '0' AS isMerchant" +
             "    from follow foll " +
             "    from follow foll " +
             "    join life_user user on foll.phone = user.user_phone " +
             "    join life_user user on foll.phone = user.user_phone " +
             "    where foll.flag = 'user' and user.delete_flag = 0 " +
             "    where foll.flag = 'user' and user.delete_flag = 0 " +
+            "</if>" +
             ") foll " +
             ") foll " +
-            "left join life_fans fans on fans.followed_id = foll.phoneId and fans.fans_id = #{fansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans on fans.followed_id = foll.phoneId and fans.fans_id = #{relationFansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans_me on fans_me.fans_id = foll.phoneId and fans_me.followed_id = #{relationFansId} and fans_me.delete_flag = 0 " +
             "left join life_blacklist lb on lb.blocked_type = foll.blockedType and lb.blocked_id = foll.blockedId and lb.blocker_type = #{blockerType} and lb.blocker_id = #{blockerId} and lb.delete_flag = 0 " +
             "left join life_blacklist lb on lb.blocked_type = foll.blockedType and lb.blocked_id = foll.blockedId and lb.blocker_type = #{blockerType} and lb.blocker_id = #{blockerId} and lb.delete_flag = 0 " +
-            "${ew.customSqlSegment} ")
-    IPage<LifeFansVo> getMyFans(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
+            "${ew.customSqlSegment} " +
+            "</script>")
+    IPage<LifeFansVo> getMyFans(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("relationFansId") String relationFansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param("onlyStoreFans") boolean onlyStoreFans, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
 
 
 //    @Select("select foll.*, if(isnull(fans.id), 0, 1) isFollowThis, 1 as isFollowMe, count(fans2.id) fansNum, count(fans3.id) followNum from ( " +
 //    @Select("select foll.*, if(isnull(fans.id), 0, 1) isFollowThis, 1 as isFollowMe, count(fans2.id) fansNum, count(fans3.id) followNum from ( " +
 //            "    with follow as ( " +
 //            "    with follow as ( " +

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeUserPersonalizationSettingMapper.java

@@ -0,0 +1,7 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+
+public interface LifeUserPersonalizationSettingMapper extends BaseMapper<LifeUserPersonalizationSetting> {
+}

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeUserPushDeviceMapper.java

@@ -0,0 +1,7 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.LifeUserPushDevice;
+
+public interface LifeUserPushDeviceMapper extends BaseMapper<LifeUserPushDevice> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeUserThirdBindMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.LifeUserThirdBind;
+
+@Mapper
+public interface LifeUserThirdBindMapper extends BaseMapper<LifeUserThirdBind> {
+}

+ 1 - 1
alien-entity/src/main/java/shop/alien/mapper/StoreClockInMapper.java

@@ -22,7 +22,7 @@ import java.util.Map;
  */
  */
 public interface StoreClockInMapper extends BaseMapper<StoreClockIn> {
 public interface StoreClockInMapper extends BaseMapper<StoreClockIn> {
 
 
-    @Select("select clock.id, clock.user_id, clock.store_id, clock.permission, user.user_name, store.store_name,store.score_avg score, clock.like_count, store.store_position, clock.content, clock.created_time, clock.img_url clockImg, img.img_url storeImg, user.user_image userImg,clock.maybe_ai_content," +
+    @Select("select clock.id, clock.user_id, clock.store_id, clock.permission, user.user_name, store.store_name, store.store_blurb storeBlurb, store.score_avg score, clock.like_count, store.store_position, clock.content, clock.created_time, clock.img_url clockImg, img.img_url storeImg, user.user_image userImg,clock.maybe_ai_content," +
             "store.administrative_region_district_name region, dict.dict_detail storeType, concat('user_', user.user_phone) phoneId, store.business_section,store.business_section_name,business_types_name,img1.img_url entranceImage," +
             "store.administrative_region_district_name region, dict.dict_detail storeType, concat('user_', user.user_phone) phoneId, store.business_section,store.business_section_name,business_types_name,img1.img_url entranceImage," +
             "( " +
             "( " +
             " select ifnull(round(sum(uorder.price) / count(1), 0), 0) " +
             " select ifnull(round(sum(uorder.price) / count(1), 0), 0) " +

+ 1 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreInfoMapper.java

@@ -182,6 +182,7 @@ public interface StoreInfoMapper extends BaseMapper<StoreInfo> {
             "left join store_dictionary e on e.type_name = 'storeType' and e.dict_id = a.store_type and e.delete_flag = 0 " +
             "left join store_dictionary e on e.type_name = 'storeType' and e.dict_id = a.store_type and e.delete_flag = 0 " +
             "${ew.customSqlSegment} " +
             "${ew.customSqlSegment} " +
             "and a.store_position is not null and a.store_position != '' " +
             "and a.store_position is not null and a.store_position != '' " +
+            "and a.head_img_status = 1 " +
             "order by distance3 asc")
             "order by distance3 asc")
     List<StoreInfoVo> getMoreRecommendedStores(@Param(Constants.WRAPPER) QueryWrapper<StoreInfoVo> queryWrapper, @Param("position") String position);
     List<StoreInfoVo> getMoreRecommendedStores(@Param(Constants.WRAPPER) QueryWrapper<StoreInfoVo> queryWrapper, @Param("position") String position);
 
 

+ 11 - 0
alien-entity/src/main/java/shop/alien/mapper/WechatPartnerApplymentMapper.java

@@ -0,0 +1,11 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.WechatPartnerApplyment;
+
+/**
+ * 微信支付服务商-特约商户进件申请单 Mapper
+ */
+public interface WechatPartnerApplymentMapper extends BaseMapper<WechatPartnerApplyment> {
+}
+

+ 1 - 0
alien-entity/src/main/resources/mapper/StoreInfoMapper.xml

@@ -33,6 +33,7 @@
             s.business_types_name,
             s.business_types_name,
             slh.license_execute_status AS states,
             slh.license_execute_status AS states,
             slh.created_time AS submit_date,
             slh.created_time AS submit_date,
+            s.review_date AS review_date,
             CASE
             CASE
                 WHEN slh.license_status = 1 THEN s.business_license_expiration_time
                 WHEN slh.license_status = 1 THEN s.business_license_expiration_time
                 ELSE NULL
                 ELSE NULL

+ 8 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/BaseRedisService.java

@@ -83,6 +83,14 @@ public class BaseRedisService {
     }
     }
 
 
     /**
     /**
+     * 判断 Set 中是否包含指定成员
+     */
+    public boolean isSetMember(String key, String member) {
+        Boolean m = stringRedisTemplate.opsForSet().isMember(key, member);
+        return m != null && m;
+    }
+
+    /**
      * 添加String值, 不设置过期时间
      * 添加String值, 不设置过期时间
      *
      *
      * @param key   键
      * @param key   键

+ 22 - 11
alien-gateway/src/main/java/shop/alien/gateway/config/JwtTokenFilter.java

@@ -122,20 +122,31 @@ public class JwtTokenFilter implements GlobalFilter, Ordered {
                     //不限制
                     //不限制
                     return allowChain(exchange, chain);
                     return allowChain(exchange, chain);
                 } else if ("user".equals(deviceType) || "miniprogram_user".equals(deviceType)) {
                 } else if ("user".equals(deviceType) || "miniprogram_user".equals(deviceType)) {
-                    // 用户/小程序:兼容 openid(点餐小程序存 miniprogram_user_token:{openid})与手机号(miniprogram_user_{phone} / user_{phone})
-                    List<String> candidateKeys = new ArrayList<>();
+                    // 用户/小程序:openid 仍为单 string;手机号维度支持多设备 Set(user_sessions: / miniprogram_user_sessions:)并兼容旧 string key
                     if (StringUtils.isNotBlank(openid)) {
                     if (StringUtils.isNotBlank(openid)) {
-                        candidateKeys.add("miniprogram_user_token:" + openid);
-                    }
-                    if (StringUtils.isNotBlank(phone)) {
-                        candidateKeys.add("miniprogram_user_" + phone);
-                        candidateKeys.add("user_" + phone);
-                    }
-                    for (String key : candidateKeys) {
-                        String val = baseRedisService.getString(key);
+                        String val = baseRedisService.getString("miniprogram_user_token:" + openid);
                         if (StringUtils.isNotBlank(val) && token.equals(val)) {
                         if (StringUtils.isNotBlank(val) && token.equals(val)) {
                             redisVal = val;
                             redisVal = val;
-                            break;
+                        }
+                    }
+                    if (redisVal == null && StringUtils.isNotBlank(phone)) {
+                        if (baseRedisService.isSetMember("miniprogram_user_sessions:" + phone, token)) {
+                            redisVal = token;
+                        } else {
+                            String mpVal = baseRedisService.getString("miniprogram_user_" + phone);
+                            if (StringUtils.isNotBlank(mpVal) && token.equals(mpVal)) {
+                                redisVal = mpVal;
+                            }
+                        }
+                    }
+                    if (redisVal == null && StringUtils.isNotBlank(phone)) {
+                        if (baseRedisService.isSetMember("user_sessions:" + phone, token)) {
+                            redisVal = token;
+                        } else {
+                            String userVal = baseRedisService.getString("user_" + phone);
+                            if (StringUtils.isNotBlank(userVal) && token.equals(userVal)) {
+                                redisVal = userVal;
+                            }
                         }
                         }
                     }
                     }
                 } else {
                 } else {

+ 22 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/LifeUserThirdBindProperties.java

@@ -0,0 +1,22 @@
+package shop.alien.gateway.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "life.user.third-bind")
+public class LifeUserThirdBindProperties {
+
+    private boolean enabled = true;
+
+    private Wechat wechat = new Wechat();
+
+    @Data
+    public static class Wechat {
+        /** 微信开放平台「移动应用」AppID / AppSecret(App 内绑定) */
+        private String mobileAppId;
+        private String mobileAppSecret;
+    }
+}

+ 96 - 0
alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserThirdBindController.java

@@ -0,0 +1,96 @@
+package shop.alien.gateway.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.LifeUserThirdBindInfoVo;
+import shop.alien.gateway.dto.LifeUserThirdBindWechatRequest;
+import shop.alien.gateway.service.LifeUserThirdBindService;
+import shop.alien.util.common.JwtUtil;
+
+import javax.validation.Valid;
+
+/**
+ * 用户第三方账号绑定(当前仅开放微信)
+ */
+@Api(tags = {"一期-用户-第三方绑定"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/user/third-bind")
+@RequiredArgsConstructor
+public class LifeUserThirdBindController {
+
+    private final LifeUserThirdBindService lifeUserThirdBindService;
+
+    @ApiOperation("查询当前用户微信绑定(昵称、openid、渠道手机号等)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/info")
+    public R<LifeUserThirdBindInfoVo> thirdBindInfo() {
+        try {
+            JSONObject sub = JwtUtil.getCurrentUserInfo();
+            if (sub == null) {
+                return R.fail("请登录");
+            }
+            Integer userId = lifeUserThirdBindService.resolveLifeUserId(sub);
+            if (userId == null) {
+                return R.fail("仅支持用户端查询");
+            }
+            return R.data(lifeUserThirdBindService.getThirdBindInfo(userId));
+        } catch (Exception e) {
+            log.warn("thirdBindInfo token parse failed", e);
+            return R.fail("登录已失效");
+        }
+    }
+
+    @ApiOperation("绑定微信(App 移动应用授权 code)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/wechat")
+    public R<LifeUserThirdBindInfoVo> thirdBindWechat(@Valid @RequestBody LifeUserThirdBindWechatRequest body) {
+        try {
+            JSONObject sub = JwtUtil.getCurrentUserInfo();
+            if (sub == null) {
+                return R.fail("请登录");
+            }
+            Integer userId = lifeUserThirdBindService.resolveLifeUserId(sub);
+            if (userId == null) {
+                return R.fail("仅支持用户端操作");
+            }
+            return lifeUserThirdBindService.bindWechat(
+                    userId, body.getCode(), body.getNickName(), body.getBindPhone());
+        } catch (Exception e) {
+            log.warn("thirdBindWechat failed", e);
+            return R.fail("登录已失效");
+        }
+    }
+
+    @ApiOperation("解绑微信")
+    @ApiOperationSupport(order = 3)
+    @PostMapping("/wechat/unbind")
+    public R<LifeUserThirdBindInfoVo> thirdBindWechatUnbind() {
+        try {
+            JSONObject sub = JwtUtil.getCurrentUserInfo();
+            if (sub == null) {
+                return R.fail("请登录");
+            }
+            Integer userId = lifeUserThirdBindService.resolveLifeUserId(sub);
+            if (userId == null) {
+                return R.fail("仅支持用户端操作");
+            }
+            return lifeUserThirdBindService.unbindWechat(userId);
+        } catch (Exception e) {
+            log.warn("thirdBindWechatUnbind failed", e);
+            return R.fail("登录已失效");
+        }
+    }
+}

+ 22 - 0
alien-gateway/src/main/java/shop/alien/gateway/dto/LifeUserThirdBindWechatRequest.java

@@ -0,0 +1,22 @@
+package shop.alien.gateway.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+@ApiModel("绑定微信请求")
+public class LifeUserThirdBindWechatRequest {
+
+    @NotBlank(message = "code 不能为空")
+    @ApiModelProperty(value = "微信开放平台移动应用 SDK 授权返回的 code", required = true)
+    private String code;
+
+    @ApiModelProperty("微信昵称(用户授权后由 App 取到并传入;换票接口不含昵称)")
+    private String nickName;
+
+    @ApiModelProperty("微信侧手机号(如开放平台/组件授权后由客户端传入,可选)")
+    private String bindPhone;
+}

+ 16 - 2
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java

@@ -89,7 +89,7 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
 //                userVo.setToken(JWTUtils1.createToken(tokenMap));
 //                userVo.setToken(JWTUtils1.createToken(tokenMap));
                 String token = getToken(phoneNum, userVo.getUserName(), tokenMap);
                 String token = getToken(phoneNum, userVo.getUserName(), tokenMap);
                 userVo.setToken(token);
                 userVo.setToken(token);
-                baseRedisService.setString("user_" + phoneNum, token);
+                addLifeUserSessionToken(phoneNum, token);
                 // 二手平台登录log,同一个macip登录多账号记录
                 // 二手平台登录log,同一个macip登录多账号记录
                 addLifeUserLogInfo(user2, macIp);
                 addLifeUserLogInfo(user2, macIp);
 
 
@@ -107,7 +107,7 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
             tokenMap.put("userType", "user");
             tokenMap.put("userType", "user");
             String token = getToken(phoneNum, user.getUserName(), tokenMap);
             String token = getToken(phoneNum, user.getUserName(), tokenMap);
             userVo.setToken(token);
             userVo.setToken(token);
-            baseRedisService.setString("user_" + phoneNum, token);
+            addLifeUserSessionToken(phoneNum, token);
             // 二手平台登录log,同一个macip登录多账号记录
             // 二手平台登录log,同一个macip登录多账号记录
             addLifeUserLogInfo(user, macIp);
             addLifeUserLogInfo(user, macIp);
 
 
@@ -141,6 +141,20 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
         return token;
         return token;
     }
     }
 
 
+    /**
+     * C 端多设备:token 放入 Redis Set;将旧版单 key 迁入 Set 后删除,避免旧 token 长期有效。
+     */
+    private void addLifeUserSessionToken(String phone, String token) {
+        String legacyKey = "user_" + phone;
+        String sessionSetKey = "user_sessions:" + phone;
+        String oldSingle = baseRedisService.getString(legacyKey);
+        if (oldSingle != null && !oldSingle.isEmpty()) {
+            baseRedisService.setSetList(sessionSetKey, oldSingle);
+        }
+        baseRedisService.delete(legacyKey);
+        baseRedisService.setSetList(sessionSetKey, token);
+    }
+
     public LifeUser getUserByPhone(String phoneNum) {
     public LifeUser getUserByPhone(String phoneNum) {
         LambdaQueryWrapper<LifeUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
         LambdaQueryWrapper<LifeUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
         lambdaQueryWrapper.eq(LifeUser::getUserPhone, phoneNum);
         lambdaQueryWrapper.eq(LifeUser::getUserPhone, phoneNum);

+ 207 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserThirdBindService.java

@@ -0,0 +1,207 @@
+package shop.alien.gateway.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.LifeUserThirdBind;
+import shop.alien.entity.store.vo.LifeUserThirdBindInfoVo;
+import shop.alien.gateway.config.LifeUserThirdBindProperties;
+import shop.alien.gateway.mapper.LifeUserGatewayMapper;
+import shop.alien.gateway.thirdparty.WeChatMobileOAuthClient;
+import shop.alien.mapper.LifeUserThirdBindMapper;
+
+import java.util.Date;
+
+@Service
+@RequiredArgsConstructor
+public class LifeUserThirdBindService {
+
+    private final LifeUserGatewayMapper lifeUserMapper;
+    private final LifeUserThirdBindMapper thirdBindMapper;
+    private final LifeUserThirdBindProperties thirdBindProperties;
+    private final WeChatMobileOAuthClient weChatMobileOAuthClient;
+
+    public Integer resolveLifeUserId(JSONObject sub) {
+        if (sub == null) {
+            return null;
+        }
+        String userType = sub.getString("userType");
+        if (!"user".equals(userType) && !"miniprogram_user".equals(userType)) {
+            return null;
+        }
+        String userIdStr = sub.getString("userId");
+        if (StringUtils.isNotBlank(userIdStr)) {
+            try {
+                return Integer.parseInt(userIdStr);
+            } catch (NumberFormatException ignored) {
+                // fall through
+            }
+        }
+        String phone = sub.getString("phone");
+        if (StringUtils.isNotBlank(phone)) {
+            LifeUser u = lifeUserMapper.selectOne(
+                    new LambdaQueryWrapper<LifeUser>().eq(LifeUser::getUserPhone, phone).last("LIMIT 1"));
+            return u != null ? u.getId() : null;
+        }
+        return null;
+    }
+
+    public LifeUserThirdBindInfoVo getThirdBindInfo(Integer lifeUserId) {
+        LifeUserThirdBindInfoVo vo = new LifeUserThirdBindInfoVo();
+        vo.setLifeUserId(lifeUserId);
+        LifeUser user = lifeUserMapper.selectById(lifeUserId);
+        if (user != null) {
+            vo.setLifeUserPhone(user.getUserPhone());
+        }
+        LifeUserThirdBind wechat = latestRow(lifeUserId, LifeUserThirdBind.PLATFORM_WECHAT);
+        LifeUserThirdBind alipay = latestRow(lifeUserId, LifeUserThirdBind.PLATFORM_ALIPAY);
+        vo.setWechat(toWechat(wechat));
+        vo.setAlipay(toAlipay(alipay));
+        return vo;
+    }
+
+    private LifeUserThirdBind latestRow(Integer lifeUserId, String platform) {
+        return thirdBindMapper.selectOne(
+                new LambdaQueryWrapper<LifeUserThirdBind>()
+                        .eq(LifeUserThirdBind::getLifeUserId, lifeUserId)
+                        .eq(LifeUserThirdBind::getPlatform, platform)
+                        .orderByDesc(LifeUserThirdBind::getUpdatedTime)
+                        .last("LIMIT 1"));
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public R<LifeUserThirdBindInfoVo> bindWechat(Integer lifeUserId, String code,
+                                                 String nickName, String bindPhone) {
+        if (!thirdBindProperties.isEnabled()) {
+            return R.fail("第三方绑定功能已关闭");
+        }
+        String guard = assertUserBindableOrMsg(lifeUserId);
+        if (guard != null) {
+            return R.fail(guard);
+        }
+        WeChatMobileOAuthClient.OAuthExchangeResult session = weChatMobileOAuthClient.exchangeCode(code);
+        if (!session.isSuccess()) {
+            String msg = session.getErrmsg();
+            if (session.getErrcode() != null) {
+                msg = StringUtils.defaultString(msg) + "(errcode=" + session.getErrcode() + ")";
+            }
+            return R.fail(StringUtils.defaultIfBlank(msg, "微信授权失败"));
+        }
+        String openid = session.getOpenid();
+        if (isWechatOpenidBoundToOtherUser(openid, lifeUserId)) {
+            return R.fail("该微信已绑定其他账号");
+        }
+        String label = StringUtils.trimToNull(nickName);
+        String phone = StringUtils.trimToNull(bindPhone);
+        upsertWechatRow(lifeUserId, openid, session.getUnionid(), label, phone);
+        return R.data(getThirdBindInfo(lifeUserId));
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public R<LifeUserThirdBindInfoVo> unbindWechat(Integer lifeUserId) {
+        if (!thirdBindProperties.isEnabled()) {
+            return R.fail("第三方绑定功能已关闭");
+        }
+        String guard = assertUserBindableOrMsg(lifeUserId);
+        if (guard != null) {
+            return R.fail(guard);
+        }
+        LifeUserThirdBind row = latestRow(lifeUserId, LifeUserThirdBind.PLATFORM_WECHAT);
+        if (row == null) {
+            return R.fail("当前未绑定微信");
+        }
+        thirdBindMapper.deleteById(row.getId());
+        return R.data(getThirdBindInfo(lifeUserId));
+    }
+
+    private String assertUserBindableOrMsg(Integer lifeUserId) {
+        LifeUser user = lifeUserMapper.selectById(lifeUserId);
+        if (user == null) {
+            return "用户不存在";
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            return "账号已注销,无法操作绑定";
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            return "账号已被封禁,无法操作绑定";
+        }
+        return null;
+    }
+
+    private boolean isWechatOpenidBoundToOtherUser(String openid, Integer lifeUserId) {
+        if (StringUtils.isBlank(openid)) {
+            return false;
+        }
+        long n = thirdBindMapper.selectCount(
+                new LambdaQueryWrapper<LifeUserThirdBind>()
+                        .eq(LifeUserThirdBind::getPlatform, LifeUserThirdBind.PLATFORM_WECHAT)
+                        .eq(LifeUserThirdBind::getOpenid, openid)
+                        .ne(LifeUserThirdBind::getLifeUserId, lifeUserId));
+        return n > 0;
+    }
+
+    private void upsertWechatRow(Integer lifeUserId, String openid, String unionid, String accountLabel, String bindPhone) {
+        Date now = new Date();
+        LifeUserThirdBind existing = latestRow(lifeUserId, LifeUserThirdBind.PLATFORM_WECHAT);
+        if (existing != null) {
+            existing.setOpenid(openid);
+            existing.setUnionid(unionid);
+            if (accountLabel != null) {
+                existing.setAccountLabel(accountLabel);
+            }
+            if (bindPhone != null) {
+                existing.setBindPhone(bindPhone);
+            }
+            existing.setUpdatedTime(now);
+            thirdBindMapper.updateById(existing);
+        } else {
+            LifeUserThirdBind row = new LifeUserThirdBind();
+            row.setLifeUserId(lifeUserId);
+            row.setPlatform(LifeUserThirdBind.PLATFORM_WECHAT);
+            row.setOpenid(openid);
+            row.setUnionid(unionid);
+            row.setAccountLabel(accountLabel);
+            row.setBindPhone(bindPhone);
+            row.setDeleteFlag(0);
+            row.setCreatedTime(now);
+            row.setUpdatedTime(now);
+            thirdBindMapper.insert(row);
+        }
+    }
+
+    private static LifeUserThirdBindInfoVo.WechatBindItem toWechat(LifeUserThirdBind row) {
+        LifeUserThirdBindInfoVo.WechatBindItem it = new LifeUserThirdBindInfoVo.WechatBindItem();
+        if (row == null) {
+            it.setBound(false);
+            return it;
+        }
+        it.setBound(true);
+        String label = row.getAccountLabel();
+        it.setNickName(label);
+        it.setAccountLabel(label);
+        it.setOpenid(row.getOpenid());
+        it.setUnionid(row.getUnionid());
+        it.setBindPhone(row.getBindPhone());
+        return it;
+    }
+
+    private static LifeUserThirdBindInfoVo.AlipayBindItem toAlipay(LifeUserThirdBind row) {
+        LifeUserThirdBindInfoVo.AlipayBindItem it = new LifeUserThirdBindInfoVo.AlipayBindItem();
+        if (row == null) {
+            it.setBound(false);
+            return it;
+        }
+        it.setBound(true);
+        String label = row.getAccountLabel();
+        it.setNickName(label);
+        it.setAccountLabel(label);
+        it.setAlipayUserId(row.getAlipayUserId());
+        it.setBindPhone(row.getBindPhone());
+        return it;
+    }
+}

+ 100 - 0
alien-gateway/src/main/java/shop/alien/gateway/thirdparty/WeChatMobileOAuthClient.java

@@ -0,0 +1,100 @@
+package shop.alien.gateway.thirdparty;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriComponentsBuilder;
+import shop.alien.gateway.config.LifeUserThirdBindProperties;
+
+import java.time.Duration;
+
+/**
+ * 微信开放平台「移动应用」OAuth2:App 内 SDK 授权 code 换 openid/unionid。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WeChatMobileOAuthClient {
+
+    private static final String OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
+
+    private final LifeUserThirdBindProperties properties;
+
+    @Value("${wechat.open.mobileAppId:}")
+    private String wechatMobileAppIdFallback;
+
+    @Value("${wechat.open.mobileAppSecret:}")
+    private String wechatMobileAppSecretFallback;
+
+    private final WebClient webClient = WebClient.builder().build();
+
+    public OAuthExchangeResult exchangeCode(String code) {
+        OAuthExchangeResult result = new OAuthExchangeResult();
+        String appId = StringUtils.firstNonBlank(
+                properties.getWechat().getMobileAppId(),
+                wechatMobileAppIdFallback);
+        String secret = StringUtils.firstNonBlank(
+                properties.getWechat().getMobileAppSecret(),
+                wechatMobileAppSecretFallback);
+        if (StringUtils.isBlank(appId) || StringUtils.isBlank(secret)) {
+            result.setErrmsg("未配置微信移动应用 appId/appSecret(life.user.third-bind.wechat.mobile-app-id 或 wechat.open.mobileAppId)");
+            return result;
+        }
+        if (StringUtils.isBlank(code)) {
+            result.setErrmsg("code 不能为空");
+            return result;
+        }
+        String url = UriComponentsBuilder.fromHttpUrl(OAUTH2_ACCESS_TOKEN_URL)
+                .queryParam("appid", appId)
+                .queryParam("secret", secret)
+                .queryParam("code", code)
+                .queryParam("grant_type", "authorization_code")
+                .build(true)
+                .toUriString();
+        try {
+            String body = webClient.get()
+                    .uri(url)
+                    .retrieve()
+                    .bodyToMono(String.class)
+                    .block(Duration.ofSeconds(15));
+            log.info("微信 App oauth2/access_token 已返回, bodyLength={}", body != null ? body.length() : 0);
+            if (StringUtils.isBlank(body)) {
+                result.setErrmsg("微信接口无响应");
+                return result;
+            }
+            JSONObject json = JSONObject.parseObject(body);
+            if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) {
+                result.setErrcode(json.getInteger("errcode"));
+                result.setErrmsg(json.getString("errmsg"));
+                return result;
+            }
+            result.setOpenid(json.getString("openid"));
+            result.setUnionid(json.getString("unionid"));
+            if (StringUtils.isBlank(result.getOpenid())) {
+                result.setErrmsg("未返回 openid");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("调用微信 App oauth2/access_token 异常", e);
+            result.setErrmsg("调用微信接口失败");
+            return result;
+        }
+    }
+
+    @Data
+    public static class OAuthExchangeResult {
+        private String openid;
+        private String unionid;
+        private Integer errcode;
+        private String errmsg;
+
+        public boolean isSuccess() {
+            return errcode == null && StringUtils.isNotBlank(openid);
+        }
+    }
+}

+ 6 - 0
alien-job/pom.xml

@@ -137,6 +137,12 @@
             <groupId>org.freemarker</groupId>
             <groupId>org.freemarker</groupId>
             <artifactId>freemarker</artifactId>
             <artifactId>freemarker</artifactId>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>shop.alien</groupId>
+            <artifactId>alien-store</artifactId>
+            <version>1.0.0</version>
+            <scope>compile</scope>
+        </dependency>
         <!-- mybatis-plus代码生成器 End -->
         <!-- mybatis-plus代码生成器 End -->
 
 
     </dependencies>
     </dependencies>

+ 9 - 4
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -2,10 +2,7 @@ package shop.alien.job.feign;
 
 
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.cloud.openfeign.FeignClient;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 
 
 import java.util.Map;
 import java.util.Map;
@@ -93,4 +90,12 @@ public interface AlienStoreFeign {
     @org.springframework.web.bind.annotation.PostMapping("/reservation/job/retryRefundFailed")
     @org.springframework.web.bind.annotation.PostMapping("/reservation/job/retryRefundFailed")
     R<Integer> retryRefundFailed();
     R<Integer> retryRefundFailed();
 
 
+    /**
+     * 特约商户进件:申请单号查询申请状态(转发微信 GET .../applyment_id/{applyment_id})。
+     * 需在配置中提供 {@code feign.alienStore.url} 指向 alien-store 根地址。
+     *
+     * @param applymentId 微信支付申请单号
+     */
+    @GetMapping("/payment/wechatPartner/v3/applyment4sub/applyment/applyment_id/{applyment_id}")
+    R<Map<String, Object>> queryWechatPartnerApplymentState(@PathVariable("applyment_id") Long applymentId);
 }
 }

+ 193 - 0
alien-job/src/main/java/shop/alien/job/jobhandler/WechatPartnerApplymentAuditSyncJobHandler.java

@@ -0,0 +1,193 @@
+package shop.alien.job.jobhandler;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.context.XxlJobHelper;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.WechatPartnerApplyment;
+import shop.alien.job.feign.AlienStoreFeign;
+import shop.alien.mapper.WechatPartnerApplymentMapper;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * XXL-JOB:定时查询微信进件审核信息,并更新本地表 {@code wechat_partner_applyment}。
+ * <p>
+ * 查询方式:通过 {@link AlienStoreFeign}(OpenFeign)调用 alien-store 转发查询接口,由 alien-store 负责对微信签名请求+验签;\n
+ * 本 Handler 解析返回数据,并按通过/驳回回写本地表。\n
+ * </p>
+ * <p>
+ * 待处理记录<strong>仅从数据库查询</strong>,不通过 XXL-Job 任务参数传入申请单号;\n
+ * 条数上限由配置 {@code job.wechatPartnerApplyment.batchSize} 控制。\n
+ * </p>
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WechatPartnerApplymentAuditSyncJobHandler {
+
+    private final AlienStoreFeign alienStoreFeign;
+    private final WechatPartnerApplymentMapper wechatPartnerApplymentMapper;
+
+    /**
+     * 【需配置】alien-store 根地址,与 {@link AlienStoreFeign} 的 {@code feign.alienStore.url} 一致;\n
+     * 仅用于 XXL 日志展示,实际请求走 Feign。
+     */
+    @Value("${feign.alienStore.url:}")
+    private String alienStoreFeignUrl;
+
+    /** 默认批量大小 */
+    @Value("${job.wechatPartnerApplyment.batchSize:1}")
+    private int batchSize;
+
+    /**
+     * XXL-JOB 任务:同步进件审核状态(通过/驳回均更新)。
+     * JobHandler 名称:{@code wechatPartnerApplymentAuditSync}
+     */
+    @XxlJob("wechatPartnerApplymentAuditSync")
+    public ReturnT<String> wechatPartnerApplymentAuditSync(String param) {
+        int shardIndex = XxlJobHelper.getShardIndex();
+        int shardTotal = XxlJobHelper.getShardTotal();
+
+        int limit = Math.max(1, Math.min(batchSize, 200));
+
+        XxlJobHelper.log("[进件XXL] start shardIndex={}, shardTotal={}, limit={}, feign.alienStore.url={}",
+                shardIndex, shardTotal, limit, alienStoreFeignUrl);
+
+        // 仅从库查询待同步:未终态(0) 且已有 applyment_id;分片广播:按 applyment_id % shardTotal 分片
+        List<WechatPartnerApplyment> list = wechatPartnerApplymentMapper.selectList(
+                new LambdaQueryWrapper<WechatPartnerApplyment>()
+                        .eq(WechatPartnerApplyment::getIsApproved, 0)
+                        .isNotNull(WechatPartnerApplyment::getApplymentId)
+                        .apply("MOD(applyment_id, {0}) = {1}", shardTotal, shardIndex)
+                        .orderByAsc(WechatPartnerApplyment::getUpdatedAt)
+                        .last("limit " + limit)
+        );
+
+        int ok = 0;
+        int fail = 0;
+        for (WechatPartnerApplyment r : list) {
+            Long id = r.getApplymentId();
+            if (id == null || id <= 0) {
+                continue;
+            }
+            try {
+                R<Map<String, Object>> result = alienStoreFeign.queryWechatPartnerApplymentState(id);
+                if (!R.isSuccess(result)) {
+                    fail++;
+                    String msg = result != null ? result.getMsg() : "响应为空";
+                    XxlJobHelper.log("[进件XXL] queryFail applyment_id={}, msg={}", id, msg);
+                    continue;
+                }
+                Map<String, Object> dataMap = result != null ? result.getData() : null;
+                if (dataMap == null || dataMap.isEmpty()) {
+                    fail++;
+                    XxlJobHelper.log("[进件XXL] queryFail applyment_id={}, data为空", id);
+                    continue;
+                }
+                JSONObject data = JSON.parseObject(JSON.toJSONString(dataMap));
+                String rawResp = JSON.toJSONString(result);
+                // 回写本地表(通过/驳回/进行中都更新状态字段)
+                upsertFromWechatQueryResult(r, data, rawResp);
+                ok++;
+                XxlJobHelper.log("[进件XXL] ok applyment_id={}, state={}", id, data.getString("applyment_state"));
+            } catch (Exception e) {
+                fail++;
+                log.error("[进件XXL] Feign 查询异常 applyment_id={}", id, e);
+                XxlJobHelper.log("[进件XXL] exception applyment_id={}, err={}", id, e.getMessage());
+            }
+        }
+
+        XxlJobHelper.log("[进件XXL] done candidate={}, ok={}, fail={}", list.size(), ok, fail);
+        return ReturnT.SUCCESS;
+    }
+
+    private void upsertFromWechatQueryResult(WechatPartnerApplyment existed, JSONObject data, String rawResp) {
+        Date now = new Date();
+        WechatPartnerApplyment record = existed != null ? existed : new WechatPartnerApplyment();
+        record.setLastQueryTime(now);
+        record.setLastQueryRespJson(rawResp);
+        record.setUpdatedAt(now);
+
+        record.setBusinessCode(blankToNull(data.getString("business_code")));
+        record.setSubMchid(blankToNull(data.getString("sub_mchid")));
+        record.setSignUrl(blankToNull(data.getString("sign_url")));
+        record.setApplymentState(blankToNull(data.getString("applyment_state")));
+        record.setApplymentStateMsg(blankToNull(data.getString("applyment_state_msg")));
+
+        Integer approved = calcIsApproved(record.getApplymentState());
+        record.setIsApproved(approved);
+        if (approved != null && approved == 2) {
+            record.setRejectReason(extractRejectReason(data));
+        } else if (approved != null && approved == 1) {
+            // 通过则清空驳回原因(避免旧值残留)
+            record.setRejectReason(null);
+        }
+
+        // 仅更新当前行
+        if (record.getId() != null) {
+            wechatPartnerApplymentMapper.updateById(record);
+        } else {
+            // 兜底:按 applyment_id 找不到 id 的情况,插入一条(不建议频繁发生)
+            record.setApplymentId(data.getLong("applyment_id"));
+            if (record.getCreatedAt() == null) {
+                record.setCreatedAt(now);
+            }
+            wechatPartnerApplymentMapper.insert(record);
+        }
+    }
+
+    private static Integer calcIsApproved(String applymentState) {
+        if (StringUtils.isBlank(applymentState)) {
+            return 0;
+        }
+        if ("APPLYMENT_STATE_FINISHED".equalsIgnoreCase(applymentState)) {
+            return 1;
+        }
+        if ("APPLYMENT_STATE_REJECTED".equalsIgnoreCase(applymentState)) {
+            return 2;
+        }
+        return 0;
+    }
+
+    private static String extractRejectReason(JSONObject data) {
+        try {
+            if (data == null || data.getJSONArray("audit_detail") == null) {
+                return null;
+            }
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < data.getJSONArray("audit_detail").size(); i++) {
+                JSONObject it = data.getJSONArray("audit_detail").getJSONObject(i);
+                if (it == null) continue;
+                String fieldName = it.getString("field_name");
+                String field = it.getString("field");
+                String reason = it.getString("reject_reason");
+                if (StringUtils.isBlank(reason)) continue;
+                if (sb.length() > 0) sb.append(";");
+                String left = StringUtils.isNotBlank(fieldName) ? fieldName : (StringUtils.isNotBlank(field) ? field : "");
+                if (StringUtils.isNotBlank(left)) sb.append(left).append(":");
+                sb.append(reason);
+            }
+            String s = sb.toString();
+            return s.length() <= 2048 ? s : s.substring(0, 2048);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static String blankToNull(String s) {
+        String t = s != null ? s.trim() : null;
+        return StringUtils.isNotBlank(t) ? t : null;
+    }
+}
+

+ 135 - 0
alien-job/src/main/java/shop/alien/job/store/AlipayJob.java

@@ -0,0 +1,135 @@
+package shop.alien.job.store;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.AntMerchantExpandIndirectZftorderQueryModel;
+import com.alipay.api.request.AntMerchantExpandIndirectZftorderQueryRequest;
+import com.alipay.api.response.AntMerchantExpandIndirectZftorderQueryResponse;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.store.AlipayZftCreateRecord;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.mapper.AlipayZftCreateRecordMapper;
+import shop.alien.store.service.StoreInfoService;
+
+import java.util.List;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AlipayJob {
+
+    private final AlipayZftCreateRecordMapper alipayZftCreateRecordMapper;
+
+    private final StoreInfoService storeInfoService;
+
+    /**
+     * 查询支付宝二级商户进件结果,并从响应中回写门店 smid。
+     */
+    @XxlJob("getAlipayPartnerQuery")
+    public void getAlipayPartnerQuery() throws AlipayApiException {
+        AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+
+        LambdaQueryWrapper<AlipayZftCreateRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AlipayZftCreateRecord::getInvokeSuccess, Boolean.TRUE);
+        wrapper.isNotNull(AlipayZftCreateRecord::getOrderId);
+        List<AlipayZftCreateRecord> records = alipayZftCreateRecordMapper.selectList(wrapper);
+
+        for (AlipayZftCreateRecord record : records) {
+            String orderId = record.getOrderId();
+            if (StringUtils.isBlank(orderId)) {
+                continue;
+            }
+
+            AntMerchantExpandIndirectZftorderQueryRequest request = new AntMerchantExpandIndirectZftorderQueryRequest();
+            AntMerchantExpandIndirectZftorderQueryModel model = new AntMerchantExpandIndirectZftorderQueryModel();
+            model.setOrderId(orderId.trim());
+            request.setBizModel(model);
+
+            AntMerchantExpandIndirectZftorderQueryResponse response = alipayClient.execute(request);
+            if (response == null) {
+                log.warn("order.query 返回为空 orderId={}", orderId);
+                continue;
+            }
+
+            if (!response.isSuccess()) {
+                log.warn("order.query 失败 orderId={} subMsg={}", orderId, response.getSubMsg());
+                continue;
+            }
+
+            String body = response.getBody();
+            String smid = extractSmidFromZftOrderQueryBody(body);
+            if (StringUtils.isBlank(smid)) {
+                log.debug("order.query 未解析到 smid orderId={}", orderId);
+                continue;
+            }
+
+            Integer storeId = record.getStoreId();
+            if (storeId == null) {
+                log.debug("进件记录无 storeId,跳过回写 orderId={}", orderId);
+                continue;
+            }
+
+            StoreInfo storeInfo = storeInfoService.getById(storeId);
+            if (storeInfo == null) {
+                log.warn("门店不存在 storeId={} orderId={}", storeId, orderId);
+                continue;
+            }
+
+            if (smid.equals(storeInfo.getAlipaySmid())) {
+                continue;
+            }
+
+            storeInfo.setAlipaySmid(smid);
+            storeInfoService.updateById(storeInfo);
+            log.info("已回写门店 alipay_smid storeId={} orderId={}", storeId, orderId);
+        }
+    }
+
+    /**
+     * 从 ant.merchant.expand.indirect.zftorder.query 响应 body 中取 orders 首条的 smid。
+     */
+    private static String extractSmidFromZftOrderQueryBody(String body) {
+        if (StringUtils.isBlank(body)) {
+            return null;
+        }
+        try {
+            JSONObject root = JSONObject.parseObject(body);
+            JSONObject resp = root.getJSONObject("ant_merchant_expand_indirect_zftorder_query_response");
+            if (resp == null) {
+                return null;
+            }
+            JSONArray orders = resp.getJSONArray("orders");
+            if (orders == null || orders.isEmpty()) {
+                return null;
+            }
+            JSONObject first = orders.getJSONObject(0);
+            return first == null ? null : first.getString("smid");
+        } catch (Exception e) {
+            log.warn("解析 zftorder.query 响应 body 失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private static AlipayConfig getAlipayConfig() {
+        String privateKey  = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCBTOvScNrk0mw+WbR6Qtx2+HrPxB5IU/V2HSPGZUTDNiCiSU9UZcuXlo0UNwMksFYR43btk4BHW9OsZETdr3uJzbEEko4jQPrY49V6V8d3yFUiexcbLKf9W2X+h7EwdPDahHk6wmM73Lm500xzv8JxdGeo/d2uGTh+qD66HcPW+QYj1YbjRV1JyCOiJ2OiwLcb28arNNLOgj6PktQ69FPSWS4hWrM7nl6DLRr6SdOlyfBT47gwPtW0BQ96/2b9O+qxKBPfrcqdRDH323HvrGFDwa8GoWTdchnKwFjmfIFHMvf+eq3tzilFi8H7vQRtzVRrndwrNa0z+1ss07fcWFDRAgMBAAECggEAItgU0OAizPk7vE22SiBMgy8RAX5rXrhpdIwDwQo3Tpf+kV1KKIdKJy6mFCWDDlcKysVOnlVag2BmmZVnzYnls8wfgQjxjuSK9Pno5JBVK51r+9/J6UPOfYMs6Duu700EPw7mEISj81TXJBGiD6tEfgiNisfm/mzDgbZbORKeXQbaTyrtB+GZn6FNSyyHA1vraARMrgfMEGNzQ4AbtfcUxGO+mejdTFs0PxAq6lovHBY3fYYHI1Nx6kf9iPoom/G4UrcMO67W6QU+1tOCZCXjy4bD2y421z/8XD73+WDyYp+Tjy0hTLqVZc7TpYAOximo1vMIUe23EdJJngdlkdpDFQKBgQDFyETL0knwBSakLfAe2BmFb2x++B4YXUnt4dGbCFBnVooxf5i06GVt/CrfkJhYK6hBSowOScIRf8P6BOSQptRZb2/I1ngQm4vcpAZw6EjUTlgOj/J3XJ+ApUNQnRqE28jDrE4m2RHg4BkQo6yA3DizJAluPCtFoCYDm1a7dV7u7wKBgQCnXEH5sD8VSxURv02/gn80g/uZIP/EOU3ycjBEqZdRGkNINwXT+zqrlZIGYb+bxLvU/R2OqKC5vhcyAL3T1A8ORYqPu5KLnAxg7C+rHuVilUWwCEH7POpCk+ETPXCZwcNvLNa5PIqBH/gdV9Y9PBTef6J4rN6V6TDFgosf5by8PwKBgDpVG71Fk1sAGep4RgbC05wgRc6Y3T9wXDqVzJ098YDY7D83E9HfbPLoWbjAS75Nef1vwCkCpgNFPIbD5KmpGp4aGM0SPC0hwzlbAy9PwxMi3CPHXsrHfZ+SnmzrOQQQUoErk40vnm9FiP74VwtWaD6llUZ25ohNeIi9yvHU5x/vAoGAdU2d1JOq85LHtsO+i9+8pyNnAsJ1YqTDtI5CtK2lqKvewswGIrlxOvi//AchVN3ExZmP0QDyfp31BhAs/T8iOl+Vqf7PzVjX+Eszch5aqwlzadmv3ZepnnamCGVE+hAsmkz0R6tebPjqYC7Ds/HbssQFLc4EyVBD5fwE5ZuR+OMCgYAvGHUYpi0dY9uMHXzL721tIopiwUfKCgLAn3yhSH3p7644NxHBqLLaBLVT2q7JAZQUaZUvXlwiyxU1zvo0xmvcbnB/Vd2qp8KbEUkvHyIYVJkM6Fn+9xBosorcrHv+7B2V1XR9WQcXvppxbN/8farWGuAA0anBD+UGrxd8B0/hHg==";
+        String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnxevClLpYc6c8A9lwmnftPO2BeU+X6aZ/+b/n1Cvq096VJKiHmqcsRgRbrnSlmPDHRrQpDti4en2Ys0L2lx7CObIr/2xP/jJVwjIO1iWHUj/w/NAbjv7dLW/FFY4SeNp8rU+hlgGgviyUxzonfNfr3v+o8grFqQq27/hiZJAofsQRMQu1dEQqoKdJj7eQLkTstiK5miJMyQ+Y3tLztrEUMBz/zRgaCEfGqmFmRZ2diy2X+1dGaX6H4+0QJ2u50eY2QTBkNuvREGbAn6/lttAgvg/+CywPYKGeC4xOfnl5wP8iA1QXYbXrVJRkZjU097nyOmSNhLy9KvJH2BNpojS1QIDAQAB";
+        AlipayConfig alipayConfig = new AlipayConfig();
+        alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
+        alipayConfig.setAppId("2021005196608960");
+        alipayConfig.setPrivateKey(privateKey);
+        alipayConfig.setFormat("json");
+        alipayConfig.setAlipayPublicKey(alipayPublicKey);
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+        return alipayConfig;
+    }
+}

+ 32 - 0
alien-lawyer/src/main/java/shop/alien/lawyer/controller/AlipayZftCreateRecordController.java

@@ -0,0 +1,32 @@
+package shop.alien.lawyer.controller;
+
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.lawyer.service.AlipayZftCreateRecordService;
+
+/**
+ * 支付宝直付通进件创建记录
+ */
+@Slf4j
+@Api(tags = {"律师平台-支付宝直付通进件"})
+@ApiSort(25)
+@CrossOrigin
+@RestController
+@RequestMapping("/lawyer/alipayZftCreateRecord")
+@RequiredArgsConstructor
+public class AlipayZftCreateRecordController {
+
+    private final AlipayZftCreateRecordService alipayZftCreateRecordService;
+
+    @ApiOperation("根据商户名称(law_firm)查询是否存在已填写支付宝二级商户账号的律师用户")
+    @ApiOperationSupport(order = 1)
+    @ApiImplicitParam(name = "merchantName", value = "商户名称", required = true, paramType = "query", dataType = "String")
+    @GetMapping("/existsSuccessful")
+    public R<Boolean> existsByMerchantName(@RequestParam("merchantName") String merchantName) {
+        log.info("AlipayZftCreateRecordController.existsByMerchantName?merchantName={}", merchantName);
+        return alipayZftCreateRecordService.existsByMerchantName(merchantName);
+    }
+}

+ 17 - 0
alien-lawyer/src/main/java/shop/alien/lawyer/service/AlipayZftCreateRecordService.java

@@ -0,0 +1,17 @@
+package shop.alien.lawyer.service;
+
+import shop.alien.entity.result.R;
+
+/**
+ * 支付宝直付通进件创建记录
+ */
+public interface AlipayZftCreateRecordService {
+
+    /**
+     * 按商户名称(对应 {@code lawyer_user.law_firm})查询是否存在已绑定支付宝二级商户账号的律师
+     *
+     * @param merchantName 商户名称(律所名称)
+     * @return true 存在且 {@code zfb_secondary_merchant_account} 有有效值,否则 false
+     */
+    R<Boolean> existsByMerchantName(String merchantName);
+}

+ 36 - 0
alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/AlipayZftCreateRecordServiceImpl.java

@@ -0,0 +1,36 @@
+package shop.alien.lawyer.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LawyerUser;
+import shop.alien.lawyer.service.AlipayZftCreateRecordService;
+import shop.alien.mapper.LawyerUserMapper;
+
+/**
+ * 支付宝直付通进件创建记录(按律所名称关联律师用户二级商户账号)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlipayZftCreateRecordServiceImpl implements AlipayZftCreateRecordService {
+
+    private final LawyerUserMapper lawyerUserMapper;
+
+    @Override
+    public R<Boolean> existsByMerchantName(String merchantName) {
+        if (!StringUtils.hasText(merchantName)) {
+            return R.data(false);
+        }
+        LambdaQueryWrapper<LawyerUser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LawyerUser::getLawFirm, merchantName.trim())
+                .eq(LawyerUser::getDeleteFlag, 0)
+                .isNotNull(LawyerUser::getZfbSecondaryMerchantAccount)
+                .ne(LawyerUser::getZfbSecondaryMerchantAccount, "");
+        long n = lawyerUserMapper.selectCount(wrapper);
+        return R.data(n > 0);
+    }
+}

+ 2 - 2
alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerConsultationOrderServiceImpl.java

@@ -565,9 +565,9 @@ public class LawyerConsultationOrderServiceImpl extends ServiceImpl<LawyerConsul
 
 
         // 生成订单编号:LAW + 年月日(8位数字)+ 随机5位数字
         // 生成订单编号:LAW + 年月日(8位数字)+ 随机5位数字
         String orderNumber = generateOrderNumber();
         String orderNumber = generateOrderNumber();
-        order.setOrderNumber(orderNumber);
+        order.setOrderNumber(!StringUtils.isEmpty(lawyerConsultationOrder.getOrderNumber())?lawyerConsultationOrder.getOrderNumber():orderNumber);
         log.info("生成订单编号:orderNumber={}", orderNumber);
         log.info("生成订单编号:orderNumber={}", orderNumber);
-
+        order.setAlipayNo(lawyerConsultationOrder.getOrderNumber());
         // 计算本单收益(平台收益)
         // 计算本单收益(平台收益)
         Integer consultationFee = calculateConsultationFee(lawyerUserId, orderAmount);
         Integer consultationFee = calculateConsultationFee(lawyerUserId, orderAmount);
         order.setConsultationFee(consultationFee);
         order.setConsultationFee(consultationFee);

+ 21 - 21
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java

@@ -880,27 +880,27 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
         }
         }
 
 
         // 视频审核
         // 视频审核
-        List<String> taskIds = performVideoReviews(goods, goodsDTO);
-
-        // 如果成功提交了视频审核任务,设置商品状态为审核中
-        if (!taskIds.isEmpty()) {
-            goods.setGoodsStatus(SecondGoodsStatusEnum.UNDER_REVIEW.getCode()); // 审核中
-            goods.setVideoTaskId(taskIds.get(0)); // 保存第一个任务ID到商品表
-            goods.setFailedReason("");
-            updateById(goods);
-            // 审核中审核记录
-            createGoodsAudit(goods, "", Constants.AuditStatus.UNDER_REVIEW);
-            // 审核中,记录操作历史
-//            recordGoodsOperation(goods);
-            return;
-        }
-
-        List<String> videoUrls = extractVideoUrls(goodsDTO.getImgUrl());
-        if (videoUrls.isEmpty()) {
-            // ai 审核
-            secondGoodsAuditService.performSecondRoundReview(goods, goodsDTO);
-            return;
-        }
+//        List<String> taskIds = performVideoReviews(goods, goodsDTO);
+//
+//        // 如果成功提交了视频审核任务,设置商品状态为审核中
+//        if (!taskIds.isEmpty()) {
+//            goods.setGoodsStatus(SecondGoodsStatusEnum.UNDER_REVIEW.getCode()); // 审核中
+//            goods.setVideoTaskId(taskIds.get(0)); // 保存第一个任务ID到商品表
+//            goods.setFailedReason("");
+//            updateById(goods);
+//            // 审核中审核记录
+//            createGoodsAudit(goods, "", Constants.AuditStatus.UNDER_REVIEW);
+//            // 审核中,记录操作历史
+////            recordGoodsOperation(goods);
+//            return;
+//        }
+
+//        List<String> videoUrls = extractVideoUrls(goodsDTO.getImgUrl());
+//        if (videoUrls.isEmpty()) {
+//            // ai 审核
+//            secondGoodsAuditService.performSecondRoundReview(goods, goodsDTO);
+//            return;
+//        }
 
 
         // 审核通过后上架商品
         // 审核通过后上架商品
         approveAndListGoods(goods);
         approveAndListGoods(goods);

+ 38 - 22
alien-second/src/main/java/shop/alien/second/task/Task.java

@@ -27,7 +27,6 @@ import shop.alien.second.feign.AlienStoreFeign;
 
 
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.ZoneId;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
@@ -317,41 +316,58 @@ public class Task {
     }
     }
 
 
     /**
     /**
-     * 二手交易平台 - 到达交易时间12个小时后,自动交易失败
+     * 二手交易平台 - 约定交易时间(transaction_time)起已满 24 小时,买卖双方仍未均确认「交易成功」则自动失败:
+     * buyer_transaction_status / seller_transaction_status 均为 1 表示该侧在成交确认里选了成功;否则(含 0 未确认、2 失败、仅一侧成功)满足时间条件则判败。
      */
      */
+    private static final String SUCCESS_CONFIRM_TIMEOUT_FAIL_REASON = "约定交易时间起满24小时双方未完成交易成功确认,系统自动标记交易失败";
+
     @Scheduled(cron = "0 * * * * ?")
     @Scheduled(cron = "0 * * * * ?")
-    private void secondTradeTimeoutFail() {
+    private void secondTradeUnconfirmedSuccessTimeoutFail() {
         if (!isEnable) {
         if (!isEnable) {
             return;
             return;
         }
         }
 
 
-        log.info("开始执行定时任务: 二手交易平台 - 到达交易时间12个小时后,自动交易失败 - secondTradeTimeoutFail");
+        log.info("开始执行定时任务: 二手交易平台 - 约定交易时间满24小时未双方确认交易成功自动失败 - secondTradeUnconfirmedSuccessTimeoutFail");
         try {
         try {
-            Date twelveBefore = Date.from(LocalDateTime.now().plusHours(12).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant());
+            // 约定交易时间 <= (当前整分 - 24h),即约定时刻至今已至少经过 24 小时
+            Date cutoff = Date.from(LocalDateTime.now().minusHours(24).withSecond(0).withNano(0).atZone(ZoneId.systemDefault()).toInstant());
 
 
-            // 查询所有待确认
             LambdaQueryWrapper<SecondTradeRecord> queryWrapper = new LambdaQueryWrapper<>();
             LambdaQueryWrapper<SecondTradeRecord> queryWrapper = new LambdaQueryWrapper<>();
             queryWrapper.eq(SecondTradeRecord::getTradeStatus, 3);
             queryWrapper.eq(SecondTradeRecord::getTradeStatus, 3);
-            queryWrapper.eq(SecondTradeRecord::getTransactionTime, twelveBefore);
+            queryWrapper.isNotNull(SecondTradeRecord::getTransactionTime);
+            queryWrapper.le(SecondTradeRecord::getTransactionTime, cutoff);
+            // 未双方均确认成功(1):任一侧非 1(未确认/已选失败/仅一侧成功)即命中
+            queryWrapper.apply("(IFNULL(buyer_transaction_status,0) <> 1 OR IFNULL(seller_transaction_status,0) <> 1)");
             List<SecondTradeRecord> tradeRecordList = secondTradeRecordMapper.selectList(queryWrapper);
             List<SecondTradeRecord> tradeRecordList = secondTradeRecordMapper.selectList(queryWrapper);
-            List<Integer> tradeIdList = tradeRecordList.stream().map(SecondTradeRecord::getId).collect(Collectors.toList());
-            if (CollectionUtil.isNotEmpty(tradeIdList)) {
-                LambdaUpdateWrapper<SecondTradeRecord> updateWrapper = new LambdaUpdateWrapper<>();
-                updateWrapper.in(SecondTradeRecord::getId, tradeIdList)
-                        .set(SecondTradeRecord::getTradeStatus, 5);
-                secondTradeRecordMapper.update(null, updateWrapper);
-
-                for (Integer id : tradeIdList) {
-                    SecondTradeOperation operation = new SecondTradeOperation();
-                    operation.setTradeId(id);
-                    operation.setUserId(0);
-                    operation.setType(6);
-                    operation.setCreatedTime(new Date());
-                    secondTradeOperationMapper.insert(operation);
+            if (CollectionUtil.isEmpty(tradeRecordList)) {
+                return;
+            }
+            Date opTime = new Date();
+            // 按单条更新且要求仍为「待交易」,避免多实例 @Scheduled 重复插入操作流水(批量 IN 更新无法区分本实例是否真正抢到行)
+            for (SecondTradeRecord tr : tradeRecordList) {
+                Integer id = tr.getId();
+                if (id == null) {
+                    continue;
+                }
+                LambdaUpdateWrapper<SecondTradeRecord> uw = new LambdaUpdateWrapper<>();
+                uw.eq(SecondTradeRecord::getId, id)
+                        .eq(SecondTradeRecord::getTradeStatus, 3)
+                        .set(SecondTradeRecord::getTradeStatus, 5)
+                        .set(SecondTradeRecord::getFailureFlag, "1")
+                        .set(SecondTradeRecord::getFailureReason, SUCCESS_CONFIRM_TIMEOUT_FAIL_REASON);
+                int updated = secondTradeRecordMapper.update(null, uw);
+                if (updated != 1) {
+                    continue;
                 }
                 }
+                SecondTradeOperation operation = new SecondTradeOperation();
+                operation.setTradeId(id);
+                operation.setUserId(0);
+                operation.setType(6);
+                operation.setCreatedTime(opTime);
+                secondTradeOperationMapper.insert(operation);
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
-            log.error("SecondGoodsTradeXxlJob.secondTradeTimeoutFail Error Mgs={}", e.getMessage());
+            log.error("Task.secondTradeUnconfirmedSuccessTimeoutFail Error Msg={}", e.getMessage(), e);
         }
         }
     }
     }
 
 

+ 104 - 0
alien-store/docs/short-link.md

@@ -0,0 +1,104 @@
+# 短链接(Short Link)说明
+
+`alien-store` 模块提供「长链 → 短链 → 302 跳转」能力,供前端生成微信分享等场景的短 URL。映射存储在 **Redis**,键前缀为 `short:link:`。
+
+## 接口
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| `POST` | `/store/short-link/shorten` | 请求体:`{"longUrl":"<完整长链接>"}`,成功返回 `shortUrl`、`code` |
+| `GET` | `/store/short-link/s/{code}` | 返回 `R<ShortLinkResolveResponse>`,其中 `data.longUrl` 为原始长链;失败为业务失败文案 |
+| `GET` | `/store/short-link/s/{code}/open` | **302** 跳转至长链(浏览器直开/书签);不存在或过期 **404** |
+
+网关需将上述路径路由到 `alien-store`。JSON 接口的返回体为项目统一的 `R<T>`。
+
+**说明**:分享出去的短链地址一般为 `…/store/short-link/s/{code}`。若在微信内由 **小程序 / H5 请求**解析,请调 **`GET /s/{code}`** 取 `longUrl` 后再 `navigateTo` 或 `location.replace`。若需 **系统浏览器打开即跳**,可把分享链指向 **`/s/{code}/open`**,或继续用原短链前缀但由落地页先请求 `/s/{code}` 再跳转。
+
+## 配置项
+
+| 配置键 | 说明 | 默认值 |
+|--------|------|--------|
+| `short-link.public-base-url` | 对外短链前缀(**无尾斜杠**),须含协议与端口(若有)。含网关 `context-path` 时写全 | 空(未配置时生成接口会报错) |
+| `short-link.allowed-hosts` | 允许被缩短的**长链**域名白名单,逗号分隔(小写 host)。**空**表示不按白名单限制(仍会拦截内网,除非开发开关打开) | 空 |
+| `short-link.https-only` | 长链是否**仅允许 https** | `true` |
+| `short-link.allow-local-network-target` | 是否允许长链指向 localhost / 内网 IP。**仅本地开发** | `false` |
+| `short-link.ttl-days` | Redis 中映射保留天数 | `30` |
+
+生成的完整短链形如(解析用第一段 URL 即可;若需 302 直达可加 `/open`):
+
+`{short-link.public-base-url}/store/short-link/s/{code}`
+
+## 各环境推荐配置
+
+### 开发环境(dev)
+
+适合本机或内网联调;长链可能是 `http://127.0.0.1`、`192.168.x`。
+
+```yaml
+short-link:
+  public-base-url: http://127.0.0.1:8080
+  https-only: false
+  allow-local-network-target: true
+  allowed-hosts:
+  ttl-days: 30
+```
+
+手机同 WiFi 联调时,将 `public-base-url` 改为 **`http://<本机局域网IP>:端口`**。微信内打开需可访问该地址;公网联调可用 ngrok 等穿透,将前缀改为穿透提供的 `https://...`。
+
+### 测试环境(test / sit)
+
+```yaml
+short-link:
+  public-base-url: https://test.ailien.shop
+  https-only: true
+  allow-local-network-target: false
+  allowed-hosts: test.ailien.shop,www.test.ailien.shop,m.test.ailien.shop
+  ttl-days: 30
+```
+
+若测试环境暂无 HTTPS,可临时 `https-only: false`,**不要**在生产打开 `allow-local-network-target`。
+
+### 预生产环境(uat / pre)
+
+与生产策略一致,仅换预发域名。
+
+```yaml
+short-link:
+  public-base-url: https://uat.ailien.shop
+  https-only: true
+  allow-local-network-target: false
+  allowed-hosts: uat.ailien.shop,www.uat.ailien.shop,m.uat.ailien.shop
+  ttl-days: 30
+```
+
+### 生产环境(prod)
+
+```yaml
+short-link:
+  public-base-url: https://prod.ailien.shop
+  https-only: true
+  allow-local-network-target: false
+  allowed-hosts: prod.ailien.shop,www.prod.ailien.shop,m.prod.ailien.shop
+  ttl-days: 30
+```
+
+生产环境**必须** `allow-local-network-target: false`,并**强烈建议**配置 `allowed-hosts`,缩小开放重定向面。
+
+## Nacos 建议
+
+- 各环境差异项(`public-base-url`、`allowed-hosts`、`https-only`、`allow-local-network-target`)放在对应环境的 data-id(如 `alien-store-dev.yml`、`alien-store-prod.yml`)。
+- 共性项(如 `ttl-days`)可放在 `common.yml`。
+
+## 相关代码
+
+| 类型 | 路径 |
+|------|------|
+| Controller | `shop.alien.store.controller.ShortLinkController` |
+| Service | `shop.alien.store.service.impl.ShortLinkServiceImpl` |
+| 工具 | `shop.alien.store.util.Base62Util`、`shop.alien.store.util.ShortLinkUrlValidator` |
+| DTO | `shop.alien.store.dto.ShortLinkShortenRequest`、`ShortLinkShortenResponse` |
+
+## 安全说明
+
+- 长链会做基本校验:协议、长度、(可选)域名白名单、内网地址拦截(可通过 `allow-local-network-target` 在**仅开发**放开)。
+- 跳转接口为公开访问,请勿在生产开启 `allow-local-network-target`。

+ 47 - 0
alien-store/src/main/java/shop/alien/store/config/UniPushProperties.java

@@ -0,0 +1,47 @@
+package shop.alien.store.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+/**
+ * uniCloud 云函数 URL 化后,由 Java 侧发起推送的 HTTP 配置。
+ * 云函数内需使用 uni-cloud-push 的 sendMessage;请求体字段需与云函数约定一致。
+ */
+@Data
+@Component
+@RefreshScope
+@ConfigurationProperties(prefix = "alien.unipush")
+public class UniPushProperties {
+
+    /**
+     * 是否启用 HTTP 调用云函数推送(未配置 url 时 {@link shop.alien.store.service.UniPushCloudInvokeService} 会直接跳过)
+     */
+    private boolean enabled = false;
+
+    /**
+     * 云函数 URL 化完整地址,例如 https://xxx.com/push/send
+     */
+    private String cloudFunctionUrl = "";
+
+    /**
+     * 可选:鉴权请求头名,如 X-Api-Secret
+     */
+    private String authHeaderName = "";
+
+    /**
+     * 可选:鉴权请求头值
+     */
+    private String authHeaderValue = "";
+
+    /**
+     * 连接超时毫秒
+     */
+    private int connectTimeoutMs = 5000;
+
+    /**
+     * 读超时毫秒
+     */
+    private int readTimeoutMs = 10000;
+}

+ 2 - 2
alien-store/src/main/java/shop/alien/store/controller/AiAuditController.java

@@ -50,10 +50,10 @@ public class AiAuditController {
     @Value("${third-party-login.base-url:http://192.168.2.250:9100/ai/user-auth-core/api/v1/auth/login}")
     @Value("${third-party-login.base-url:http://192.168.2.250:9100/ai/user-auth-core/api/v1/auth/login}")
     private String loginUrl;
     private String loginUrl;
 
 
-    @Value("${third-party-text-check.base-url:http://124.93.18.180:8892/api/v1/moderate}")
+    @Value("${third-party-text-check.base-url:http://verify.ailien.shop:8099/api/v1/moderate}")
     private String aiTextCheckUrl;
     private String aiTextCheckUrl;
 
 
-    @Value("${third-party-content_compliance-check.base-url:http://124.93.18.180:8892/api/v1/moderate}")
+    @Value("${third-party-content_compliance-check.base-url:http://verify.ailien.shop:8099/api/v1/moderate}")
     private String aiContentCheckUrl;
     private String aiContentCheckUrl;
 
 
     @ApiOperation("ai文本审核")
     @ApiOperation("ai文本审核")

+ 62 - 0
alien-store/src/main/java/shop/alien/store/controller/AlipayPartnerPaymentController.java

@@ -0,0 +1,62 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.store.strategy.payment.PaymentStrategyFactory;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Api(tags = {"支付宝服务商支付接口"})
+@CrossOrigin
+@RestController
+@RequestMapping("/alipayPartnerPayment")
+@RequiredArgsConstructor
+public class AlipayPartnerPaymentController {
+
+    private final PaymentStrategyFactory paymentStrategyFactory;
+
+    /**
+     * 支付宝支付回调通知接口
+     * 异步回调地址状态码 (http 状态码) 为 200 时表示异步通知成功,返回码为 404 或 500 时则表示服务器内部错误,需要商家自行排查。
+     * @param notifyData 回调 JSON 报文
+     * @return 204 无 body 或 5XX + 失败报文
+     */
+    /**
+     * 服务商模式支付宝支付回调(与直连支付宝回调分离,需在商户平台配置 notify_url 指向本地址)
+     */
+    @RequestMapping("/alipayPartnerNotify")
+    public ResponseEntity<?> alipayPartnerNotify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[支付宝服务商回调] 收到请求, Content-Length={}, Alipay-Serial={}",
+                request.getContentLength(), request.getHeader("Alipay-Serial"));
+        try {
+            R result = paymentStrategyFactory.getStrategy(PaymentEnum.ALIPAY_PARTNER.getType()).handleNotify(notifyData);
+            if (R.isSuccess(result)) {
+                return ResponseEntity.noContent().build();
+            }
+            String message = result.getMsg() != null ? result.getMsg() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", message);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        } catch (Exception e) {
+            log.error("[支付宝服务商回调] 处理异常", e);
+            String msg = e.getMessage() != null ? e.getMessage() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", msg);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        }
+    }
+}

+ 111 - 0
alien-store/src/main/java/shop/alien/store/controller/AlipayZftOnboardingController.java

@@ -0,0 +1,111 @@
+package shop.alien.store.controller;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AntMerchantExpandIndirectZftCreateModel;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.AlipayZftBizRequestDto;
+import shop.alien.entity.store.dto.AlipayZftMerchantCreateDto;
+import shop.alien.entity.store.dto.AlipayZftMerchantSimplecreateDto;
+import shop.alien.store.service.AlipayZftOnboardingService;
+
+/**
+ * 支付宝直付通二级商户入驻(接口 API 进件)— 对应开放平台
+ * ant.merchant.expand.indirect.zft.* / ant.merchant.expand.order.query 等。
+ *
+ * @see <a href="https://opendocs.alipay.com/solution/0dec7x">支付宝方案文档</a>
+ */
+@Slf4j
+@Api(tags = {"支付宝直付通-二级商户进件"})
+@ApiSort(19)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/alipay/zft")
+@RequiredArgsConstructor
+public class AlipayZftOnboardingController {
+
+    private final AlipayZftOnboardingService alipayZftOnboardingService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation(value = "进件前咨询", notes = "ant.merchant.expand.indirect.zft.consult")
+    @PostMapping("/consult")
+    public R<String> consult(@RequestBody AlipayZftBizRequestDto request) {
+        log.info("AlipayZftOnboardingController.consult storeId={}", request != null ? request.getStoreId() : null);
+        return alipayZftOnboardingService.zftConsult(request);
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation(value = "二级商户标准进件", notes = "ant.merchant.expand.indirect.zft.create;业务参数可与官方网关一致放在 body.biz_content(JSON 对象),"
+            + "或顶层 external_id 等与 extraBiz 合并;app_auth_token、notify_url、app_id 等同 curl 公共参数")
+    @PostMapping("/create")
+    public R<String> create(@RequestBody AntMerchantExpandIndirectZftCreateModel request,
+                            @RequestParam(value = "storeId", required = false) Integer storeId) throws AlipayApiException {
+        log.info("AlipayZftOnboardingController.create storeId={}", storeId);
+        return alipayZftOnboardingService.zftCreate(request, storeId);
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation(value = "二级商户简化进件(免证照)", notes = "ant.merchant.expand.indirect.zft.simplecreate,结构化 body 对应 AntMerchantExpandIndirectZftSimplecreateModel")
+    @PostMapping("/simplecreate")
+    public R<String> simplecreate(@RequestBody AlipayZftMerchantSimplecreateDto request) {
+        log.info("AlipayZftOnboardingController.simplecreate storeId={}", request != null ? request.getStoreId() : null);
+        return alipayZftOnboardingService.zftSimplecreate(request);
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation(value = "申请单查询", notes = "ant.merchant.expand.order.query;传 orderId(申请单号)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "orderId", value = "申请单 id(支付宝 order_id)", required = true, dataType = "string", paramType = "query")
+    })
+    @PostMapping("/order/query")
+    public R<String> orderQuery(@RequestParam String orderId) {
+        log.info("AlipayZftOnboardingController.orderQuery orderId={}", orderId);
+        return alipayZftOnboardingService.orderQuery(orderId);
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation(value = "二级商户信息修改", notes = "ant.merchant.expand.indirect.zft.modify")
+    @PostMapping("/modify")
+    public R<String> modify(@RequestBody AlipayZftBizRequestDto request) throws AlipayApiException {
+        log.info("AlipayZftOnboardingController.modify storeId={}", request != null ? request.getStoreId() : null);
+        return alipayZftOnboardingService.zftModify(request);
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation(value = "申请单补件", notes = "ant.merchant.expand.indirect.supplement.create")
+    @PostMapping("/supplement/create")
+    public R<String> supplementCreate(@RequestBody AlipayZftBizRequestDto request) {
+        log.info("AlipayZftOnboardingController.supplementCreate storeId={}", request != null ? request.getStoreId() : null);
+        return alipayZftOnboardingService.supplementCreate(request);
+    }
+
+    @ApiOperationSupport(order = 7)
+    @ApiOperation(value = "进件图片上传", notes = "ant.merchant.expand.indirect.image.upload")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID(取对应支付宝配置)", required = true, dataType = "int", paramType = "form"),
+            @ApiImplicitParam(name = "imageType", value = "图片格式:bmp/jpg/jpeg/png/gif", required = true, dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "imageContent", value = "图片文件(二进制,<=10MB)", required = true, dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "app_id", value = "支付宝应用 app_id;不传则使用门店支付配置", required = false, dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "appAuthToken", value = "应用授权令牌 app_auth_token(第三方代调用时选填)", required = false, dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "apiVersion", value = "接口版本 api_version(一般无需传)", required = false, dataType = "string", paramType = "form")
+    })
+    @PostMapping("/image/upload")
+    public R<String> imageUpload(@RequestParam(required = false) Integer storeId,
+                                 @RequestParam String imageType,
+                                 @RequestParam("imageContent") MultipartFile imageContent,
+                                 @RequestParam(value = "app_id", required = false) String appId,
+                                 @RequestParam(required = false) String appAuthToken,
+                                 @RequestParam(required = false) String apiVersion) throws AlipayApiException {
+        log.info("AlipayZftOnboardingController.imageUpload storeId={}, imageType={}", storeId, imageType);
+        return alipayZftOnboardingService.imageUpload(storeId, imageType, imageContent, appId, appAuthToken, apiVersion);
+    }
+}

+ 38 - 0
alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java

@@ -5,7 +5,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import shop.alien.entity.store.CommonRating;
 import shop.alien.entity.store.CommonRating;
+import shop.alien.entity.store.vo.MyMergedReviewItemVo;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.CommonRatingService;
@@ -190,4 +192,40 @@ public class CommonRatingController {
         return commonRatingService.getMyRatingList(pageNum, pageSize, businessType, userId, auditStatus);
         return commonRatingService.getMyRatingList(pageNum, pageSize, businessType, userId, auditStatus);
     }
     }
 
 
+    @ApiOperation(value = "融合查询我的评价列表",
+            notes = "【请求】GET /commonRating/getMyMergedRatingList\n\n"
+                    + "【功能】合并「门店评价 common_rating」与「律师订单评价」,按发布时间倒序(新在前);服务端合并后再分页。\n\n"
+                    + "【查询参数】\n"
+                    + "- pageNum:页码,默认 1\n"
+                    + "- pageSize:每页条数,默认 10\n"
+                    + "- businessType:门店侧 business_type,不传默认 1(门店评价),语义同 getMyRatingList\n"
+                    + "- userId:必填,被查询的用户 ID(Long)\n"
+                    + "- auditStatus:可选,仅过滤门店评价侧;律师订单评价不受此参数影响\n"
+                    + "- currentUserId:可选,传入时用于律师评价侧「是否已点赞」等当前用户态\n\n"
+                    + "【响应】外层统一 R;success=true 时 data 为分页对象(IPage),元素类型 MyMergedReviewItemVo。\n"
+                    + "- data.records:列表项,itemType=STORE 为门店评价,LAWYER 为律师订单评价,字段见模型说明\n"
+                    + "- data.total:门店评价总数 + 律师评价总数(合并前各自 total 之和)\n"
+                    + "- data.current、data.size、data.pages:标准分页字段\n\n"
+                    + "【错误】userId 为空时返回失败(msg 提示)。")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码,默认 1", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "pageSize", value = "每页条数,默认 10", dataType = "Integer", paramType = "query", defaultValue = "10"),
+            @ApiImplicitParam(name = "businessType", value = "门店侧业务类型,不传默认 1-门店评价", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "userId", value = "用户 ID(必填)", dataType = "Long", paramType = "query", required = true, example = "10001"),
+            @ApiImplicitParam(name = "auditStatus", value = "门店评价审核状态筛选,可选;律师侧不过滤", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "currentUserId", value = "当前登录用户 ID,可选;律师评价用于点赞态等", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/getMyMergedRatingList")
+    public R<IPage<MyMergedReviewItemVo>> getMyMergedRatingList(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) Integer businessType,
+            @RequestParam Long userId,
+            @RequestParam(required = false) Integer auditStatus,
+            @RequestParam(required = false) Integer currentUserId) {
+        log.info("CommonRatingController.getMyMergedRatingList?pageNum={}&pageSize={}&businessType={}&userId={}&auditStatus={}&currentUserId={}",
+                pageNum, pageSize, businessType, userId, auditStatus, currentUserId);
+        return commonRatingService.getMyMergedRatingList(pageNum, pageSize, businessType, userId, auditStatus, currentUserId);
+    }
+
 }
 }

+ 16 - 8
alien-store/src/main/java/shop/alien/store/controller/LifeStoreController.java

@@ -59,11 +59,15 @@ public class LifeStoreController {
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "页数", dataType = "Integer", paramType = "query", required = true),
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "页数", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "页容", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "页容", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "fansId", value = "粉丝id", dataType = "String", paramType = "query", required = true),
             @ApiImplicitParam(name = "fansId", value = "粉丝id", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query")})
+            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "type", value = "筛选:不传或0=全部关注;1且fansId为店铺时仅店铺关注", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "myFansId", value = "可选;有值时关注/拉黑四字段相对该 id,列表仍按 fansId", dataType = "String", paramType = "query")})
     @GetMapping("/getMyFollowed")
     @GetMapping("/getMyFollowed")
-    public R<IPage<LifeFansVo>> getMyFollowed(@RequestParam("fansId") String fansId, @RequestParam("name") String name, @RequestParam("page") int page, @RequestParam("size") int size) {
-        log.info("LifeStoreController.getMyFollowed?fansId={},name={},page={},size={}", fansId, name, page, size);
-        return R.data(lifeStoreService.getMyFollowed(fansId, name, page, size));
+    public R<IPage<LifeFansVo>> getMyFollowed(@RequestParam("fansId") String fansId, @RequestParam("name") String name, @RequestParam("page") int page, @RequestParam("size") int size,
+                                              @RequestParam(required = false) Integer type,
+                                              @RequestParam(required = false) String myFansId) {
+        log.info("LifeStoreController.getMyFollowed?fansId={},name={},page={},size={},type={},myFansId={}", fansId, name, page, size, type, myFansId);
+        return R.data(lifeStoreService.getMyFollowed(fansId, name, page, size, type, myFansId));
     }
     }
 
 
     @ApiOperationSupport(order = 3)
     @ApiOperationSupport(order = 3)
@@ -71,11 +75,15 @@ public class LifeStoreController {
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "页数", dataType = "Integer", paramType = "query", required = true),
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "页数", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "页容", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "页容", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "fansId", value = "粉丝id", dataType = "String", paramType = "query", required = true),
             @ApiImplicitParam(name = "fansId", value = "粉丝id", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query")})
+            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "type", value = "筛选:不传或0=全部粉丝;1且fansId为店铺时仅店铺粉丝", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "myFansId", value = "可选;有值时关注/拉黑四字段相对该 id,列表仍按 fansId", dataType = "String", paramType = "query")})
     @GetMapping("/getMyFans")
     @GetMapping("/getMyFans")
-    public R<IPage<LifeFansVo>> getMyFans(String fansId, @RequestParam(defaultValue = "") String name, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) {
-        log.info("LifeStoreController.getMyFans?fansId={},name={},page={},size={}", fansId, name, page, size);
-        return R.data(lifeStoreService.getMyFans(fansId, name, page, size));
+    public R<IPage<LifeFansVo>> getMyFans(String fansId, @RequestParam(defaultValue = "") String name, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size,
+                                          @RequestParam(required = false) Integer type,
+                                          @RequestParam(required = false) String myFansId) {
+        log.info("LifeStoreController.getMyFans?fansId={},name={},page={},size={},type={},myFansId={}", fansId, name, page, size, type, myFansId);
+        return R.data(lifeStoreService.getMyFans(fansId, name, page, size, type, myFansId));
     }
     }
 
 
     @ApiOperationSupport(order = 4)
     @ApiOperationSupport(order = 4)

+ 84 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserPersonalizationSettingController.java

@@ -0,0 +1,84 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+import shop.alien.store.service.LifeUserPersonalizationSettingService;
+
+@Api(tags = {"用户个性化设置"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/lifeUserPersonalizationSetting")
+@RequiredArgsConstructor
+public class LifeUserPersonalizationSettingController {
+
+    private final LifeUserPersonalizationSettingService lifeUserPersonalizationSettingService;
+
+    @ApiOperation("新增个性化设置")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/add")
+    public R<String> add(@RequestBody LifeUserPersonalizationSetting setting) {
+        return lifeUserPersonalizationSettingService.add(setting);
+    }
+
+    @ApiOperation("根据主键删除(逻辑删除)")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "主键ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/deleteById")
+    public R<String> deleteById(@RequestParam Integer id) {
+        return lifeUserPersonalizationSettingService.deleteById(id);
+    }
+
+    @ApiOperation("更新个性化设置;传 id 或传 userId 均可定位记录(无 userId 对应记录时会先插入默认再更新)")
+    @ApiOperationSupport(order = 3)
+    @PostMapping("/update")
+    public R<String> update(@RequestBody LifeUserPersonalizationSetting setting) {
+        return lifeUserPersonalizationSettingService.update(setting);
+    }
+
+    @ApiOperation("根据主键查询")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "主键ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getById")
+    public R<LifeUserPersonalizationSetting> getById(@RequestParam Integer id) {
+        return lifeUserPersonalizationSettingService.getInfoById(id);
+    }
+
+    @ApiOperation("根据用户ID查询;若无记录则自动插入一条默认值后返回")
+    @ApiOperationSupport(order = 5)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getByUserId")
+    public R<LifeUserPersonalizationSetting> getByUserId(@RequestParam Integer userId) {
+        return lifeUserPersonalizationSettingService.getByUserId(userId);
+    }
+
+    @ApiOperation("分页列表,可按 userId 筛选")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "pageSize", value = "每页数量", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/list")
+    public R<IPage<LifeUserPersonalizationSetting>> list(
+            @RequestParam Integer pageNum,
+            @RequestParam Integer pageSize,
+            @RequestParam(required = false) Integer userId) {
+        return lifeUserPersonalizationSettingService.list(pageNum, pageSize, userId);
+    }
+}

+ 62 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserPushDeviceController.java

@@ -0,0 +1,62 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPushDevice;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.store.dto.LifeUserPushBindDto;
+import shop.alien.store.service.LifeUserPushDeviceService;
+import shop.alien.util.common.TokenInfo;
+import springfox.documentation.annotations.ApiIgnore;
+
+import java.util.List;
+
+/**
+ * uni-push 等设备 cid 绑定;业务主键与类型取自登录态 JWT(userId + userType),不信任客户端传参区分身份。
+ * <p>userType:user → life_user.id;store/merchant → store_user.id;lawyer → lawyer_user.id</p>
+ */
+@Api(tags = {"用户推送设备"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/lifeUserPushDevice")
+@RequiredArgsConstructor
+public class LifeUserPushDeviceController {
+
+    private final LifeUserPushDeviceService lifeUserPushDeviceService;
+
+    @ApiOperation("绑定当前用户与 push cid(登录后调用)")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/bind")
+    public R<String> bind(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                          @RequestBody LifeUserPushBindDto dto) {
+        log.info("LifeUserPushDeviceController.bind, userId={}",
+                userLoginInfo == null ? null : userLoginInfo.getUserId());
+        return lifeUserPushDeviceService.bind(userLoginInfo, dto);
+    }
+
+    @ApiOperation("解绑指定 cid(仅删除当前用户名下记录)")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pushClientId", value = "cid", required = true, paramType = "query", dataType = "String")
+    })
+    @PostMapping("/unbind")
+    public R<String> unbind(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                            @RequestParam String pushClientId) {
+        return lifeUserPushDeviceService.unbindByCid(userLoginInfo, pushClientId);
+    }
+
+    @ApiOperation("当前用户已绑定的推送设备列表")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/listMine")
+    public R<List<LifeUserPushDevice>> listMine(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo) {
+        return lifeUserPushDeviceService.listMine(userLoginInfo);
+    }
+}

+ 89 - 10
alien-store/src/main/java/shop/alien/store/controller/PaymentController.java

@@ -7,13 +7,20 @@ import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
+import shop.alien.store.feign.DiningServiceFeign;
 import shop.alien.store.strategy.payment.PaymentStrategyFactory;
 import shop.alien.store.strategy.payment.PaymentStrategyFactory;
+import shop.alien.util.common.constant.PaymentEnum;
 
 
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
 /**
 /**
@@ -30,32 +37,95 @@ import java.util.Map;
 public class PaymentController {
 public class PaymentController {
 
 
     private final PaymentStrategyFactory paymentStrategyFactory;
     private final PaymentStrategyFactory paymentStrategyFactory;
+    private final DiningServiceFeign diningServiceFeign;
 
 
+    private static String authHeader(HttpServletRequest request) {
+        String h = request.getHeader("Authorization");
+        if (h == null || h.isEmpty()) {
+            h = request.getHeader("authorization");
+        }
+        return h;
+    }
 
 
     @ApiOperation("创建预支付订单")
     @ApiOperation("创建预支付订单")
     @ApiImplicitParams({
     @ApiImplicitParams({
             @ApiImplicitParam(name = "price", value = "订单金额", required = true, paramType = "query", dataType = "String"),
             @ApiImplicitParam(name = "price", value = "订单金额", required = true, paramType = "query", dataType = "String"),
             @ApiImplicitParam(name = "subject", value = "订单标题", required = true, paramType = "query", dataType = "String"),
             @ApiImplicitParam(name = "subject", value = "订单标题", required = true, paramType = "query", dataType = "String"),
-            @ApiImplicitParam(name = "payType", value = "支付类型(alipay:支付宝, wechatPay:微信支付)", required = true, paramType = "query", dataType = "String")
+            @ApiImplicitParam(name = "smid", value = "smid", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "支付类型(alipay:支付宝, wechatPay:微信支付, wechatPayPartner:微信服务商模式, wechatPayMininProgram:微信小程序点餐)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID(wechatPayPartner 必填,用于读取 store_info.wechat_sub_mchid)", required = false, paramType = "query", dataType = "int")
     })
     })
     @RequestMapping("/prePay")
     @RequestMapping("/prePay")
-    public R prePay(String price, String subject, String payType) {
-        log.info("PaymentController:prePay, price: {}, subject: {}, payType: {}", price, subject, payType);
+    public R prePay(
+            HttpServletRequest request,
+            @RequestParam String price,
+            @RequestParam String subject,
+            @RequestParam String payType,
+            @RequestParam(required = false) String smid,
+            @RequestParam(required = false) String payer,
+            @RequestParam(required = false) String orderNo,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) Integer couponId,
+            @RequestParam(required = false) Integer payerId,
+            @RequestParam(required = false) String tablewareFee,
+            @RequestParam(required = false) String discountAmount,
+            @RequestParam(required = false) String payAmount) {
+        log.info("PaymentController:prePay, price: {}, subject: {}, payType: {}, orderNo: {}, storeId: {}", price, subject, payType, orderNo, storeId);
         try {
         try {
-            return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(price, subject);
+//            if ("wechatPayMininProgram".equals(payType) && storeId != null && orderNo != null && payer != null) {
+//                return diningServiceFeign.prePay(
+//                        authHeader(request), price, subject, payType, payer, orderNo, storeId,
+//                        couponId, payerId, tablewareFee, discountAmount, payAmount);
+//            }
+            // 微信服务商 APP:传入 orderNo 时与小程序策略一致,绑定/刷新 store_order 与微信商户单号
+            if ("wechatPayPartner".equals(payType) && storeId != null && orderNo != null) {
+                return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(
+                        price, subject, payer, orderNo, storeId, couponId, payerId,
+                        tablewareFee, discountAmount, payAmount);
+            }
+            if ("aliPayPartner".equals(payType)) {
+                return paymentStrategyFactory.getStrategy(payType).createPreAliPayOrder(orderNo, price, subject, smid, storeId);
+            }
+            return paymentStrategyFactory.getStrategy(payType).createPrePayOrder(price, subject, storeId);
         } catch (Exception e) {
         } catch (Exception e) {
             return R.fail(e.getMessage());
             return R.fail(e.getMessage());
         }
         }
     }
     }
 
 
     /**
     /**
-     * 通知接口 之后可能会用
-     * @param notifyData
-     * @return
+     * 微信服务商 APP 支付回调(需在商户平台将 notify_url 配置为本地址;验签通过后返回 204)
+     */
+    @RequestMapping("/weChatPartnerNotify")
+    public ResponseEntity<?> weChatPartnerNotify(@RequestBody String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[微信服务商APP回调] 收到请求, Content-Length={}, Wechatpay-Serial={}",
+                request.getContentLength(), request.getHeader("Wechatpay-Serial"));
+        try {
+            R<?> result = paymentStrategyFactory.getStrategy(PaymentEnum.WECHAT_PAY_PARTNER.getType())
+                    .handleNotify(notifyData, request);
+            if (R.isSuccess(result)) {
+                return ResponseEntity.noContent().build();
+            }
+            String message = result != null && result.getMsg() != null ? result.getMsg() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", message);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        } catch (Exception e) {
+            log.error("[微信服务商APP回调] 处理异常", e);
+            String msg = e.getMessage() != null ? e.getMessage() : "失败";
+            Map<String, String> failBody = new HashMap<>(2);
+            failBody.put("code", "FAIL");
+            failBody.put("message", msg);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failBody);
+        }
+    }
+
+    /**
+     * 预留:非微信 V3 回调(当前未使用)
      */
      */
     @RequestMapping("/notify")
     @RequestMapping("/notify")
     public R notify(String notifyData) {
     public R notify(String notifyData) {
-        return  null;
+        return null;
     }
     }
 
 
     /**
     /**
@@ -64,10 +134,19 @@ public class PaymentController {
      * @param payType 支付类型
      * @param payType 支付类型
      * @return 订单状态信息
      * @return 订单状态信息
      */
      */
+    /**
+     * wechatPayPartner 查单需传 storeId,与预下单一致,用于解析子商户号。
+     */
     @RequestMapping("/searchOrderByOutTradeNoPath")
     @RequestMapping("/searchOrderByOutTradeNoPath")
-    public R searchOrderByOutTradeNoPath(String transactionId, String payType) {
+    public R searchOrderByOutTradeNoPath(
+            @RequestParam String transactionId,
+            @RequestParam String payType,
+            @RequestParam(required = false) Integer storeId) {
         try {
         try {
-            return paymentStrategyFactory.getStrategy(payType).searchOrderByOutTradeNoPath(transactionId);
+//            if ("wechatPayMininProgram".equals(payType) && storeId != null) {
+//                return diningServiceFeign.searchOrderByOutTradeNoPath(transactionId, payType, storeId);
+//            }
+            return paymentStrategyFactory.getStrategy(payType).searchOrderByOutTradeNoPath(transactionId, storeId);
         } catch (Exception e) {
         } catch (Exception e) {
             return R.fail(e.getMessage());
             return R.fail(e.getMessage());
         }
         }

+ 68 - 0
alien-store/src/main/java/shop/alien/store/controller/ShortLinkController.java

@@ -0,0 +1,68 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.dto.ShortLinkResolveResponse;
+import shop.alien.store.dto.ShortLinkShortenRequest;
+import shop.alien.store.dto.ShortLinkShortenResponse;
+import shop.alien.store.service.ShortLinkService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 短链接:前端生成短链、解析长链(JSON);可选 open 路径 302。
+ */
+@Api(tags = {"短链接"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/store/short-link")
+@RequiredArgsConstructor
+public class ShortLinkController {
+
+    private final ShortLinkService shortLinkService;
+
+    @ApiOperation(value = "生成短链", notes = "需在配置 short-link.public-base-url 后返回完整 https 短 URL。可选 short-link.allowed-hosts 限制长链域名。")
+    @PostMapping("/shorten")
+    public R<ShortLinkShortenResponse> shorten(@RequestBody ShortLinkShortenRequest request) {
+        if (request == null || !StringUtils.hasText(request.getLongUrl())) {
+            return R.fail("longUrl 不能为空");
+        }
+        try {
+            ShortLinkShortenResponse res = shortLinkService.shorten(request.getLongUrl());
+            return R.data(res);
+        } catch (IllegalArgumentException ex) {
+            return R.fail(ex.getMessage());
+        } catch (Exception ex) {
+            log.warn("生成短链失败: {}", ex.getMessage());
+            return R.fail("生成短链失败");
+        }
+    }
+
+    @ApiOperation(value = "解析短链", notes = "返回原始 longUrl,由前端自行跳转;无需登录")
+    @GetMapping("/s/{code}")
+    public R<ShortLinkResolveResponse> resolve(@PathVariable String code) {
+        String longUrl = shortLinkService.resolveLongUrl(code);
+        if (!StringUtils.hasText(longUrl)) {
+            return R.fail("链接不存在或已过期");
+        }
+        return R.data(new ShortLinkResolveResponse(longUrl));
+    }
+
+    @ApiOperation(value = "短链浏览器跳转", notes = "302 到原始长链接;无需登录(书签/H5 直开用,小程序一般用上方 JSON 接口)")
+    @GetMapping("/s/{code}/open")
+    public void openRedirect(@PathVariable String code, HttpServletResponse response) throws IOException {
+        String longUrl = shortLinkService.resolveLongUrl(code);
+        if (!StringUtils.hasText(longUrl)) {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND, "链接不存在或已过期");
+            return;
+        }
+        response.sendRedirect(longUrl);
+    }
+}

+ 3 - 3
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -1051,11 +1051,11 @@ public class StoreInfoController {
     @ApiOperation(value = "AI服务-店铺审核")
     @ApiOperation(value = "AI服务-店铺审核")
     @ApiOperationSupport(order = 15)
     @ApiOperationSupport(order = 15)
     @PostMapping("/aiApproveStoreInfo")
     @PostMapping("/aiApproveStoreInfo")
-    public R<Boolean> aiApproveStoreInfo(@RequestBody AiApproveStoreInfo aiApproveStoreInfo) {
+    public R<AiApproveStoreInfoResultVo> aiApproveStoreInfo(@RequestBody AiApproveStoreInfo aiApproveStoreInfo) {
         log.info("StoreInfoController.aiApproveStoreInfo");
         log.info("StoreInfoController.aiApproveStoreInfo");
         try {
         try {
-            storeInfoService.aiApproveStoreInfo(aiApproveStoreInfo);
-            return R.success("店铺审核完成");
+            AiApproveStoreInfoResultVo result = storeInfoService.aiApproveStoreInfo(aiApproveStoreInfo);
+            return R.data(result, "店铺审核完成");
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("AI服务-店铺审核异常", e);
             log.error("AI服务-店铺审核异常", e);
             return R.fail("店铺审核失败:" + e.getMessage());
             return R.fail("店铺审核失败:" + e.getMessage());

+ 117 - 0
alien-store/src/main/java/shop/alien/store/controller/WeChatPartnerApplymentController.java

@@ -0,0 +1,117 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.WechatPartnerApplyment;
+import shop.alien.store.service.WeChatPartnerApplymentService;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 微信支付服务商 — 特约商户进件
+ * <p>
+ * 对外路径与微信 API 路径段对齐:{@code .../v3/applyment4sub/applyment/},
+ * 实际转发至 {@code https://api.mch.weixin.qq.com/v3/applyment4sub/applyment/}。
+ * </p>
+ */
+@Slf4j
+@Api(tags = {"微信支付服务商-特约进件"})
+@CrossOrigin
+@RestController
+@RequestMapping("/payment/wechatPartner")
+@RequiredArgsConstructor
+public class WeChatPartnerApplymentController {
+
+    private final WeChatPartnerApplymentService weChatPartnerApplymentService;
+
+    /**
+     * 提交特约商户进件申请单(对应微信 {@code POST /v3/applyment4sub/applyment/})
+     * <p>
+     * 请求体 JSON 字段以微信支付官方文档为准(如 business_code、contact_info、subject_info、
+     * bank_account_info 等),本接口不做字段级校验,原样转发。
+     * </p>
+     *
+     * @param requestBody    与微信文档一致的 JSON 字符串
+     * @param idempotencyKey 可选,传入 {@code Idempotency-Key},同一业务单重试时请保持不变
+     * @param storeId        可选,本系统门店 {@code store_info.id};审核通过后将微信 {@code sub_mchid} 写入该门店
+     */
+    @ApiOperation("特约商户进件-提交申请单(转发微信 /v3/applyment4sub/applyment/)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "Idempotency-Key", value = "幂等键,可选;重试同一进件时请与首次一致", paramType = "header", dataType = "String"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID store_info.id,可选;用于审核通过后回写 wechat_sub_mchid", paramType = "query", dataType = "Integer")
+    })
+    @PostMapping(value = "/v3/applyment4sub/applyment", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public R<Map<String, Object>> submitApplyment(
+            @RequestBody String requestBody,
+            @RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
+            @RequestParam(value = "storeId", required = false) Integer storeId) {
+        log.info("WeChatPartnerApplymentController.submitApplyment bodyLen={}, idempotencyKeyPresent={}, storeId={}",
+                requestBody != null ? requestBody.length() : 0, idempotencyKey != null && !idempotencyKey.isEmpty(), storeId);
+        return weChatPartnerApplymentService.submitApplyment(requestBody, idempotencyKey, storeId);
+    }
+
+    /**
+     * 商户图片上传,对应微信 {@code POST /v3/merchant/media/upload},成功返回 {@code media_id}。
+     */
+    @ApiOperation("特约商户进件-图片上传(转发微信 /v3/merchant/media/upload,返回 media_id)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "file", value = "图片文件 JPG/PNG/BMP,≤5MB", required = true, dataType = "file", paramType = "form")
+    })
+    @PostMapping(value = "/v3/merchant/media/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<Map<String, Object>> uploadMerchantMedia(@RequestParam("file") MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return R.fail("请选择图片文件");
+        }
+        try {
+            return weChatPartnerApplymentService.uploadMerchantMedia(file.getBytes(), file.getOriginalFilename());
+        } catch (IOException e) {
+            return R.fail("读取上传文件失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 申请单号查询申请状态,对应微信
+     * {@code GET /v3/applyment4sub/applyment/applyment_id/{applyment_id}}
+     * (<a href="https://pay.weixin.qq.com/doc/v3/partner/4012697052">文档</a>)。
+     *
+     * @param applymentId 微信支付分配的申请单号
+     */
+    @ApiOperation("特约商户进件-申请单号查询申请状态(转发微信 GET .../applyment_id/{applyment_id})")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "applyment_id", value = "微信支付申请单号", required = true, paramType = "path", dataType = "Long")
+    })
+    @GetMapping("/v3/applyment4sub/applyment/applyment_id/{applyment_id}")
+    public R<Map<String, Object>> queryApplymentStateByApplymentId(
+            @PathVariable("applyment_id") Long applymentId) {
+        if (applymentId == null) {
+            return R.fail("applyment_id 不能为空");
+        }
+        log.info("WeChatPartnerApplymentController.queryApplymentStateByApplymentId applyment_id={}", applymentId);
+        return weChatPartnerApplymentService.queryApplymentStateByApplymentId(applymentId);
+    }
+
+    /**
+     * 按门店查询特约商户进件状态:读本地 {@code wechat_partner_applyment} 该店最新一条记录(不调微信)。
+     *
+     * @param storeId 门店主键 {@code store_info.id}(进件提交时需已传同一 storeId)
+     */
+    @ApiOperation("特约商户进件-按门店查询进件状态(读本地库)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID store_info.id", required = true, paramType = "path", dataType = "int")
+    })
+    @GetMapping("/applyment/store/{storeId}")
+    public R<List<WechatPartnerApplyment>> queryApplymentStateByStoreId(@PathVariable("storeId") Integer storeId) {
+        log.info("WeChatPartnerApplymentController.queryApplymentStateByStoreId storeId={}", storeId);
+        return weChatPartnerApplymentService.queryApplymentStateByStoreId(storeId);
+    }
+}

+ 26 - 0
alien-store/src/main/java/shop/alien/store/dto/LifeUserPushBindDto.java

@@ -0,0 +1,26 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * App 上报 uni-push cid 与当前登录用户绑定
+ */
+@Data
+@ApiModel(description = "绑定推送 cid 请求体")
+public class LifeUserPushBindDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "uni.getPushClientId 返回的 cid", required = true)
+    private String pushClientId;
+
+    @ApiModelProperty(value = "平台:ios、android、harmony 等")
+    private String platform;
+
+    @ApiModelProperty(value = "DCloud 应用 appid,如 __UNI__xxxx")
+    private String dcloudAppId;
+}

+ 20 - 0
alien-store/src/main/java/shop/alien/store/dto/ShortLinkResolveResponse.java

@@ -0,0 +1,20 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 解析短链结果(前端自行跳转,如 webview / uni)
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel(value = "短链解析结果")
+public class ShortLinkResolveResponse {
+
+    @ApiModelProperty(value = "原始长链接")
+    private String longUrl;
+}

+ 16 - 0
alien-store/src/main/java/shop/alien/store/dto/ShortLinkShortenRequest.java

@@ -0,0 +1,16 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 生成短链请求
+ */
+@Data
+@ApiModel(value = "短链生成请求")
+public class ShortLinkShortenRequest {
+
+    @ApiModelProperty(value = "需要缩短的完整长链接(建议 https)", required = true)
+    private String longUrl;
+}

+ 23 - 0
alien-store/src/main/java/shop/alien/store/dto/ShortLinkShortenResponse.java

@@ -0,0 +1,23 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 生成短链响应
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel(value = "短链生成结果")
+public class ShortLinkShortenResponse {
+
+    @ApiModelProperty(value = "完整短链接,可直接用于微信分享")
+    private String shortUrl;
+
+    @ApiModelProperty(value = "短码(路径段)")
+    private String code;
+}

+ 28 - 0
alien-store/src/main/java/shop/alien/store/feign/DiningServiceFeign.java

@@ -270,4 +270,32 @@ public interface DiningServiceFeign {
      */
      */
     @PostMapping(value = "/dining/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     @PostMapping(value = "/dining/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     R<String> uploadFile(@RequestPart("file") MultipartFile file);
     R<String> uploadFile(@RequestPart("file") MultipartFile file);
+
+    @GetMapping("/payment/prePay")
+    R<Object> prePay(
+            @RequestHeader(value = "Authorization", required = false) String authorization,
+            @RequestParam("price") String price,
+            @RequestParam("subject") String subject,
+            @RequestParam("payType") String payType,
+            @RequestParam("payer") String payer,
+            @RequestParam("orderNo") String orderNo,
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam(value = "couponId", required = false) Integer couponId,
+            @RequestParam(value = "payerId", required = false) Integer payerId,
+            @RequestParam(value = "tablewareFee", required = false) String tablewareFee,
+            @RequestParam(value = "discountAmount", required = false) String discountAmount,
+            @RequestParam(value = "payAmount", required = false) String payAmount);
+
+    @GetMapping("/payment/searchOrderByOutTradeNoPath")
+    R<Object> searchOrderByOutTradeNoPath(
+            @RequestParam("transactionId") String transactionId,
+            @RequestParam("payType") String payType,
+            @RequestParam("storeId") Integer storeId);
+
+    /**
+     * 支付成功后重置餐桌与购物车(与 alien-dining {@code StoreOrderService.resetTableAfterPayment} 一致),供微信回调等业务使用。
+     */
+    @PostMapping("/payment/internal/resetTableAfterPayment")
+    R<Void> resetTableAfterPaymentInternal(
+            @RequestParam("tableId") Integer tableId);
 }
 }

+ 53 - 0
alien-store/src/main/java/shop/alien/store/service/AlipayZftOnboardingService.java

@@ -0,0 +1,53 @@
+package shop.alien.store.service;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AntMerchantExpandIndirectZftCreateModel;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.AlipayZftBizRequestDto;
+import shop.alien.entity.store.dto.AlipayZftMerchantCreateDto;
+import shop.alien.entity.store.dto.AlipayZftMerchantSimplecreateDto;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 支付宝直付通二级商户入驻相关接口封装(调用支付宝开放平台 API)。
+ */
+public interface AlipayZftOnboardingService {
+
+    /**
+     * ant.merchant.expand.indirect.zft.consult — 进件前咨询
+     */
+    R<String> zftConsult(AlipayZftBizRequestDto request);
+
+    /**
+     * ant.merchant.expand.indirect.zft.create — 二级商户标准进件(结构化必填:external_id、merchant_type、name、alias_name、mcc、service)
+     */
+    R<String> zftCreate(AntMerchantExpandIndirectZftCreateModel request, Integer storeId) throws AlipayApiException;
+
+    /**
+     * ant.merchant.expand.indirect.zft.simplecreate — 二级商户免证照进件(SDK setBizModel)
+     */
+    R<String> zftSimplecreate(AlipayZftMerchantSimplecreateDto request);
+
+    /**
+     * ant.merchant.expand.order.query — 商户申请单查询
+     *
+     * @param orderId 申请单 id(支付宝 order_id)
+     */
+    R<String> orderQuery(String orderId);
+
+    /**
+     * ant.merchant.expand.indirect.zft.modify — 二级商户信息修改
+     */
+    R<String> zftModify(AlipayZftBizRequestDto request) throws AlipayApiException;
+
+    /**
+     * ant.merchant.expand.indirect.supplement.create — 申请单补件
+     */
+    R<String> supplementCreate(AlipayZftBizRequestDto request);
+
+    /**
+     * ant.merchant.expand.indirect.image.upload — 图片上传
+     */
+    R<String> imageUpload(Integer storeId, String imageType, MultipartFile imageContent,
+                          String appId, String appAuthToken, String apiVersion) throws AlipayApiException;
+}

+ 8 - 0
alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonRating;
 import shop.alien.entity.store.CommonRating;
 import shop.alien.entity.store.vo.CommonRatingVo;
 import shop.alien.entity.store.vo.CommonRatingVo;
+import shop.alien.entity.store.vo.MyMergedReviewItemVo;
 
 
 /**
 /**
  * 评价表 服务类
  * 评价表 服务类
@@ -74,6 +75,13 @@ public interface CommonRatingService extends IService<CommonRating> {
      */
      */
     R getMyRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long userId, Integer auditStatus);
     R getMyRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long userId, Integer auditStatus);
 
 
+    /**
+     * 融合「门店我的评价」与「律师订单我的评价」,按发布时间倒序分页。
+     * 分页说明:从两侧各取至多 pageNum*pageSize 条(上限 1000)合并后再截取当前页。
+     */
+    R<IPage<MyMergedReviewItemVo>> getMyMergedRatingList(Integer pageNum, Integer pageSize, Integer businessType,
+                                                         Long userId, Integer auditStatus, Integer currentUserId);
+
     R<IPage<CommonRatingVo>> doListBusinessWithType(IPage<CommonRating> page2, Integer i, Long userId, Integer replyStatus);
     R<IPage<CommonRatingVo>> doListBusinessWithType(IPage<CommonRating> page2, Integer i, Long userId, Integer replyStatus);
 
 
   /*  /**
   /*  /**

+ 129 - 2
alien-store/src/main/java/shop/alien/store/service/LifeCommentService.java

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.ObjectUtils;
@@ -36,6 +37,10 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 public class LifeCommentService {
 public class LifeCommentService {
 
 
+    /** App 点击推送后跳转页面(uni-app 页面路径) */
+    private static final String DYNAMICS_LIKE_PUSH_OPEN_PATH =
+            "pages/secondHandTransactions/pages/message/noticesAndMessage";
+
     private final LifeCommentMapper lifeCommentMapper;
     private final LifeCommentMapper lifeCommentMapper;
 
 
     private final StoreCommentMapper storeCommentMapper;
     private final StoreCommentMapper storeCommentMapper;
@@ -62,6 +67,20 @@ public class LifeCommentService {
 
 
     private final CommonCommentMapper commonCommentMapper;
     private final CommonCommentMapper commonCommentMapper;
 
 
+    private final LifeUserPersonalizationSettingService lifeUserPersonalizationSettingService;
+
+    private final LifeUserPushDeviceService lifeUserPushDeviceService;
+
+    private final UniPushCloudInvokeService uniPushCloudInvokeService;
+
+    private final StoreUserMapper storeUserMapper;
+
+    /**
+     * 系统app通知开关
+     */
+    @Value("${alien.unipush.enabled:false}")
+    private boolean uniPushOn;
+
     /**
     /**
      * 点赞操作
      * 点赞操作
      * <p>
      * <p>
@@ -121,9 +140,29 @@ public class LifeCommentService {
             // 根据类型更新对应表的点赞数
             // 根据类型更新对应表的点赞数
             int updateResult = updateLikeCountByType(huifuId, type);
             int updateResult = updateLikeCountByType(huifuId, type);
             
             
-            // 如果是动态类型,发送通知
+            // 动态点赞:按「被点赞动态的发布者」的个性化设置决定是否通知(接收方是否愿意收点赞类通知)
             if (updateResult > 0 && CommonConstant.LIKE_TYPE_DYNAMICS.equals(type)) {
             if (updateResult > 0 && CommonConstant.LIKE_TYPE_DYNAMICS.equals(type)) {
-                insertNotice(userId, huifuId, type);
+                Integer receiverLifeUserId = resolveLifeUserIdFromDynamicsAuthorPhoneId(huifuId);
+                boolean suppressNotice = receiverLifeUserId != null
+                        && lifeUserPersonalizationSettingService.shouldSuppressLikeRelatedNotice(receiverLifeUserId);
+                if (!suppressNotice) {
+                    try {
+                        insertNotice(userId, huifuId, type);
+                    } catch (Exception e) {
+                        log.error("动态点赞站内信保存失败, huifuId={}", huifuId, e);
+                    }
+
+                    // 发送系统通知开关
+                    if(uniPushOn){
+                        try {
+                            sendDynamicsLikeAppPushByDynamicsId(Integer.parseInt(huifuId.trim()));
+                        } catch (NumberFormatException e) {
+                            log.warn("动态点赞 App 推送跳过:huifuId 非合法动态 id, huifuId={}", huifuId);
+                        } catch (Exception e) {
+                            log.warn("动态点赞 App 推送失败, huifuId={}, err={}", huifuId, e.getMessage());
+                        }
+                    }
+                }
             }
             }
             
             
             log.info("点赞操作完成,userId={},huifuId={},type={},更新结果={}", userId, huifuId, type, updateResult);
             log.info("点赞操作完成,userId={},huifuId={},type={},更新结果={}", userId, huifuId, type, updateResult);
@@ -212,6 +251,37 @@ public class LifeCommentService {
     }
     }
 
 
     /**
     /**
+     * 根据动态主键查发布者 phoneId,仅当为 C 端 user_ 前缀时解析为 life_user.id;门店动态 store_ 无 C 端个性化表则返回 null(不拦截通知)。
+     */
+    private Integer resolveLifeUserIdFromDynamicsAuthorPhoneId(String dynamicsIdStr) {
+        if (!StringUtils.hasText(dynamicsIdStr)) {
+            return null;
+        }
+        try {
+            int dynamicsId = Integer.parseInt(dynamicsIdStr.trim());
+            LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(dynamicsId);
+            if (dynamics == null || !StringUtils.hasText(dynamics.getPhoneId())) {
+                return null;
+            }
+            String phoneId = dynamics.getPhoneId().trim();
+            if (!phoneId.startsWith("user_")) {
+                return null;
+            }
+            String phone = phoneId.substring("user_".length());
+            if (!StringUtils.hasText(phone)) {
+                return null;
+            }
+            LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, phone)
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            return u != null ? u.getId() : null;
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
      * 发送通知
      * 发送通知
      */
      */
     private void insertNotice(String userId, String huifuId, String type) {
     private void insertNotice(String userId, String huifuId, String type) {
@@ -229,6 +299,63 @@ public class LifeCommentService {
     }
     }
 
 
     /**
     /**
+     * 仅发起 App 推送(uniCloud),与 {@link #insertNotice} 独立;未配置、无 cid 时静默返回。
+     */
+    private void sendDynamicsLikeAppPushByDynamicsId(int dynamicsId) {
+        LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(dynamicsId);
+        sendDynamicsLikeAppPushOnly(dynamics, "动态通知", "点赞了你的动态", dynamicsId);
+    }
+
+    private void sendDynamicsLikeAppPushOnly(LifeUserDynamics dynamics, String title, String content, int dynamicsId) {
+        if (dynamics == null || !StringUtils.hasText(dynamics.getPhoneId())) {
+            return;
+        }
+        String phoneId = dynamics.getPhoneId().trim();
+        List<String> cids = resolvePushClientIdsForDynamicsAuthor(phoneId);
+        if (CollectionUtils.isEmpty(cids)) {
+            return;
+        }
+        Map<String, Object> payload = new HashMap<>(12);
+        payload.put("scene", "dynamics_like");
+        payload.put("dynamicsId", dynamicsId);
+        payload.put("noticeType", 0);
+        payload.put("path", DYNAMICS_LIKE_PUSH_OPEN_PATH);
+        uniPushCloudInvokeService.sendToClientIds(cids, title, content, payload);
+    }
+
+    private List<String> resolvePushClientIdsForDynamicsAuthor(String phoneId) {
+        if (phoneId.startsWith("user_")) {
+            String phone = phoneId.substring("user_".length());
+            if (!StringUtils.hasText(phone)) {
+                return Collections.emptyList();
+            }
+            LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, phone)
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (u == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(u.getId(), PushDeviceOwnerType.USER.getCode());
+        }
+        if (phoneId.startsWith("store_")) {
+            String phone = phoneId.substring("store_".length());
+            if (!StringUtils.hasText(phone)) {
+                return Collections.emptyList();
+            }
+            StoreUser su = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getPhone, phone)
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (su == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(su.getId(), PushDeviceOwnerType.STORE.getCode());
+        }
+        return Collections.emptyList();
+    }
+
+    /**
      * 取消点赞操作
      * 取消点赞操作
      * <p>
      * <p>
      * 检查是否已点赞,如果已点赞则删除点赞记录并更新对应表的点赞数
      * 检查是否已点赞,如果已点赞则删除点赞记录并更新对应表的点赞数

+ 27 - 12
alien-store/src/main/java/shop/alien/store/service/LifeStoreService.java

@@ -70,26 +70,34 @@ public class LifeStoreService {
 
 
     /**
     /**
      * 我的关注
      * 我的关注
+     *
+     * @param type 筛选类型:不传或 0 表示店铺+用户关注;1 且 fansId 为店铺时仅返回店铺类型关注
+     * @param myFansId 可选;有值时 isFollowMe、isFollowThis、isBlocked、blackListid 相对该主体计算,列表仍按 fansId
      */
      */
-    public IPage<LifeFansVo> getMyFollowed(String fansId, String name, int page, int size) {QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
+    public IPage<LifeFansVo> getMyFollowed(String fansId, String name, int page, int size, Integer type, String myFansId) {
+        QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
         wrapper.like(StringUtils.isNotEmpty(name), "foll.name", name)
         wrapper.like(StringUtils.isNotEmpty(name), "foll.name", name)
                 .or(StringUtils.isNotEmpty(name))
                 .or(StringUtils.isNotEmpty(name))
                 .like(StringUtils.isNotEmpty(name), "foll.phoneId", name);
                 .like(StringUtils.isNotEmpty(name), "foll.phoneId", name);
         wrapper.groupBy("foll.phoneId");
         wrapper.groupBy("foll.phoneId");
+        String trimmedMyFansId = StringUtils.isNotEmpty(myFansId) ? myFansId.trim() : null;
+        String relationFansId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
+        String blockerSourceId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
         String blockerType = "";
         String blockerType = "";
         String blockerId = "";
         String blockerId = "";
-        if ("user".equals(fansId.split("_")[0])) {
-            String myselfUserPhone = fansId.split("_")[1];
+        if ("user".equals(blockerSourceId.split("_")[0])) {
+            String myselfUserPhone = blockerSourceId.split("_")[1];
             blockerType = "2";
             blockerType = "2";
             LifeUser myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
             LifeUser myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
             blockerId = String.valueOf(myLifeUser.getId());
             blockerId = String.valueOf(myLifeUser.getId());
         } else {
         } else {
-            String myselfStorePhone = fansId.split("_")[1];
+            String myselfStorePhone = blockerSourceId.split("_")[1];
             blockerType = "1";
             blockerType = "1";
             StoreUser myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
             StoreUser myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
             blockerId = String.valueOf(myStoreUser.getId());
             blockerId = String.valueOf(myStoreUser.getId());
         }
         }
-        IPage<LifeFansVo> myFollowed = lifeFansMapper.getMyFollowed(new Page<>(page, size), fansId, blockerType, blockerId, wrapper);
+        boolean onlyStoreFollowed = (type != null && type == 1) && fansId != null && fansId.startsWith("store_");
+        IPage<LifeFansVo> myFollowed = lifeFansMapper.getMyFollowed(new Page<>(page, size), fansId, relationFansId, blockerType, blockerId, onlyStoreFollowed, wrapper);
         // 过滤掉我拉黑的
         // 过滤掉我拉黑的
         List<LifeFansVo> collect = myFollowed.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         List<LifeFansVo> collect = myFollowed.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         myFollowed.setRecords(collect);
         myFollowed.setRecords(collect);
@@ -106,25 +114,32 @@ public class LifeStoreService {
 
 
     /**
     /**
      * 我的粉丝
      * 我的粉丝
+     *
+     * @param type 筛选类型:不传或 0 表示店铺+用户粉丝;1 且 fansId 为店铺时仅返回店铺粉丝
+     * @param myFansId 可选;有值时 isFollowMe、isFollowThis、isBlocked、blackListid 相对该主体计算,列表仍按 fansId
      */
      */
-    public IPage<LifeFansVo> getMyFans(String fansId, String name, int page, int size) {
+    public IPage<LifeFansVo> getMyFans(String fansId, String name, int page, int size, Integer type, String myFansId) {
         QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
         QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
         wrapper.like(StringUtils.isNotEmpty(name), "foll.name", name);
         wrapper.like(StringUtils.isNotEmpty(name), "foll.name", name);
         wrapper.groupBy("foll.phoneId");
         wrapper.groupBy("foll.phoneId");
+        String trimmedMyFansId = StringUtils.isNotEmpty(myFansId) ? myFansId.trim() : null;
+        String relationFansId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
+        String blockerSourceId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
         String blockerType = "";
         String blockerType = "";
         String blockerId = "";
         String blockerId = "";
-        if ("user".equals(fansId.split("_")[0])) {
-            String myselfUserPhone = fansId.split("_")[1];
+        if ("user".equals(blockerSourceId.split("_")[0])) {
+            String myselfUserPhone = blockerSourceId.split("_")[1];
             blockerType = "2";
             blockerType = "2";
             LifeUser myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
             LifeUser myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
             blockerId = String.valueOf(myLifeUser.getId());
             blockerId = String.valueOf(myLifeUser.getId());
         } else {
         } else {
-            String myselfStorePhone = fansId.split("_")[1];
+            String myselfStorePhone = blockerSourceId.split("_")[1];
             blockerType = "1";
             blockerType = "1";
             StoreUser myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
             StoreUser myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
             blockerId = String.valueOf(myStoreUser.getId());
             blockerId = String.valueOf(myStoreUser.getId());
         }
         }
-        IPage<LifeFansVo> myFans = lifeFansMapper.getMyFans(new Page<>(page, size), fansId, blockerType, blockerId, wrapper);
+        boolean onlyStoreFans = (type != null && type == 1) && fansId != null && fansId.startsWith("store_");
+        IPage<LifeFansVo> myFans = lifeFansMapper.getMyFans(new Page<>(page, size), fansId, relationFansId, blockerType, blockerId, onlyStoreFans, wrapper);
         List<LifeFansVo> collect = myFans.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         List<LifeFansVo> collect = myFans.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         myFans.setRecords(collect);
         myFans.setRecords(collect);
         myFans.setTotal(collect.size());
         myFans.setTotal(collect.size());
@@ -260,11 +275,11 @@ public class LifeStoreService {
         
         
         // 统计关注数量(过滤被拉黑的)
         // 统计关注数量(过滤被拉黑的)
         int followedNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize, 
         int followedNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize, 
-                (page, w) -> lifeFansMapper.getMyFollowed(page, phoneId, blockerType, blockerId, w));
+                (page, w) -> lifeFansMapper.getMyFollowed(page, phoneId, phoneId, blockerType, blockerId, false, w));
 
 
         // 统计粉丝数量(过滤被拉黑的)
         // 统计粉丝数量(过滤被拉黑的)
         int fansNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize,
         int fansNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize,
-                (page, w) -> lifeFansMapper.getMyFans(page, phoneId, blockerType, blockerId, w));
+                (page, w) -> lifeFansMapper.getMyFans(page, phoneId, phoneId, blockerType, blockerId, false, w));
 
 
         // 统计好友数量(过滤被拉黑的)
         // 统计好友数量(过滤被拉黑的)
         int friendNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize,
         int friendNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize,

+ 46 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserPersonalizationSettingService.java

@@ -0,0 +1,46 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+
+public interface LifeUserPersonalizationSettingService extends IService<LifeUserPersonalizationSetting> {
+
+    R<String> add(LifeUserPersonalizationSetting setting);
+
+    R<String> deleteById(Integer id);
+
+    R<String> update(LifeUserPersonalizationSetting setting);
+
+    R<LifeUserPersonalizationSetting> getInfoById(Integer id);
+
+    R<LifeUserPersonalizationSetting> getByUserId(Integer userId);
+
+    /**
+     * 查询用户个性化设置:优先 Redis;未命中则与 {@link #getByUserId} 一致查库(含无记录时初始化)并写入 Redis。
+     *
+     * @param userId life_user.id
+     * @return 实体;userId 为空或查库/初始化失败时返回 null
+     */
+    LifeUserPersonalizationSetting getByUserIdCacheAside(Integer userId);
+
+    /**
+     * 是否不应向该用户发送与「收到点赞」相关的通知:notifyReceiveMessage=0,或 notifyReceiveMessage=1 且 notifyLike=0。
+     * 调用方应传入接收通知的一方对应的 life_user.id(例如动态点赞场景为动态发布者,而非点赞操作人)。
+     *
+     * @param userId life_user.id(接收方)
+     * @return userId 为空时 false(不拦截);有设置且满足上述条件时 true
+     */
+    boolean shouldSuppressLikeRelatedNotice(Integer userId);
+
+    /**
+     * 是否不应发送与关注相关的通知:notifyReceiveMessage=0,或 notifyReceiveMessage=1 且 notifyFollow=0。
+     *
+     * @param userId life_user.id(被关注方)
+     * @return userId 为空时 false(不拦截);有设置且满足上述条件时 true
+     */
+    boolean shouldSuppressFollowRelatedNotice(Integer userId);
+
+    R<IPage<LifeUserPersonalizationSetting>> list(Integer pageNum, Integer pageSize, Integer userId);
+}

+ 34 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserPushDeviceService.java

@@ -0,0 +1,34 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPushDevice;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.store.dto.LifeUserPushBindDto;
+
+import java.util.List;
+
+public interface LifeUserPushDeviceService {
+
+    /**
+     * 将 cid 绑定到当前登录用户;同一 cid 全局唯一,重复上报会更新为当前用户并刷新平台等信息。
+     */
+    R<String> bind(UserLoginInfo login, LifeUserPushBindDto dto);
+
+    /**
+     * 当前用户解绑指定 cid(仅可删除本人绑定记录)
+     */
+    R<String> unbindByCid(UserLoginInfo login, String pushClientId);
+
+    /**
+     * 查询当前用户已绑定的设备列表
+     */
+    R<List<LifeUserPushDevice>> listMine(UserLoginInfo login);
+
+    /**
+     * 供业务侧按归属类型 + 业务用户主键查询 cid 列表(去重),用于组装云函数推送目标。
+     *
+     * @param userId    ownerType=user 时为 life_user.id;store 为 store_user.id;lawyer 为 lawyer_user.id
+     * @param ownerType {@link shop.alien.entity.store.PushDeviceOwnerType#getCode()}:user / store / lawyer
+     */
+    List<String> listPushClientIdsByUserId(int userId, String ownerType);
+}

+ 106 - 1
alien-store/src/main/java/shop/alien/store/service/LifeUserService.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Triple;
 import org.apache.commons.lang3.tuple.Triple;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +25,8 @@ import shop.alien.entity.second.SecondRiskControlRecord;
 import shop.alien.entity.store.LifeFans;
 import shop.alien.entity.store.LifeFans;
 import shop.alien.entity.store.LifeNotice;
 import shop.alien.entity.store.LifeNotice;
 import shop.alien.entity.store.LifeUser;
 import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.PushDeviceOwnerType;
+import shop.alien.entity.store.StoreUser;
 import shop.alien.entity.store.vo.LifeMessageVo;
 import shop.alien.entity.store.vo.LifeMessageVo;
 import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.entity.store.vo.WebSocketVo;
@@ -31,6 +34,7 @@ import shop.alien.mapper.LifeFansMapper;
 import shop.alien.mapper.LifeMessageMapper;
 import shop.alien.mapper.LifeMessageMapper;
 import shop.alien.mapper.LifeNoticeMapper;
 import shop.alien.mapper.LifeNoticeMapper;
 import shop.alien.mapper.LifeUserMapper;
 import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.StoreUserMapper;
 import shop.alien.mapper.second.LifeUserLogMapper;
 import shop.alien.mapper.second.LifeUserLogMapper;
 import shop.alien.mapper.second.SecondRiskControlRecordMapper;
 import shop.alien.mapper.second.SecondRiskControlRecordMapper;
 import shop.alien.mapper.second.SecondUserCreditMapper;
 import shop.alien.mapper.second.SecondUserCreditMapper;
@@ -48,10 +52,15 @@ import java.util.stream.Collectors;
 /**
 /**
  * 用户
  * 用户
  */
  */
+@Slf4j
 @Service
 @Service
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
 public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
 
 
+    /** App 点击推送后跳转页面(与动态点赞通知一致) */
+    private static final String FOLLOW_APP_PUSH_OPEN_PATH =
+            "pages/secondHandTransactions/pages/message/noticesAndMessage";
+
     private final LifeUserMapper lifeUserMapper;
     private final LifeUserMapper lifeUserMapper;
 
 
     private final LifeFansMapper lifeFansMapper;
     private final LifeFansMapper lifeFansMapper;
@@ -74,12 +83,26 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
 
 
     private final SecondRiskControlRecordMapper secondRiskControlRecordMapper;
     private final SecondRiskControlRecordMapper secondRiskControlRecordMapper;
 
 
+    private final LifeUserPersonalizationSettingService lifeUserPersonalizationSettingService;
+
+    private final LifeUserPushDeviceService lifeUserPushDeviceService;
+
+    private final UniPushCloudInvokeService uniPushCloudInvokeService;
+
+    private final StoreUserMapper storeUserMapper;
+
     @Autowired
     @Autowired
     private RiskControlProperties riskControlProperties;
     private RiskControlProperties riskControlProperties;
 
 
     @Value("${jwt.expiration-time}")
     @Value("${jwt.expiration-time}")
     private String effectiveTime;
     private String effectiveTime;
 
 
+    /**
+     * 系统app通知开关
+     */
+    @Value("${alien.unipush.enabled:false}")
+    private boolean uniPushOn;
+
     public IPage<LifeUser> getStoresPage(int page, int size, String realName, String userPhone) {
     public IPage<LifeUser> getStoresPage(int page, int size, String realName, String userPhone) {
         IPage<LifeUser> storePage = new Page<>(page, size);
         IPage<LifeUser> storePage = new Page<>(page, size);
         QueryWrapper<LifeUser> queryWrapper = new QueryWrapper<>();
         QueryWrapper<LifeUser> queryWrapper = new QueryWrapper<>();
@@ -126,12 +149,94 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
             if (!CollectionUtils.isEmpty(userList)) {
             if (!CollectionUtils.isEmpty(userList)) {
                 notice.setBusinessId(userList.get(0).getId());
                 notice.setBusinessId(userList.get(0).getId());
             }
             }
-            lifeNoticeMapper.insert(notice);
+            Integer followedLifeUserId = resolveLifeUserIdFromFollowedId(fans.getFollowedId());
+            boolean suppressNotice = followedLifeUserId != null
+                    && lifeUserPersonalizationSettingService.shouldSuppressFollowRelatedNotice(followedLifeUserId);
+            if (!suppressNotice) {
+                lifeNoticeMapper.insert(notice);
+
+                // 发送系统通知开关
+                if(uniPushOn) {
+                    try {
+                        sendFollowRelationAppPush(fans.getFollowedId());
+                    } catch (Exception e) {
+                        log.warn("关注 App 推送失败,followedId={},err={}", fans.getFollowedId(), e.getMessage());
+                    }
+                }
+            }
         }
         }
 
 
         return num;
         return num;
     }
     }
 
 
+    /**
+     * 关注站内通知写入成功后,向被关注方已绑定 cid 的设备发 App 系统消息;与站内信条件一致,失败不影响关注与通知入库。
+     */
+    private void sendFollowRelationAppPush(String followedId) {
+        if (StringUtils.isBlank(followedId)) {
+            return;
+        }
+        List<String> cids = resolvePushClientIdsForFollowReceiver(followedId.trim());
+        if (CollectionUtils.isEmpty(cids)) {
+            return;
+        }
+        Map<String, Object> payload = new HashMap<>(8);
+        payload.put("scene", "follow");
+        payload.put("noticeType", 0);
+        payload.put("path", FOLLOW_APP_PUSH_OPEN_PATH);
+        uniPushCloudInvokeService.sendToClientIds(cids, "关注通知", "关注了你", payload);
+    }
+
+    private List<String> resolvePushClientIdsForFollowReceiver(String phoneId) {
+        if (phoneId.startsWith("user_")) {
+            String phone = phoneId.substring("user_".length());
+            if (StringUtils.isBlank(phone)) {
+                return Collections.emptyList();
+            }
+            LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, phone)
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (u == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(u.getId(), PushDeviceOwnerType.USER.getCode());
+        }
+        if (phoneId.startsWith("store_")) {
+            String phone = phoneId.substring("store_".length());
+            if (StringUtils.isBlank(phone)) {
+                return Collections.emptyList();
+            }
+            StoreUser su = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getPhone, phone)
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (su == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(su.getId(), PushDeviceOwnerType.STORE.getCode());
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * followedId 为 user_手机号 时解析为 life_user.id;店铺等非用户被关注方返回 null(不读个性化表,照常发通知)
+     */
+    private Integer resolveLifeUserIdFromFollowedId(String followedId) {
+        if (StringUtils.isBlank(followedId) || !followedId.startsWith("user_")) {
+            return null;
+        }
+        String phone = followedId.substring("user_".length());
+        if (StringUtils.isBlank(phone)) {
+            return null;
+        }
+        LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                .eq(LifeUser::getUserPhone, phone)
+                .eq(LifeUser::getDeleteFlag, 0)
+                .last("LIMIT 1"));
+        return u != null ? u.getId() : null;
+    }
+
     public int cancelFans(LifeFans fans) {
     public int cancelFans(LifeFans fans) {
         LambdaUpdateWrapper<LifeFans> wrapper = new LambdaUpdateWrapper<>();
         LambdaUpdateWrapper<LifeFans> wrapper = new LambdaUpdateWrapper<>();
         wrapper.eq(LifeFans::getFansId, fans.getFansId());
         wrapper.eq(LifeFans::getFansId, fans.getFansId());

+ 140 - 39
alien-store/src/main/java/shop/alien/store/service/MerchantPaymentSyncScheduler.java

@@ -1,63 +1,164 @@
 package shop.alien.store.service;
 package shop.alien.store.service;
 
 
+import com.alibaba.fastjson.JSONObject;
+import com.alipay.api.internal.util.AlipaySignature;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import shop.alien.entity.result.R;
-import shop.alien.entity.store.MerchantPaymentOrder;
-import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
-import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.store.StorePaymentConfig;
 
 
+import javax.servlet.http.HttpServletRequest;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Date;
 import java.util.Date;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 
 
 /**
 /**
- * 商户支付状态定时同步(无异步回调时的兜底)
- * 定时扫描近期待支付单,主动向支付宝/微信查单并更新订单状态。
+ * 1)商户支付状态定时同步(无异步回调时的兜底),由 {@code payment.sync.enabled} 控制是否执行;<br/>
+ * 2)支付宝开放平台<strong>消息通知</strong>:二级商户进件审核通过 {@code ant.merchant.expand.indirect.zft.passed},
+ * POST 至应用网关(请在开放平台配置网关 URL,例如 .../store/alipay/message/zft-passed),
+ * 验签通过后更新门店支付配置中的二级商户标识等。
  *
  *
  * @author system
  * @author system
  */
  */
 @Slf4j
 @Slf4j
-@Component
+@RestController
+@RequestMapping("/store/alipay/message")
 @RequiredArgsConstructor
 @RequiredArgsConstructor
-@ConditionalOnProperty(name = "payment.sync.enabled", havingValue = "true", matchIfMissing = false)
 public class MerchantPaymentSyncScheduler {
 public class MerchantPaymentSyncScheduler {
 
 
-    /** 扫描最近多少分钟内创建的待支付单 */
-    private static final int WITHIN_MINUTES = 30;
+    private static final String MSG_ZFT_PASSED = "ant.merchant.expand.indirect.zft.passed";
 
 
-    private final MerchantPaymentOrderService merchantPaymentOrderService;
-    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+    /** 为 false 时不跑同步任务;消息通知接口始终可用 */
+    @Value("${payment.sync.enabled:false}")
+    private boolean paymentSyncEnabled;
+
+    /** 生产环境应为 true;本地联调可临时关闭验签 */
+    @Value("${payment.alipay.zft-passed.verify-signature:true}")
+    private boolean verifyZftPassedSignature;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+
+    /**
+     * 支付宝异步消息:直付通二级商户进件审核通过。
+     * <p>
+     * Content-Type: application/x-www-form-urlencoded,公共参数含 notify_id、utc_timestamp、msg_method、
+     * app_id、version、biz_content、sign、sign_type、charset 等;biz_content 内含 order_id、smid、
+     * external_id、card_alias_no、memo。
+     * </p>
+     * 处理成功请返回纯文本 {@code success},失败返回 {@code fail}(支付宝将按策略重试)。
+     */
+    @PostMapping(value = "/zft-passed", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = "text/plain;charset=UTF-8")
+    public String onZftPassedMessage(HttpServletRequest request) {
+        try {
+            Map<String, String> params = toSingleValueMap(request);
+            String msgMethod = params.get("msg_method");
+            if (!MSG_ZFT_PASSED.equals(msgMethod)) {
+                log.debug("忽略非 zft.passed 消息 msg_method={}", msgMethod);
+                return "success";
+            }
+            String appId = params.get("app_id");
+            StorePaymentConfig config = storePaymentConfigService.getByAppId(appId);
+            if (config == null) {
+                log.warn("zft.passed 未找到 app_id 对应支付配置 appId={}", appId);
+                return "fail";
+            }
+            if (verifyZftPassedSignature && !verifyAlipaySignature(params, config)) {
+                log.warn("zft.passed 验签失败 appId={}", appId);
+                return "fail";
+            }
+            String bizRaw = params.get("biz_content");
+            if (StringUtils.isBlank(bizRaw)) {
+                return "fail";
+            }
+            JSONObject biz = JSONObject.parseObject(bizRaw);
+            applyZftPassed(config, biz);
+            return "success";
+        } catch (Exception e) {
+            log.error("zft.passed 消息处理异常", e);
+            return "fail";
+        }
+    }
+
+    private void applyZftPassed(StorePaymentConfig config, JSONObject biz) {
+        String smid = biz.getString("smid");
+        if (StringUtils.isNotBlank(smid)) {
+            config.setStoreAliId(smid);
+        }
+        log.info("直付通二级商户进件审核通过 storeId={} orderId={} smid={} externalId={} cardAliasNo={} memo={}",
+                config.getStoreId(),
+                biz.getString("order_id"),
+                smid,
+                biz.getString("external_id"),
+                biz.getString("card_alias_no"),
+                biz.getString("memo"));
+        storePaymentConfigService.updateById(config);
+    }
+
+    private boolean verifyAlipaySignature(Map<String, String> params, StorePaymentConfig config) {
+        if (config.getAlipayPublicCert() == null || config.getAlipayPublicCert().length == 0) {
+            log.warn("验签失败:未配置支付宝公钥证书");
+            return false;
+        }
+        Path temp = null;
+        try {
+            temp = Files.createTempFile("alipay_pub_", ".crt");
+            Files.write(temp, config.getAlipayPublicCert());
+            String signType = StringUtils.defaultIfBlank(params.get("sign_type"), "RSA2");
+            return AlipaySignature.rsaCertCheckV1(params, temp.toAbsolutePath().toString(), "UTF-8", signType);
+        } catch (Exception e) {
+            log.error("支付宝消息验签异常", e);
+            return false;
+        } finally {
+            if (temp != null) {
+                try {
+                    Files.deleteIfExists(temp);
+                } catch (Exception ignored) {
+                    // ignore
+                }
+            }
+        }
+    }
+
+    private static Map<String, String> toSingleValueMap(HttpServletRequest request) {
+        Map<String, String> params = new HashMap<>();
+        Map<String, String[]> raw = request.getParameterMap();
+        for (Map.Entry<String, String[]> e : raw.entrySet()) {
+            String[] v = e.getValue();
+            if (v == null || v.length == 0) {
+                params.put(e.getKey(), "");
+            } else if (v.length == 1) {
+                params.put(e.getKey(), v[0]);
+            } else {
+                StringBuilder sb = new StringBuilder();
+                for (int i = 0; i < v.length; i++) {
+                    if (i > 0) {
+                        sb.append(',');
+                    }
+                    sb.append(v[i]);
+                }
+                params.put(e.getKey(), sb.toString());
+            }
+        }
+        return params;
+    }
 
 
     /**
     /**
-     * 每 2 分钟执行一次:拉取近期待支付单,逐笔查第三方并更新状态
+     * 每 2 分钟执行一次:拉取近期待支付单,逐笔查第三方并更新状态(需开启 payment.sync.enabled)
      */
      */
     @Scheduled(fixedDelayString = "${payment.sync.interval-ms:120000}")
     @Scheduled(fixedDelayString = "${payment.sync.interval-ms:120000}")
     public void syncPaymentStatus() {
     public void syncPaymentStatus() {
-        //暂时不使用定时
-         log.info("商户支付同步:本批时间={}", new Date().getTime());
-
-//        List<MerchantPaymentOrder> list = merchantPaymentOrderService.listUnpaidRecent(WITHIN_MINUTES);
-//        if (list == null || list.isEmpty()) {
-//            return;
-//        }
-//        log.debug("商户支付同步:本批待支付单数={}", list.size());
-//        for (MerchantPaymentOrder po : list) {
-//            try {
-//                if (po.getPayType() == null || !merchantPaymentStrategyFactory.supports(po.getPayType())) {
-//                    log.warn("商户支付同步:不支持的 payType={}, outTradeNo={}", po.getPayType(), po.getOutTradeNo());
-//                    continue;
-//                }
-//                MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(po.getPayType());
-//                R<Object> r = strategy.queryPayStatus(po.getStoreId(), po.getOutTradeNo());
-//                if (r != null && R.isSuccess(r)) {
-//                    log.info("商户支付同步:已更新为已支付,outTradeNo={}", po.getOutTradeNo());
-//                }
-//            } catch (Exception e) {
-//                log.warn("商户支付同步:查单异常 outTradeNo={}", po.getOutTradeNo(), e);
-//            }
-//        }
+        if (!paymentSyncEnabled) {
+            return;
+        }
+        log.info("商户支付同步:本批时间={}", new Date().getTime());
+        // 待启用:按 MerchantPaymentOrder 查单并回写状态,可注入 MerchantPaymentOrderService、MerchantPaymentStrategyFactory
     }
     }
 }
 }

+ 19 - 0
alien-store/src/main/java/shop/alien/store/service/ShortLinkService.java

@@ -0,0 +1,19 @@
+package shop.alien.store.service;
+
+import shop.alien.store.dto.ShortLinkShortenResponse;
+
+/**
+ * 短链接:生成与解析重定向目标
+ */
+public interface ShortLinkService {
+
+    /**
+     * 为长链接生成短链(写入 Redis,带过期时间)
+     */
+    ShortLinkShortenResponse shorten(String longUrl);
+
+    /**
+     * 根据短码解析长链接;不存在或过期返回 null
+     */
+    String resolveLongUrl(String code);
+}

+ 1 - 1
alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java

@@ -519,7 +519,7 @@ public interface StoreInfoService extends IService<StoreInfo> {
      */
      */
     List<StoreDictionaryVo> getBusinessClassifyData(Integer parentId);
     List<StoreDictionaryVo> getBusinessClassifyData(Integer parentId);
 
 
-    void aiApproveStoreInfo(AiApproveStoreInfo aiApproveStoreInfo);
+    AiApproveStoreInfoResultVo aiApproveStoreInfo(AiApproveStoreInfo aiApproveStoreInfo);
 
 
 
 
     /**
     /**

+ 8 - 0
alien-store/src/main/java/shop/alien/store/service/StorePaymentConfigService.java

@@ -29,6 +29,14 @@ public interface StorePaymentConfigService extends IService<StorePaymentConfig>
     StorePaymentConfig getByStoreUserId(Integer storeUserId);
     StorePaymentConfig getByStoreUserId(Integer storeUserId);
 
 
     /**
     /**
+     * 根据支付宝应用 app_id 精确查询一条支付配置(异步通知验签、按 app_id 反查门店)
+     *
+     * @param appId 应用ID
+     * @return 多条时取一条,不存在返回 null
+     */
+    StorePaymentConfig getByAppId(String appId);
+
+    /**
      * 分页查询支付配置列表
      * 分页查询支付配置列表
      *
      *
      * @param page   分页参数
      * @param page   分页参数

+ 82 - 0
alien-store/src/main/java/shop/alien/store/service/UniPushCloudInvokeService.java

@@ -0,0 +1,82 @@
+package shop.alien.store.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.store.config.UniPushProperties;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 调用 uniCloud URL 化云函数触发推送。请求体需与云函数内解析字段一致(示例见类注释)。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UniPushCloudInvokeService {
+
+    private final UniPushProperties properties;
+
+    /**
+     * 向云函数 POST JSON,默认结构(云函数内自行读取并调用 uniPush.sendMessage):
+     * <pre>
+     * {
+     *   "push_clientid": ["cid1","cid2"],
+     *   "title": "...",
+     *   "content": "...",
+     *   "payload": { }  // 可选,自定义透传
+     * }
+     * </pre>
+     */
+    public String sendToClientIds(List<String> pushClientIds, String title, String content, Map<String, Object> payload) {
+        if (!properties.isEnabled() || StringUtils.isBlank(properties.getCloudFunctionUrl())) {
+            log.debug("uni-push 云函数调用未启用或未配置 cloudFunctionUrl,跳过");
+            return null;
+        }
+        if (pushClientIds == null || pushClientIds.isEmpty()) {
+            log.warn("uni-push 推送跳过:pushClientIds 为空");
+            return null;
+        }
+
+        JSONObject body = new JSONObject();
+        body.put("push_clientid", pushClientIds);
+        body.put("title", title != null ? title : "");
+        body.put("content", content != null ? content : "");
+        if (payload != null && !payload.isEmpty()) {
+            body.put("payload", payload);
+        }
+
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(properties.getConnectTimeoutMs());
+        factory.setReadTimeout(properties.getReadTimeoutMs());
+        RestTemplate restTemplate = new RestTemplate(factory);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        if (StringUtils.isNotBlank(properties.getAuthHeaderName())
+                && StringUtils.isNotBlank(properties.getAuthHeaderValue())) {
+            headers.set(properties.getAuthHeaderName(), properties.getAuthHeaderValue());
+        }
+
+        HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
+        try {
+            ResponseEntity<String> resp = restTemplate.postForEntity(
+                    properties.getCloudFunctionUrl(), entity, String.class);
+            String respBody = resp.getBody();
+            log.info("uni-push 云函数调用完成, status={}, body={}", resp.getStatusCode(), respBody);
+            return respBody;
+        } catch (Exception e) {
+            log.error("uni-push 云函数调用失败: {}", e.getMessage(), e);
+            throw e;
+        }
+    }
+}

+ 1103 - 0
alien-store/src/main/java/shop/alien/store/service/WeChatPartnerApplymentService.java

@@ -0,0 +1,1103 @@
+package shop.alien.store.service;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.apache.commons.lang3.StringUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.WechatPartnerApplyment;
+import shop.alien.entity.result.R;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.WechatPartnerApplymentMapper;
+import shop.alien.store.util.WXPayUtility;
+import shop.alien.util.system.OSUtil;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.lang.reflect.Type;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 微信支付服务商 — 特约商户进件(提交申请单)
+ * <p>
+ * 对应微信接口:{@code POST /v3/applyment4sub/applyment/},请求体结构以
+ * <a href="https://pay.weixin.qq.com/doc/v3/partner/4012719997">官方文档(提交申请单)</a> 为准;
+ * 提交前会经 {@link #normalizeApplymentJsonForV3(String)} 将旧版扁平 {@code identity_info} 等写法规范为当前文档结构。
+ * </p>
+ * <p>
+ * 使用与 {@code payment.wechatPartnerPay} 相同的服务商商户证书、{@code sp_mchid} 签名。
+ * </p>
+ * <p>
+ * 前端可传敏感字段明文,由 {@link #encryptSensitiveFieldsInApplymentJson(String)} 使用<strong>微信支付平台公钥</strong>
+ * 按文档做 RSA-OAEP 加密后再提交(与 {@link WXPayUtility#encrypt(PublicKey, String)} 一致)。
+ * </p>
+ * <p>
+ * 若微信返回「请确认待处理的消息是否为加密后的密文」:① 核对商户平台下载的<strong>微信支付平台公钥</strong>文件与
+ * {@code wechatPayPublicKeyId}(请求头 {@code Wechatpay-Serial})为同一组;② 勿对已是密文或 media_id 的字段再加密;
+ * ③ 可临时设 {@code payment.wechatPartnerPay.applymentSkipSensitiveEncrypt=true} 判断是否加密链路问题(仅调试)。
+ * </p>
+ */
+@Slf4j
+@Service
+public class WeChatPartnerApplymentService {
+
+    private static final String POST = "POST";
+    private static final String GET = "GET";
+    /** 微信商户图片上传:单文件最大 5MB */
+    private static final long MERCHANT_MEDIA_MAX_BYTES = 5L * 1024 * 1024;
+    private static final Gson GSON = new Gson();
+    private static final Type MAP_TYPE = new TypeToken<Map<String, Object>>() {}.getType();
+
+    /**
+     * 特约商户进件 JSON 中,凡键名在此集合内且值为非空字符串(或数字),将视为需加密的敏感信息。
+     * 名称与微信「提交申请单」文档中要求加密的字段保持一致;若文档新增字段,可在此补充。
+     */
+    private static final Set<String> APPLYMENT_SENSITIVE_FIELD_NAMES;
+
+    static {
+        Set<String> s = new HashSet<>(Arrays.asList(
+                // contact_info
+                "contact_name",
+                "contact_id_number",
+                "mobile_phone",
+                "contact_email",
+                // subject_info.identity_info.id_card_info / id_doc_info
+                "id_card_name",
+                "id_card_number",
+                "id_card_address",
+                "id_doc_name",
+                "id_doc_number",
+                "id_doc_address",
+                // ubo_info_list[]
+                "ubo_id_doc_name",
+                "ubo_id_doc_number",
+                "ubo_id_doc_address",
+                // bank_account_info(微信字段名为 account_number,非 bank_account)
+                "account_name",
+                "account_number",
+                "bank_account"
+        ));
+        APPLYMENT_SENSITIVE_FIELD_NAMES = Collections.unmodifiableSet(s);
+    }
+
+    @Value("${payment.wechatPartnerPay.host:https://api.mch.weixin.qq.com}")
+    private String wechatPayApiHost;
+
+    /**
+     * 【需配置】进件提交路径,须与微信 API 一致(注意末尾斜杠与签名用 path 一致)
+     */
+    @Value("${payment.wechatPartnerPay.applyment4subPath:/v3/applyment4sub/applyment/}")
+    private String applyment4subPath;
+
+    /**
+     * 商户图片上传(用于进件等材料),与微信 {@code POST /v3/merchant/media/upload} 一致
+     */
+    @Value("${payment.wechatPartnerPay.mediaUploadPath:/v3/merchant/media/upload}")
+    private String mediaUploadPath;
+
+    /**
+     * 【需配置】申请单号查询申请状态路径模板,须含占位符 {@code {applyment_id}},与微信签名用 URI 一致。
+     * 文档:{@code GET /v3/applyment4sub/applyment/applyment_id/{applyment_id}}
+     */
+    @Value("${payment.wechatPartnerPay.applymentQueryByIdPath:/v3/applyment4sub/applyment/applyment_id/{applyment_id}}")
+    private String applymentQueryByIdPath;
+
+    @Value("${payment.wechatPartnerPay.business.spMchId}")
+    private String spMchId;
+
+    @Value("${payment.wechatPartnerPay.business.win.privateKeyPath}")
+    private String privateWinKeyPath;
+
+    @Value("${payment.wechatPartnerPay.business.linux.privateKeyPath}")
+    private String privateLinuxKeyPath;
+
+    @Value("${payment.wechatPartnerPay.business.win.wechatPayPublicKeyFilePath}")
+    private String wechatWinPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPartnerPay.business.linux.wechatPayPublicKeyFilePath}")
+    private String wechatLinuxPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPartnerPay.business.merchantSerialNumber}")
+    private String merchantSerialNumber;
+
+    @Value("${payment.wechatPartnerPay.business.wechatPayPublicKeyId}")
+    private String wechatPayPublicKeyId;
+
+    /**
+     * 为 true 时<strong>不对</strong>进件 JSON 做敏感字段 RSA 加密,直接提交(仅联调排查「密文」类报错时使用)。
+     * 生产环境必须为 false。
+     */
+    @Value("${payment.wechatPartnerPay.applymentSkipSensitiveEncrypt:false}")
+    private boolean applymentSkipSensitiveEncrypt;
+
+    private PrivateKey privateKey;
+    private PublicKey wechatPayPublicKey;
+
+    @Resource
+    private WechatPartnerApplymentMapper wechatPartnerApplymentMapper;
+
+    @Resource
+    private StoreInfoMapper storeInfoMapper;
+
+    @PostConstruct
+    public void loadCertificates() {
+        String privateKeyPath;
+        String pubPath;
+        if ("windows".equals(OSUtil.getOsName())) {
+            privateKeyPath = privateWinKeyPath;
+            pubPath = wechatWinPayPublicKeyFilePath;
+        } else {
+            privateKeyPath = privateLinuxKeyPath;
+            pubPath = wechatLinuxPayPublicKeyFilePath;
+        }
+        log.info("[进件] 加载服务商证书 privateKeyPath={}", privateKeyPath);
+        this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
+        this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(pubPath);
+    }
+
+    /**
+     * 调用微信支付「商户图片上传」接口,返回 {@code media_id} 供进件等场景引用。
+     * <p>
+     * 签名原文为 {@code meta} 的 JSON 字符串(与 multipart 中 meta 段一致),算法见微信文档。
+     * </p>
+     *
+     * @param fileContent      图片二进制(JPG/PNG/BMP,≤5MB)
+     * @param originalFilename 原始文件名,需带合法后缀;仅用于 meta.filename 与 file 段展示名
+     * @return 成功时 data 中含 {@code media_id};失败为 R.fail
+     */
+    public R<Map<String, Object>> uploadMerchantMedia(byte[] fileContent, String originalFilename) {
+        if (fileContent == null || fileContent.length == 0) {
+            log.warn("[图片上传] 文件为空");
+            return R.fail("文件不能为空");
+        }
+        if (fileContent.length > MERCHANT_MEDIA_MAX_BYTES) {
+            log.warn("[图片上传] 超过大小限制 size={}", fileContent.length);
+            return R.fail("图片大小不能超过 5MB");
+        }
+        String safeName = sanitizeUploadFilename(originalFilename);
+        if (!hasAllowedImageExtension(safeName)) {
+            log.warn("[图片上传] 后缀不允许 filename={}", safeName);
+            return R.fail("仅支持 JPG、JPEG、BMP、PNG 格式");
+        }
+        String sha256Hex;
+        try {
+            sha256Hex = sha256HexLower(fileContent);
+        } catch (NoSuchAlgorithmException e) {
+            log.error("[图片上传] SHA256 不可用", e);
+            return R.fail("服务器摘要算法不可用");
+        }
+        // 顺序固定,保证 meta JSON 与 Authorization 签名原文、multipart 中 meta 段字节完全一致
+        Map<String, String> metaMap = new LinkedHashMap<>();
+        metaMap.put("filename", safeName);
+        metaMap.put("sha256", sha256Hex);
+        String metaJson = GSON.toJson(metaMap);
+
+        String uri = mediaUploadPath;
+        if (StringUtils.isBlank(uri)) {
+            uri = "/v3/merchant/media/upload";
+        }
+        String url = wechatPayApiHost + uri;
+        log.info("[图片上传] 调用微信 url={}, filename={}, size={}, sha256={}",
+                url, safeName, fileContent.length, sha256Hex);
+
+        MediaType fileMediaType = guessImageMediaType(safeName);
+        RequestBody filePart = RequestBody.create(fileContent, fileMediaType);
+        MultipartBody multipartBody = new MultipartBody.Builder()
+                .setType(MultipartBody.FORM)
+                .addFormDataPart("meta", metaJson)
+                .addFormDataPart("file", safeName, filePart)
+                .build();
+
+        Request.Builder builder = new Request.Builder().url(url);
+        builder.addHeader("Accept", "application/json");
+        builder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        builder.addHeader("Authorization", WXPayUtility.buildAuthorization(
+                spMchId, merchantSerialNumber, privateKey, POST, uri, metaJson));
+        builder.post(multipartBody);
+        Request request = builder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response response = client.newCall(request).execute()) {
+            String respBody = WXPayUtility.extractBody(response);
+            if (response.code() >= 200 && response.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey,
+                        response.headers(), respBody);
+                Map<String, Object> map = parseJsonToMap(respBody);
+                log.info("[图片上传] 成功 media_id={}", map != null ? map.get("media_id") : null);
+                return R.data(map);
+            }
+            log.error("[图片上传] 微信返回失败 httpCode={}, body={}", response.code(), respBody);
+            WXPayUtility.ApiException ex = new WXPayUtility.ApiException(response.code(), respBody, response.headers());
+            String errMsg = StringUtils.isNotBlank(ex.getErrorMessage()) ? ex.getErrorMessage()
+                    : (StringUtils.isNotBlank(respBody) ? respBody : "图片上传失败");
+            return R.fail(errMsg);
+        } catch (IOException e) {
+            log.error("[图片上传] HTTP 异常 url={}", url, e);
+            return R.fail("图片上传网络异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 去掉路径、仅保留文件名;若无后缀则默认 {@code upload.png}
+     */
+    private static String sanitizeUploadFilename(String originalFilename) {
+        String name = StringUtils.isNotBlank(originalFilename) ? originalFilename.trim() : "upload.png";
+        int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
+        if (slash >= 0 && slash < name.length() - 1) {
+            name = name.substring(slash + 1);
+        }
+        if (StringUtils.isBlank(name) || !name.contains(".")) {
+            name = "upload.png";
+        }
+        return name;
+    }
+
+    private static boolean hasAllowedImageExtension(String filename) {
+        String lower = filename.toLowerCase(Locale.ROOT);
+        return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
+                || lower.endsWith(".png") || lower.endsWith(".bmp");
+    }
+
+    private static MediaType guessImageMediaType(String filename) {
+        String lower = filename.toLowerCase(Locale.ROOT);
+        if (lower.endsWith(".png")) {
+            return MediaType.parse("image/png");
+        }
+        if (lower.endsWith(".bmp")) {
+            return MediaType.parse("image/bmp");
+        }
+        return MediaType.parse("image/jpeg");
+    }
+
+    private static String sha256HexLower(byte[] data) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("SHA-256");
+        byte[] digest = md.digest(data);
+        StringBuilder sb = new StringBuilder(digest.length * 2);
+        for (byte b : digest) {
+            sb.append(String.format(Locale.ROOT, "%02x", b));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 对进件请求 JSON 中需要加密的敏感字段(见 {@link #APPLYMENT_SENSITIVE_FIELD_NAMES})逐层遍历:
+     * 若存在对应键且值为非空明文,则使用<strong>当前配置的微信支付平台公钥</strong>加密为 Base64 密文后写回。
+     * <p>
+     * 算法与 {@link WXPayUtility#encrypt(PublicKey, String)} 一致(RSA/ECB/OAEPWithSHA-1AndMGF1Padding)。
+     * 若某字段在 JSON 中不存在或为空字符串,则跳过。
+     * </p>
+     *
+     * @param plainApplymentJson 前端传入的进件 JSON(敏感字段可为明文)
+     * @return 加密后的 JSON 字符串,用于签名与请求体
+     * @throws com.google.gson.JsonSyntaxException JSON 无法解析
+     * @throws IllegalArgumentException           某字段明文过长导致 RSA 加密失败等
+     */
+    public String encryptSensitiveFieldsInApplymentJson(String plainApplymentJson) {
+        if (applymentSkipSensitiveEncrypt) {
+            log.warn("[进件] 已配置 applymentSkipSensitiveEncrypt=true,跳过敏感字段加密(仅调试用)");
+            return plainApplymentJson;
+        }
+        if (StringUtils.isBlank(plainApplymentJson)) {
+            return plainApplymentJson;
+        }
+        Map<String, Object> root = GSON.fromJson(plainApplymentJson, MAP_TYPE);
+        if (root == null || root.isEmpty()) {
+            return plainApplymentJson;
+        }
+        AtomicInteger encryptedCount = new AtomicInteger(0);
+        encryptSensitiveFieldsRecursive(root, wechatPayPublicKey, encryptedCount);
+        log.info("[进件] 敏感字段加密完成,已加密字段数={}", encryptedCount.get());
+        return GSON.toJson(root);
+    }
+
+    /**
+     * 递归遍历 Map / List,对命中键名的字符串(或数字转字符串)做加密替换。
+     */
+    @SuppressWarnings("unchecked")
+    private void encryptSensitiveFieldsRecursive(Object node, PublicKey platformPublicKey, AtomicInteger encryptedCount) {
+        if (node == null) {
+            return;
+        }
+        if (node instanceof Map) {
+            Map<String, Object> map = (Map<String, Object>) node;
+            for (Map.Entry<String, Object> e : map.entrySet()) {
+                String key = e.getKey();
+                Object val = e.getValue();
+                if (APPLYMENT_SENSITIVE_FIELD_NAMES.contains(key)) {
+                    String plain = null;
+                    if (val instanceof String) {
+                        plain = (String) val;
+                    } else if (val instanceof Number) {
+                        plain = String.valueOf(val);
+                    }
+                    if (StringUtils.isNotBlank(plain)) {
+                        if (looksLikeAlreadyEncryptedOrNonPlain(plain)) {
+                            log.info("[进件] 跳过加密(疑似已是密文或非明文) key={}, len={}", key,
+                                    plain != null ? plain.length() : 0);
+                            continue;
+                        }
+                        try {
+                            String cipher = WXPayUtility.encrypt(platformPublicKey, plain);
+                            e.setValue(cipher);
+                            encryptedCount.incrementAndGet();
+                            log.debug("[进件] 已加密字段 key={}", key);
+                        } catch (RuntimeException ex) {
+                            log.error("[进件] 加密字段失败 key={}, plainLen={}", key,
+                                    plain != null ? plain.length() : 0, ex);
+                            throw ex;
+                        }
+                    }
+                } else if (val instanceof Map) {
+                    encryptSensitiveFieldsRecursive(val, platformPublicKey, encryptedCount);
+                } else if (val instanceof List) {
+                    for (Object item : (List<?>) val) {
+                        encryptSensitiveFieldsRecursive(item, platformPublicKey, encryptedCount);
+                    }
+                }
+            }
+        } else if (node instanceof List) {
+            for (Object item : (List<?>) node) {
+                encryptSensitiveFieldsRecursive(item, platformPublicKey, encryptedCount);
+            }
+        }
+    }
+
+    /**
+     * 避免对已是微信返回的密文、media_id、或长 Base64 再套一层 RSA(否则会触发「请确认待处理的消息是否为加密后的密文」)。
+     */
+    private static boolean looksLikeAlreadyEncryptedOrNonPlain(String plain) {
+        if (plain.length() < 32) {
+            return false;
+        }
+        // 图片/材料 media_id 常见形态
+        if (plain.startsWith("V") && plain.contains("_") && plain.length() > 40) {
+            return true;
+        }
+        // 典型 RSA2048+OAEP 密文 Base64 长度较大,且仅含 Base64 字符
+        if (plain.length() >= 200 && plain.matches("^[A-Za-z0-9+/=_-]+$")) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 将进件 JSON 规范为微信支付 APIv3《提交申请单》当前文档结构(身份证信息须放在 {@code identity_info.id_card_info} 下)。
+     * <ul>
+     *   <li>若 {@code subject_info.identity_info} 在根级存放了 {@code id_card_copy} 等(旧写法),且未在 {@code id_card_info} 中给出,
+     *       则迁入 {@code id_card_info}。</li>
+     *   <li>将 {@code id_card_valid_time_begin} / {@code id_card_valid_time} 重命名为 {@code card_period_begin} / {@code card_period_end}。</li>
+     *   <li>将各 {@code *_period_end}、营业执照等 {@code period_end} 中误写的英文 {@code forever} 规范为文档用语「长期」。</li>
+     *   <li>{@code settlement_info.settlement_id} 微信要求 string;若前端传为 JSON 数字(如 {@code 716}、{@code 716.0}),转为字符串避免 {@code PARAM_ERROR}。</li>
+     * </ul>
+     *
+     * @param json 前端或历史模板传入的 JSON 字符串
+     * @return 规范后的 JSON;解析失败时原样返回并打日志
+     */
+    public String normalizeApplymentJsonForV3(String json) {
+        if (StringUtils.isBlank(json)) {
+            return json;
+        }
+        Map<String, Object> root;
+        try {
+            root = GSON.fromJson(json, MAP_TYPE);
+        } catch (Exception e) {
+            log.warn("[进件] normalizeApplymentJsonForV3 解析失败,原样提交: {}", e.getMessage());
+            return json;
+        }
+        if (root == null || root.isEmpty()) {
+            return json;
+        }
+        try {
+            normalizeIdentityInfoIdCardNested(root);
+            normalizeForeverToLongPeriodRecursive(root);
+            coerceSettlementInfoSettlementIdToString(root);
+        } catch (Exception e) {
+            log.error("[进件] normalizeApplymentJsonForV3 处理异常,原样提交", e);
+            return json;
+        }
+        String out = GSON.toJson(root);
+        log.info("[进件] 已按 APIv3 文档规范 identity_info.id_card_info 等结构,输出长度={}", out.length());
+        return out;
+    }
+
+    /**
+     * 微信「入驻结算规则 ID」字段类型为 string;Gson 解析 JSON 数字会得到 {@link Number},序列化回 JSON 仍为数字,导致接口报错。
+     * 前置将 {@code settlement_info.settlement_id} 若为数字则转为十进制字符串(如 {@code 716.0} → {@code "716"})。
+     */
+    @SuppressWarnings("unchecked")
+    private void coerceSettlementInfoSettlementIdToString(Map<String, Object> root) {
+        Object si = root.get("settlement_info");
+        if (!(si instanceof Map)) {
+            return;
+        }
+        Map<String, Object> settlementInfo = (Map<String, Object>) si;
+        Object sid = settlementInfo.get("settlement_id");
+        if (!(sid instanceof Number)) {
+            return;
+        }
+        Number n = (Number) sid;
+        String asStr = new BigDecimal(n.toString()).stripTrailingZeros().toPlainString();
+        settlementInfo.put("settlement_id", asStr);
+        log.info("[进件] settlement_info.settlement_id 已由数字 {} 转为字符串 \"{}\"", n, asStr);
+    }
+
+    /**
+     * 将 {@code subject_info.identity_info} 下扁平身份证字段迁入 {@code id_card_info},并重命名有效期字段。
+     */
+    @SuppressWarnings("unchecked")
+    private static void normalizeIdentityInfoIdCardNested(Map<String, Object> root) {
+        Object subj = root.get("subject_info");
+        if (!(subj instanceof Map)) {
+            return;
+        }
+        Map<String, Object> subjectInfo = (Map<String, Object>) subj;
+        Object idObj = subjectInfo.get("identity_info");
+        if (!(idObj instanceof Map)) {
+            return;
+        }
+        Map<String, Object> identityInfo = (Map<String, Object>) idObj;
+
+        Object existing = identityInfo.get("id_card_info");
+        Map<String, Object> idCardInfo;
+        if (existing instanceof Map) {
+            idCardInfo = (Map<String, Object>) existing;
+        } else {
+            idCardInfo = new LinkedHashMap<>();
+            identityInfo.put("id_card_info", idCardInfo);
+        }
+
+        String[] migrateKeys = {
+                "id_card_copy", "id_card_national", "id_card_name", "id_card_number", "id_card_address"
+        };
+        for (String mk : migrateKeys) {
+            if (!identityInfo.containsKey(mk)) {
+                continue;
+            }
+            Object v = identityInfo.get(mk);
+            Object cur = idCardInfo.get(mk);
+            if (isNullOrBlankish(cur) && !isNullOrBlankish(v)) {
+                idCardInfo.put(mk, v);
+            }
+            identityInfo.remove(mk);
+        }
+        if (identityInfo.containsKey("id_card_valid_time_begin")) {
+            if (!idCardInfo.containsKey("card_period_begin")) {
+                idCardInfo.put("card_period_begin", identityInfo.get("id_card_valid_time_begin"));
+            }
+            identityInfo.remove("id_card_valid_time_begin");
+        }
+        if (identityInfo.containsKey("id_card_valid_time")) {
+            if (!idCardInfo.containsKey("card_period_end")) {
+                idCardInfo.put("card_period_end", identityInfo.get("id_card_valid_time"));
+            }
+            identityInfo.remove("id_card_valid_time");
+        }
+
+        renameMapKeyIfPresent(idCardInfo, "id_card_valid_time_begin", "card_period_begin");
+        renameMapKeyIfPresent(idCardInfo, "id_card_valid_time", "card_period_end");
+
+        if (idCardInfo.isEmpty()) {
+            identityInfo.remove("id_card_info");
+        }
+    }
+
+    private static boolean isNullOrBlankish(Object v) {
+        if (v == null) {
+            return true;
+        }
+        if (v instanceof String) {
+            return StringUtils.isBlank((String) v);
+        }
+        return false;
+    }
+
+    private static void renameMapKeyIfPresent(Map<String, Object> map, String oldKey, String newKey) {
+        if (!map.containsKey(oldKey)) {
+            return;
+        }
+        if (map.containsKey(newKey)) {
+            map.remove(oldKey);
+            return;
+        }
+        Object val = map.remove(oldKey);
+        map.put(newKey, val);
+    }
+
+    /**
+     * 文档要求身份证等到期填「长期」,部分示例使用 forever;统一为「长期」避免校验失败。
+     */
+    @SuppressWarnings("unchecked")
+    private static void normalizeForeverToLongPeriodRecursive(Object node) {
+        if (node instanceof Map) {
+            Map<String, Object> m = (Map<String, Object>) node;
+            for (Map.Entry<String, Object> e : m.entrySet()) {
+                String k = e.getKey();
+                Object v = e.getValue();
+                if (v instanceof String && "forever".equalsIgnoreCase((String) v)) {
+                    if ("period_end".equals(k) || (k != null && k.endsWith("period_end"))) {
+                        e.setValue("长期");
+                    }
+                } else if (v instanceof Map || v instanceof List) {
+                    normalizeForeverToLongPeriodRecursive(v);
+                }
+            }
+        } else if (node instanceof List) {
+            for (Object item : (List<?>) node) {
+                normalizeForeverToLongPeriodRecursive(item);
+            }
+        }
+    }
+
+    /**
+     * 提交特约商户进件申请单,请求体为微信要求的 JSON(含 business_code、subject_info、bank_account_info 等)。
+     *
+     * @param requestJson  与微信文档一致的 JSON 字符串(可先经 {@link #normalizeApplymentJsonForV3(String)} 规范结构)。
+     *                     若传入 {@code storeId},服务端会写入必填字段 {@code business_code}(规则见 {@link #mergeBusinessCodeIntoApplymentJson(String, Integer)});
+     *                     未传 {@code storeId} 时须在 JSON 中自行填写非空的 {@code business_code}。
+     * @param idempotencyKey 可选;传入则写入 {@code Idempotency-Key} 请求头,用于幂等重试(同一进件业务请固定同一值)
+     * @param storeId        推荐传入;本系统门店 {@code store_info.id}。传入时后端生成 {@code business_code = 服务商商户号(sp_mchid) + "_" + storeId};审核通过后将 {@code sub_mchid} 写入该门店
+     * @return 成功时 data 为微信返回的 JSON 解析后的 Map;失败为 R.fail
+     */
+    public R<Map<String, Object>> submitApplyment(String requestJson, String idempotencyKey, Integer storeId) {
+        if (StringUtils.isBlank(requestJson)) {
+            log.warn("[进件] 请求体为空");
+            return R.fail("请求体不能为空");
+        }
+        final String normalizedJson = normalizeApplymentJsonForV3(requestJson);
+        final String jsonWithBusinessCode;
+        try {
+            jsonWithBusinessCode = mergeBusinessCodeIntoApplymentJson(normalizedJson, storeId);
+        } catch (IllegalArgumentException e) {
+            log.warn("[进件] business_code 处理失败: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        }
+        final String payloadJson;
+        try {
+            payloadJson = encryptSensitiveFieldsInApplymentJson(jsonWithBusinessCode);
+        } catch (Exception e) {
+            log.error("[进件] 敏感字段加密失败", e);
+            return R.fail("进件请求敏感信息加密失败: " + e.getMessage());
+        }
+
+        // 落库:提交前先写入/更新一条记录(以 sp_mchid + business_code 唯一)
+        String businessCode = extractBusinessCodeSafe(jsonWithBusinessCode);
+        upsertApplymentOnSubmitStart(businessCode, idempotencyKey, payloadJson, storeId);
+
+        String uri = applyment4subPath;
+        String url = wechatPayApiHost + uri;
+        log.info("[进件] 调用微信进件接口 url={}, bodyLen={}, hasIdempotencyKey={}",
+                url, payloadJson.length(), StringUtils.isNotBlank(idempotencyKey));
+
+        Request.Builder builder = new Request.Builder().url(url);
+        builder.addHeader("Accept", "application/json");
+        builder.addHeader("Content-Type", "application/json; charset=utf-8");
+        builder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        builder.addHeader("Authorization", WXPayUtility.buildAuthorization(
+                spMchId, merchantSerialNumber, privateKey, POST, uri, payloadJson));
+        if (StringUtils.isNotBlank(idempotencyKey)) {
+            builder.addHeader("Idempotency-Key", idempotencyKey);
+        }
+
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(payloadJson, jsonMediaType);
+        builder.method(POST, body);
+        Request request = builder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response response = client.newCall(request).execute()) {
+            String respBody = WXPayUtility.extractBody(response);
+            if (response.code() >= 200 && response.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey,
+                        response.headers(), respBody);
+                Map<String, Object> map = parseJsonToMap(respBody);
+                log.info("[进件] 微信受理成功 httpCode={}", response.code());
+                upsertApplymentOnSubmitResult(businessCode, map, respBody);
+                return R.data(map);
+            }
+            log.error("[进件] 微信返回失败 httpCode={}, body={}", response.code(), respBody);
+            WXPayUtility.ApiException ex = new WXPayUtility.ApiException(response.code(), respBody, response.headers());
+            String errMsg = StringUtils.isNotBlank(ex.getErrorMessage()) ? ex.getErrorMessage()
+                    : (StringUtils.isNotBlank(respBody) ? respBody : "进件请求失败");
+            upsertApplymentOnSubmitError(businessCode, errMsg, respBody);
+            return R.fail(errMsg);
+        } catch (IOException e) {
+            log.error("[进件] HTTP 调用异常 url={}", url, e);
+            upsertApplymentOnSubmitError(businessCode, "进件请求网络异常: " + e.getMessage(), null);
+            return R.fail("进件请求网络异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 申请单号查询申请状态,对应微信
+     * <a href="https://pay.weixin.qq.com/doc/v3/partner/4012697052">GET /v3/applyment4sub/applyment/applyment_id/{applyment_id}</a>。
+     * <p>
+     * 成功时返回体含 {@code applyment_state}、{@code applyment_state_msg}、{@code sign_url}、
+     * {@code audit_detail}(驳回时)等,与官方文档一致。
+     * </p>
+     *
+     * @param applymentId 微信支付分配的申请单号
+     * @return 成功时 data 为微信 JSON 解析后的 Map;失败为 R.fail
+     */
+    public R<Map<String, Object>> queryApplymentStateByApplymentId(long applymentId) {
+        if (applymentId <= 0) {
+            log.warn("[进件查询] applyment_id 非法 applymentId={}", applymentId);
+            return R.fail("applyment_id 必须为正整数");
+        }
+        String template = StringUtils.isNotBlank(applymentQueryByIdPath)
+                ? applymentQueryByIdPath
+                : "/v3/applyment4sub/applyment/applyment_id/{applyment_id}";
+        String uri = template.replace("{applyment_id}", String.valueOf(applymentId));
+        String url = wechatPayApiHost + uri;
+        log.info("[进件查询] 调用微信查询申请单状态 url={}, uri={}", url, uri);
+
+        Request.Builder builder = new Request.Builder().url(url);
+        builder.addHeader("Accept", "application/json");
+        builder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        builder.addHeader("Authorization", WXPayUtility.buildAuthorization(
+                spMchId, merchantSerialNumber, privateKey, GET, uri, null));
+        builder.get();
+        Request request = builder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response response = client.newCall(request).execute()) {
+            String respBody = WXPayUtility.extractBody(response);
+            if (response.code() >= 200 && response.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey,
+                        response.headers(), respBody);
+                Map<String, Object> map = parseJsonToMap(respBody);
+                log.info("[进件查询] 成功 applymentId={}, state={}",
+                        applymentId, map != null ? map.get("applyment_state") : null);
+                upsertApplymentOnQueryResult(applymentId, map, respBody);
+                return R.data(map);
+            }
+            log.error("[进件查询] 微信返回失败 applymentId={}, httpCode={}, body={}",
+                    applymentId, response.code(), respBody);
+            WXPayUtility.ApiException ex = new WXPayUtility.ApiException(response.code(), respBody, response.headers());
+            String errMsg = StringUtils.isNotBlank(ex.getErrorMessage()) ? ex.getErrorMessage()
+                    : (StringUtils.isNotBlank(respBody) ? respBody : "查询申请单状态失败");
+            upsertApplymentOnQueryError(applymentId, errMsg, respBody);
+            return R.fail(errMsg);
+        } catch (IOException e) {
+            log.error("[进件查询] HTTP 异常 applymentId={}, url={}", applymentId, url, e);
+            upsertApplymentOnQueryError(applymentId, "查询申请单状态网络异常: " + e.getMessage(), null);
+            return R.fail("查询申请单状态网络异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 按门店查询最近一次特约商户进件状态(仅读本地库,不调微信接口)。
+     * <p>
+     * 从 {@code wechat_partner_applyment} 按 {@code store_id} 取最新一条(按更新时间、主键倒序)。
+     * 进件提交、定时任务或查询接口已把微信侧状态写入本表,此处直接返回落库数据即可。
+     * </p>
+     *
+     * @param storeId 门店主键 {@link StoreInfo#getId()},须与进件提交时 query 参数 {@code storeId} 一致
+     * @return 成功时 data 为列表:有记录时含一条 Map,无进件记录时为空列表(不返回业务错误)
+     */
+    public R<List<WechatPartnerApplyment>> queryApplymentStateByStoreId(Integer storeId) {
+        if (storeId == null || storeId <= 0) {
+            log.warn("[进件查询] storeId 非法 storeId={}", storeId);
+            return R.fail("门店 storeId 不能为空且必须大于 0");
+        }
+        List<WechatPartnerApplyment> record = findLatestApplymentByStoreId(storeId);
+        if (record == null) {
+            log.info("[进件查询] 按门店无进件记录,返回空列表 storeId={}", storeId);
+            return R.data(Collections.emptyList());
+        }
+        log.info("[进件查询] 按门店返回本地库状态 storeId={}", storeId);
+        return R.data(record);
+    }
+
+    /**
+     * 同一门店可能存在多次进件记录时,取最新一条用于展示与查询。
+     */
+    private List<WechatPartnerApplyment> findLatestApplymentByStoreId(int storeId) {
+        LambdaQueryWrapper<WechatPartnerApplyment> qw = new LambdaQueryWrapper<WechatPartnerApplyment>()
+                .eq(WechatPartnerApplyment::getStoreId, storeId)
+                .orderByDesc(WechatPartnerApplyment::getUpdatedAt)
+                .orderByDesc(WechatPartnerApplyment::getId);
+        return wechatPartnerApplymentMapper.selectList(qw);
+    }
+
+
+    /**
+     * 合并 {@code business_code} 到进件根节点。
+     * <p>
+     * 微信要求 {@code business_code}(业务申请编号)必填且唯一。当请求带 {@code storeId} 时,由后端统一生成为:
+     * {@code 服务商商户号(sp_mchid) + "_" + storeId}(下划线分隔,与配置项 {@code payment.wechatPartnerPay.business.spMchId} 一致),
+     * 避免前端漏传或传空串导致 {@code PARAM_ERROR}。
+     * </p>
+     * <p>
+     * 未传 {@code storeId} 时,若 JSON 中 {@code business_code} 已非空则保留;否则抛出 {@link IllegalArgumentException}。
+     * </p>
+     *
+     * @param normalizedJson 已规范化的进件 JSON 字符串
+     * @param storeId        可选,有值则覆盖/写入 {@code business_code}
+     * @return 合并后的 JSON 字符串
+     */
+    private String mergeBusinessCodeIntoApplymentJson(String normalizedJson, Integer storeId) {
+        Map<String, Object> root;
+        try {
+            root = GSON.fromJson(normalizedJson, MAP_TYPE);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("进件 JSON 解析失败,无法写入 business_code: " + e.getMessage());
+        }
+        if (root == null) {
+            root = new LinkedHashMap<>();
+        }
+        if (storeId != null) {
+            String sp = StringUtils.trimToEmpty(spMchId);
+            if (StringUtils.isBlank(sp)) {
+                throw new IllegalArgumentException("未配置服务商商户号 payment.wechatPartnerPay.business.spMchId,无法生成 business_code");
+            }
+            String code = sp + "_" + storeId + "_" + System.currentTimeMillis();
+            root.put("business_code", code);
+            log.info("[进件] 已生成并写入 business_code={}(sp_mchid_storeId 格式,storeId={})", code, storeId);
+            return GSON.toJson(root);
+        }
+        Object v = root.get("business_code");
+        String existing = v == null ? "" : String.valueOf(v).trim();
+        if (StringUtils.isBlank(existing)) {
+            throw new IllegalArgumentException(
+                    "请传入查询参数 storeId(后端将生成 business_code=服务商商户号_storeId),或在 JSON 根节点填写非空的 business_code");
+        }
+        return GSON.toJson(root);
+    }
+
+    /**
+     * 从进件 JSON 中提取 business_code(用于落库幂等主键);解析失败则返回 null。
+     */
+    private static String extractBusinessCodeSafe(String requestJson) {
+        if (StringUtils.isBlank(requestJson)) {
+            return null;
+        }
+        try {
+            Map<String, Object> root = GSON.fromJson(requestJson, MAP_TYPE);
+            Object v = root != null ? root.get("business_code") : null;
+            if (v == null) {
+                return null;
+            }
+            String s = String.valueOf(v).trim();
+            return StringUtils.isNotBlank(s) ? s : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private void upsertApplymentOnSubmitStart(String businessCode, String idempotencyKey, String payloadJson, Integer storeId) {
+        if (StringUtils.isBlank(businessCode)) {
+            log.warn("[进件落库] business_code 为空,跳过落库(请在请求体中填写 business_code)");
+            return;
+        }
+        String dbSpMchid = spMchidForDb();
+        Date now = new Date();
+        WechatPartnerApplyment record = findByBusinessCode(businessCode);
+        if (record == null) {
+            record = new WechatPartnerApplyment();
+            record.setSpMchid(dbSpMchid);
+            record.setBusinessCode(businessCode);
+            record.setCreatedAt(now);
+            record.setIsApproved(0);
+            record.setApplymentState("APPLYMENT_STATE_EDITTING");
+        }
+        // 若历史记录的 sp_mchid 为空(或 UNKNOWN),且现在已可确定,则补齐
+        if (StringUtils.isBlank(record.getSpMchid()) || "UNKNOWN".equalsIgnoreCase(record.getSpMchid())) {
+            record.setSpMchid(dbSpMchid);
+        }
+        if (storeId != null) {
+            record.setStoreId(storeId);
+        }
+        record.setIdempotencyKey(idempotencyKey);
+        record.setLastSubmitTime(now);
+        record.setRequestJson(payloadJson);
+        record.setUpdatedAt(now);
+        if (record.getId() == null) {
+            wechatPartnerApplymentMapper.insert(record);
+        } else {
+            wechatPartnerApplymentMapper.updateById(record);
+        }
+        log.info("[进件落库] submitStart sp_mchid={}, business_code={}, storeId={}, id={}", record.getSpMchid(), businessCode, record.getStoreId(), record.getId());
+    }
+
+    private void upsertApplymentOnSubmitResult(String businessCode, Map<String, Object> respMap, String respBody) {
+        if (StringUtils.isBlank(businessCode)) {
+            return;
+        }
+        String dbSpMchid = spMchidForDb();
+        WechatPartnerApplyment record = findByBusinessCode(businessCode);
+        if (record == null) {
+            record = new WechatPartnerApplyment();
+            record.setSpMchid(dbSpMchid);
+            record.setBusinessCode(businessCode);
+            record.setCreatedAt(new Date());
+        }
+        if (StringUtils.isBlank(record.getSpMchid()) || "UNKNOWN".equalsIgnoreCase(record.getSpMchid())) {
+            record.setSpMchid(dbSpMchid);
+        }
+        Date now = new Date();
+        record.setLastSubmitRespJson(respBody);
+        record.setUpdatedAt(now);
+        record.setApplymentId(asLong(respMap != null ? respMap.get("applyment_id") : null));
+        record.setSubMchid(asString(respMap != null ? respMap.get("sub_mchid") : null));
+        record.setSignUrl(asString(respMap != null ? respMap.get("sign_url") : null));
+        record.setApplymentState(asString(respMap != null ? respMap.get("applyment_state") : null));
+        record.setApplymentStateMsg(asString(respMap != null ? respMap.get("applyment_state_msg") : null));
+        record.setIsApproved(calcIsApproved(record.getApplymentState()));
+        if (record.getIsApproved() != null && record.getIsApproved() == 2) {
+            record.setRejectReason(extractRejectReason(respMap));
+        }
+        if (record.getId() == null) {
+            wechatPartnerApplymentMapper.insert(record);
+        } else {
+            wechatPartnerApplymentMapper.updateById(record);
+        }
+        log.info("[进件落库] submitResult business_code={}, applyment_id={}, state={}",
+                businessCode, record.getApplymentId(), record.getApplymentState());
+        syncStoreInfoWechatSubMchidIfApproved(record);
+    }
+
+    private void upsertApplymentOnSubmitError(String businessCode, String errMsg, String respBody) {
+        if (StringUtils.isBlank(businessCode)) {
+            return;
+        }
+        String dbSpMchid = spMchidForDb();
+        WechatPartnerApplyment record = findByBusinessCode(businessCode);
+        if (record == null) {
+            record = new WechatPartnerApplyment();
+            record.setSpMchid(dbSpMchid);
+            record.setBusinessCode(businessCode);
+            record.setCreatedAt(new Date());
+        }
+        if (StringUtils.isBlank(record.getSpMchid()) || "UNKNOWN".equalsIgnoreCase(record.getSpMchid())) {
+            record.setSpMchid(dbSpMchid);
+        }
+        Date now = new Date();
+        record.setLastSubmitRespJson(respBody);
+        record.setRejectReason(trimTo(errMsg, 2048));
+        record.setUpdatedAt(now);
+        // 错误时不强制设置 isApproved=2,因为可能是参数错误/系统错误;保留 0
+        if (record.getIsApproved() == null) {
+            record.setIsApproved(0);
+        }
+        if (record.getId() == null) {
+            wechatPartnerApplymentMapper.insert(record);
+        } else {
+            wechatPartnerApplymentMapper.updateById(record);
+        }
+        log.warn("[进件落库] submitError business_code={}, msg={}", businessCode, errMsg);
+    }
+
+    private void upsertApplymentOnQueryResult(long applymentId, Map<String, Object> respMap, String respBody) {
+        Date now = new Date();
+        String dbSpMchid = spMchidForDb();
+        WechatPartnerApplyment record = findByApplymentId(applymentId);
+        if (record == null) {
+            record = new WechatPartnerApplyment();
+            record.setSpMchid(dbSpMchid);
+            record.setApplymentId(applymentId);
+            record.setCreatedAt(now);
+            // 兜底:尝试从响应里带回 business_code,方便后续用业务编号对齐
+            record.setBusinessCode(asString(respMap != null ? respMap.get("business_code") : null));
+        }
+        if (StringUtils.isBlank(record.getSpMchid()) || "UNKNOWN".equalsIgnoreCase(record.getSpMchid())) {
+            record.setSpMchid(dbSpMchid);
+        }
+        record.setLastQueryTime(now);
+        record.setLastQueryRespJson(respBody);
+        record.setUpdatedAt(now);
+        record.setSubMchid(asString(respMap != null ? respMap.get("sub_mchid") : null));
+        record.setSignUrl(asString(respMap != null ? respMap.get("sign_url") : null));
+        record.setApplymentState(asString(respMap != null ? respMap.get("applyment_state") : null));
+        record.setApplymentStateMsg(asString(respMap != null ? respMap.get("applyment_state_msg") : null));
+        record.setIsApproved(calcIsApproved(record.getApplymentState()));
+        if (record.getIsApproved() != null && record.getIsApproved() == 2) {
+            record.setRejectReason(extractRejectReason(respMap));
+        }
+        if (record.getId() == null) {
+            wechatPartnerApplymentMapper.insert(record);
+        } else {
+            wechatPartnerApplymentMapper.updateById(record);
+        }
+        syncStoreInfoWechatSubMchidIfApproved(record);
+    }
+
+    private void upsertApplymentOnQueryError(long applymentId, String errMsg, String respBody) {
+        WechatPartnerApplyment record = findByApplymentId(applymentId);
+        if (record == null) {
+            // 查询失败但本地无记录时,不强行插入;避免污染
+            log.warn("[进件落库] queryError 本地无记录 applyment_id={}, msg={}", applymentId, errMsg);
+            return;
+        }
+        Date now = new Date();
+        record.setLastQueryTime(now);
+        record.setLastQueryRespJson(respBody);
+        record.setRejectReason(trimTo(errMsg, 2048));
+        record.setUpdatedAt(now);
+        wechatPartnerApplymentMapper.updateById(record);
+    }
+
+    /**
+     * 进件状态为「已完成」且微信返回 {@code sub_mchid} 时,将特约商户号写入关联门店 {@code store_info.wechat_sub_mchid}。
+     */
+    private void syncStoreInfoWechatSubMchidIfApproved(WechatPartnerApplyment record) {
+        if (record == null || record.getStoreId() == null) {
+            return;
+        }
+        if (record.getIsApproved() == null || record.getIsApproved() != 1) {
+            return;
+        }
+        if (StringUtils.isBlank(record.getSubMchid())) {
+            log.warn("[进件] 状态已完成但 sub_mchid 为空,跳过写入门店 storeId={}", record.getStoreId());
+            return;
+        }
+        try {
+            StoreInfo si = storeInfoMapper.selectById(record.getStoreId());
+            if (si == null) {
+                log.warn("[进件] 门店不存在,跳过写入 wechat_sub_mchid storeId={}", record.getStoreId());
+                return;
+            }
+            storeInfoMapper.update(null, new LambdaUpdateWrapper<StoreInfo>()
+                    .eq(StoreInfo::getId, record.getStoreId())
+                    .set(StoreInfo::getWechatSubMchid, record.getSubMchid()));
+            log.info("[进件] 已写入门店 wechat_sub_mchid storeId={}, sub_mchid={}", record.getStoreId(), record.getSubMchid());
+        } catch (Exception e) {
+            log.error("[进件] 写入 store_info.wechat_sub_mchid 失败 storeId={}", record.getStoreId(), e);
+        }
+    }
+
+    private WechatPartnerApplyment findByBusinessCode(String businessCode) {
+        LambdaQueryWrapper<WechatPartnerApplyment> qw = new LambdaQueryWrapper<WechatPartnerApplyment>()
+                .eq(WechatPartnerApplyment::getBusinessCode, businessCode)
+                .last("limit 1");
+        // 兼容:提交初期可能未能确定 sp_mchid,因此此处不强制按 sp_mchid 过滤
+        return wechatPartnerApplymentMapper.selectOne(qw);
+    }
+
+    private WechatPartnerApplyment findByApplymentId(long applymentId) {
+        LambdaQueryWrapper<WechatPartnerApplyment> qw = new LambdaQueryWrapper<WechatPartnerApplyment>()
+                .eq(WechatPartnerApplyment::getApplymentId, applymentId)
+                .last("limit 1");
+        return wechatPartnerApplymentMapper.selectOne(qw);
+    }
+
+    /**
+     * 提交初期某些环境可能尚未配置/注入 sp_mchid。为保证落库可用:
+     * - 若已配置,则返回真实 sp_mchid;
+     * - 若未配置,则返回固定占位值 UNKNOWN(避免 NOT NULL/唯一键问题)。
+     */
+    private String spMchidForDb() {
+        if (StringUtils.isNotBlank(spMchId)) {
+            return spMchId;
+        }
+        log.warn("[进件落库] sp_mchid 未配置,使用占位 UNKNOWN(建议尽快补齐 payment.wechatPartnerPay.business.spMchId)");
+        return "UNKNOWN";
+    }
+
+    private static Integer calcIsApproved(String applymentState) {
+        if (StringUtils.isBlank(applymentState)) {
+            return 0;
+        }
+        if ("APPLYMENT_STATE_FINISHED".equalsIgnoreCase(applymentState)) {
+            return 1;
+        }
+        if ("APPLYMENT_STATE_REJECTED".equalsIgnoreCase(applymentState)) {
+            return 2;
+        }
+        return 0;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static String extractRejectReason(Map<String, Object> respMap) {
+        if (respMap == null) {
+            return null;
+        }
+        Object audit = respMap.get("audit_detail");
+        if (!(audit instanceof List)) {
+            return null;
+        }
+        List<Object> list = (List<Object>) audit;
+        List<String> reasons = new ArrayList<>();
+        for (Object item : list) {
+            if (!(item instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> m = (Map<String, Object>) item;
+            String field = asString(m.get("field"));
+            String name = asString(m.get("field_name"));
+            String reason = asString(m.get("reject_reason"));
+            if (StringUtils.isBlank(reason)) {
+                continue;
+            }
+            String line = StringUtils.isNotBlank(name) ? name : (StringUtils.isNotBlank(field) ? field : "");
+            if (StringUtils.isNotBlank(line)) {
+                reasons.add(line + ":" + reason);
+            } else {
+                reasons.add(reason);
+            }
+        }
+        return trimTo(String.join(";", reasons), 2048);
+    }
+
+    private static String asString(Object v) {
+        if (v == null) {
+            return null;
+        }
+        String s = String.valueOf(v);
+        s = s != null ? s.trim() : null;
+        return StringUtils.isNotBlank(s) ? s : null;
+    }
+
+    private static Long asLong(Object v) {
+        if (v == null) {
+            return null;
+        }
+        try {
+            if (v instanceof Number) {
+                return ((Number) v).longValue();
+            }
+            String s = String.valueOf(v).trim();
+            if (StringUtils.isBlank(s)) {
+                return null;
+            }
+            return Long.parseLong(s);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static String trimTo(String s, int maxLen) {
+        if (s == null) {
+            return null;
+        }
+        if (s.length() <= maxLen) {
+            return s;
+        }
+        return s.substring(0, maxLen);
+    }
+
+    private static Map<String, Object> parseJsonToMap(String json) {
+        if (StringUtils.isBlank(json)) {
+            return new HashMap<>();
+        }
+        try {
+            return GSON.fromJson(json, MAP_TYPE);
+        } catch (Exception e) {
+            log.warn("[进件] 响应 JSON 解析为 Map 失败,返回原始字符串封装", e);
+            Map<String, Object> m = new HashMap<>();
+            m.put("raw", json);
+            return m;
+        }
+    }
+}

+ 801 - 0
alien-store/src/main/java/shop/alien/store/service/impl/AlipayZftOnboardingServiceImpl.java

@@ -0,0 +1,801 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alipay.api.*;
+import com.alipay.api.domain.*;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.AlipayZftCreateRecord;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.dto.AlipayZftBizRequestDto;
+import shop.alien.entity.store.dto.AlipayZftMerchantCreateDto;
+import shop.alien.entity.store.dto.AlipayZftMerchantSimplecreateDto;
+import shop.alien.mapper.AlipayZftCreateRecordMapper;
+import shop.alien.store.service.AlipayZftOnboardingService;
+import shop.alien.store.service.StorePaymentConfigService;
+
+import java.lang.reflect.Method;
+import java.util.Date;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 使用门店 {@link StorePaymentConfig} 中的证书与密钥,以服务商/平台应用身份调用支付宝进件接口。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlipayZftOnboardingServiceImpl implements AlipayZftOnboardingService {
+
+    @Value("${payment.aliPay.host}")
+    private String aliPayHost;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+
+    private final AlipayZftCreateRecordMapper alipayZftCreateRecordMapper;
+
+    @Override
+    public R<String> zftConsult(AlipayZftBizRequestDto request) {
+        AntMerchantExpandIndirectZftConsultRequest api = new AntMerchantExpandIndirectZftConsultRequest();
+        return invoke(request, api);
+    }
+
+    @Override
+    public R<String> zftCreate(AntMerchantExpandIndirectZftCreateModel request1, Integer storeId) throws AlipayApiException {
+        // 初始化SDK
+        AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+
+        // 构造请求参数以调用接口
+        AntMerchantExpandIndirectZftCreateRequest request = new AntMerchantExpandIndirectZftCreateRequest();
+        AntMerchantExpandIndirectZftCreateModel model = new AntMerchantExpandIndirectZftCreateModel();
+
+        // 设置商户编号
+        model.setExternalId("2088560449225278");
+
+        // 设置商户类型
+        model.setMerchantType("01");
+
+        // 设置进件的二级商户名称
+        model.setName("爱丽恩(大连)餐饮有限公司");
+
+        // 设置商户别名
+        model.setAliasName("爱丽恩(大连)餐饮有限公司");
+
+        // 设置商户类别码 mcc
+        model.setMcc("B0002");
+
+        // 设置商户使用服务
+        List<String> service = new ArrayList<String>();
+        service.add("当面付");
+        service.add("app支付");
+        model.setService(service);
+
+        // 设置内景照
+        List<String> inDoorImages = new ArrayList<String>();
+        inDoorImages.add("889493da-4b52-4506-9a25-5dc29b96753b.png");
+        model.setInDoorImages(inDoorImages);
+
+        // 设置商户证件编号
+        model.setCertNo("91210202MAE7M17B6P");
+
+        // 设置商户证件类型
+        model.setCertType("201");
+
+        // 设置商户证件图片url
+        model.setCertImage("c2a7a12b-26c1-4eb3-bfcd-2f404fc2ca63.png");
+
+        // 设置证件反面图片
+//        model.setCertImageBack("25a10d04-ca9c-41aa-896f-3e0904bec470.jpg");
+
+        // 设置法人名称
+        model.setLegalName("唐永顺");
+
+        // 设置法人身份证号
+        model.setLegalCertNo("210106196804141555");
+
+        // 设置法人身份证正面url
+//        model.setLegalCertFrontImage("25a10d04-ca9c-41aa-896f-3e0904bec470.jpg");
+
+        // 设置法人身份证反面url
+//        model.setLegalCertBackImage("25a10d04-ca9c-41aa-896f-3e0904bec470.jpg");
+
+        // 设置经营地址
+        AddressInfo businessAddress = new AddressInfo();
+        businessAddress.setAddress("大连市中山区港湾街12A号8A-7号");
+        businessAddress.setDistrictCode("210202");
+//        businessAddress.setLatitude("60.270001");
+        businessAddress.setCityCode("210200");
+//        businessAddress.setPoiid("B0FFIVU189");
+        businessAddress.setProvinceCode("210000");
+//        businessAddress.setLongitude("120.760001");
+        model.setBusinessAddress(businessAddress);
+
+        // 设置客服电话
+//        model.setServicePhone("0571-85022088");
+
+        // 设置商户联系人信息
+        List<ContactInfo> contactInfos = new ArrayList<ContactInfo>();
+        ContactInfo contactInfos0 = new ContactInfo();
+//        contactInfos0.setIdCardNo("110000199001011234");
+//        contactInfos0.setPhone("0571-85022088");
+        contactInfos0.setName("王骏");
+        contactInfos0.setMobile("13352287427");
+//        contactInfos0.setEmail("user@domain.com");
+        contactInfos.add(contactInfos0);
+        model.setContactInfos(contactInfos);
+
+        // 设置结算银行卡
+//        List<SettleCardInfo> bizCards = new ArrayList<SettleCardInfo>();
+//        SettleCardInfo bizCards0 = new SettleCardInfo();
+//        bizCards0.setAccountInstName("招商银行");
+//        bizCards0.setBankCode("103290003044");
+//        bizCards0.setAccountType("DC");
+//        bizCards0.setUsageType("01");
+//        bizCards0.setAccountHolderName("张三");
+//        bizCards0.setAccountInstCity("杭州市");
+//        bizCards0.setAccountInstId("CMB");
+//        bizCards0.setAccountNo("6214855710610408");
+//        bizCards0.setAccountInstProvince("浙江省");
+//        bizCards0.setAccountBranchName("招商银行杭州高新支行");
+//        bizCards.add(bizCards0);
+//        model.setBizCards(bizCards);
+
+        // 设置商户行业资质
+//        List<IndustryQualificationInfo> qualifications = new ArrayList<IndustryQualificationInfo>();
+//        IndustryQualificationInfo qualifications0 = new IndustryQualificationInfo();
+//        qualifications0.setIndustryQualificationImage("c6c0c7a1-b9d5-4e5d-b9d4-9eed39f00e65.jpg");
+//        qualifications0.setIndustryQualificationType("323");
+//        qualifications.add(qualifications0);
+//        model.setQualifications(qualifications);
+
+        // 设置门头照
+        List<String> outDoorImages = new ArrayList<String>();
+        outDoorImages.add("64212801-093d-49a8-9769-5e2b288fb858.png");
+        model.setOutDoorImages(outDoorImages);
+
+        // 设置授权函
+//        model.setLicenseAuthLetterImage("c6c0c7a1-b9d5-4e5d-b9d4-9eed39f00e65.jpg");
+
+
+
+        // 设置二级商户与服务商的签约时间
+//        model.setSignTimeWithIsv("2015-04-15");
+
+        // 设置结算支付宝账号
+        model.setAlipayLogonId("ailien@alienyan.cn");
+
+        // 设置sites
+        List<SiteInfo> sites = new ArrayList<SiteInfo>();
+        SiteInfo sites0 = new SiteInfo();
+//        sites0.setIcpOrgName("支付宝(中国)网络技术有限公司");
+        sites0.setSiteType("02");
+//        sites0.setSiteDomain("www.alipay.com");
+//        sites0.setScreenshotImage("c6c0c7a1-b9d5-4e5d-b9d4-9eed39f00e65.jpg");
+//        sites0.setRemark("备注说明");
+//        sites0.setAuthLetterImage("c6c0c7a1-b9d5-4e5d-b9d4-9eed39f00e65.jpg");
+        sites0.setSiteName("应用2.0签约2025092468860692");
+//        sites0.setMarket("豌豆荚");
+//        sites0.setPassword("123456");
+//        sites0.setDownload("https://itunes.apple.com/cn/app/id333206289?mt=8");
+//        sites0.setTinyAppId("2021004105652035");
+//        sites0.setSiteUrl("https://open.alipay.com");
+//        sites0.setIcpServiceName("支付宝");
+//        sites0.setRemarkImage("c6c0c7a1-b9d5-4e5d-b9d4-9eed39f00e65.jpg");
+//        sites0.setAccount("测试账号");
+//        sites0.setIcpNo("沪ICP备15027489号-2");
+//        sites0.setStatus("ONLINE");
+        sites.add(sites0);
+        model.setSites(sites);
+
+        // 设置开票资料信息
+//        MerchantInvoiceInfo invoiceInfo = new MerchantInvoiceInfo();
+//        invoiceInfo.setMailTelephone("057162288888");
+//        invoiceInfo.setTaxPayerQualification("01");
+//        invoiceInfo.setAddress("浙江省杭州市西湖区西溪路蚂蚁金服");
+//        invoiceInfo.setAcceptElectronic(true);
+//        invoiceInfo.setTelephone("057162288888");
+//        invoiceInfo.setTitle("蚂蚁金服(杭州)信息技术有限公司");
+//        invoiceInfo.setMailName("张三");
+//        invoiceInfo.setAutoInvoice(true);
+//        invoiceInfo.setTaxPayerValid("19981011");
+//        invoiceInfo.setTaxNo("51010482542598631219");
+//        invoiceInfo.setBankName("中国银行");
+//        AddressInfo mailAddress = new AddressInfo();
+//        mailAddress.setAddress("万塘路18号黄龙时代广场B座");
+//        mailAddress.setDistrictCode("371002");
+//        mailAddress.setLatitude("60.270001");
+//        mailAddress.setCityCode("371000");
+//        mailAddress.setPoiid("B0FFIVU189");
+//        mailAddress.setProvinceCode("370000");
+//        mailAddress.setType("BUSINESS_ADDRESS");
+//        mailAddress.setLongitude("120.760001");
+//        invoiceInfo.setMailAddress(mailAddress);
+//        invoiceInfo.setBankAccount("1234567812345678123");
+//        model.setInvoiceInfo(invoiceInfo);
+
+        // 设置目前只有个体工商户商户类型要求填入本字段
+//        model.setCertName("xxxx小卖铺");
+
+        // 设置签约支付宝账户
+        model.setBindingAlipayLogonId("ailien@alienyan.cn");
+
+        // 设置默认可不填
+//        model.setLegalCertType("100");
+
+        // 设置默认结算规则
+//        DefaultSettleRule defaultSettleRule = new DefaultSettleRule();
+//        defaultSettleRule.setDefaultSettleType("alipayAccount");
+//        defaultSettleRule.setDefaultSettleTarget("myalipay@alipay.com");
+//        model.setDefaultSettleRule(defaultSettleRule);
+
+        // 设置商家性质
+//        model.setMerchantNature("01");
+
+        //java测试用
+//        request.setBizModel(model);
+        request.setBizModel(request1);
+        // 第三方代调用模式下请设置app_auth_token
+        // request.putOtherTextParam("app_auth_token", "<-- 请填写应用授权令牌 -->");
+
+        Date now = new Date();
+        AlipayZftCreateRecord record = new AlipayZftCreateRecord();
+        record.setStoreId(storeId);
+        record.setExternalId(request1 != null ? request1.getExternalId() : null);
+        record.setMerchantName(request1 != null ? request1.getName() : null);
+        record.setRequestJson(request1 != null ? JSON.toJSONString(request1) : "{}");
+        record.setCreatedTime(now);
+        record.setUpdatedTime(now);
+
+        AntMerchantExpandIndirectZftCreateResponse response;
+        try {
+            response = alipayClient.execute(request);
+        } catch (AlipayApiException e) {
+            record.setInvokeSuccess(false);
+            record.setSubMsg(StringUtils.isNotBlank(e.getErrMsg()) ? e.getErrMsg() : e.getMessage());
+            alipayZftCreateRecordMapper.insert(record);
+            throw e;
+        }
+        record.setResponseBody(response != null ? response.getBody() : null);
+        record.setInvokeSuccess(response != null && response.isSuccess());
+        if (response != null) {
+            record.setSubCode(response.getSubCode());
+            record.setSubMsg(response.getSubMsg());
+            record.setOrderId(request1 != null ? response.getOrderId() : null);
+        }
+        alipayZftCreateRecordMapper.insert(record);
+
+        if (response.isSuccess()) {
+            return R.data("调用成功", response.getBody());
+        } else {
+            return R.fail("调用失败:" + response.getBody());
+        }
+    }
+
+    private static AlipayConfig getAlipayConfig() {
+
+        String privateKey  = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCBTOvScNrk0mw+WbR6Qtx2+HrPxB5IU/V2HSPGZUTDNiCiSU9UZcuXlo0UNwMksFYR43btk4BHW9OsZETdr3uJzbEEko4jQPrY49V6V8d3yFUiexcbLKf9W2X+h7EwdPDahHk6wmM73Lm500xzv8JxdGeo/d2uGTh+qD66HcPW+QYj1YbjRV1JyCOiJ2OiwLcb28arNNLOgj6PktQ69FPSWS4hWrM7nl6DLRr6SdOlyfBT47gwPtW0BQ96/2b9O+qxKBPfrcqdRDH323HvrGFDwa8GoWTdchnKwFjmfIFHMvf+eq3tzilFi8H7vQRtzVRrndwrNa0z+1ss07fcWFDRAgMBAAECggEAItgU0OAizPk7vE22SiBMgy8RAX5rXrhpdIwDwQo3Tpf+kV1KKIdKJy6mFCWDDlcKysVOnlVag2BmmZVnzYnls8wfgQjxjuSK9Pno5JBVK51r+9/J6UPOfYMs6Duu700EPw7mEISj81TXJBGiD6tEfgiNisfm/mzDgbZbORKeXQbaTyrtB+GZn6FNSyyHA1vraARMrgfMEGNzQ4AbtfcUxGO+mejdTFs0PxAq6lovHBY3fYYHI1Nx6kf9iPoom/G4UrcMO67W6QU+1tOCZCXjy4bD2y421z/8XD73+WDyYp+Tjy0hTLqVZc7TpYAOximo1vMIUe23EdJJngdlkdpDFQKBgQDFyETL0knwBSakLfAe2BmFb2x++B4YXUnt4dGbCFBnVooxf5i06GVt/CrfkJhYK6hBSowOScIRf8P6BOSQptRZb2/I1ngQm4vcpAZw6EjUTlgOj/J3XJ+ApUNQnRqE28jDrE4m2RHg4BkQo6yA3DizJAluPCtFoCYDm1a7dV7u7wKBgQCnXEH5sD8VSxURv02/gn80g/uZIP/EOU3ycjBEqZdRGkNINwXT+zqrlZIGYb+bxLvU/R2OqKC5vhcyAL3T1A8ORYqPu5KLnAxg7C+rHuVilUWwCEH7POpCk+ETPXCZwcNvLNa5PIqBH/gdV9Y9PBTef6J4rN6V6TDFgosf5by8PwKBgDpVG71Fk1sAGep4RgbC05wgRc6Y3T9wXDqVzJ098YDY7D83E9HfbPLoWbjAS75Nef1vwCkCpgNFPIbD5KmpGp4aGM0SPC0hwzlbAy9PwxMi3CPHXsrHfZ+SnmzrOQQQUoErk40vnm9FiP74VwtWaD6llUZ25ohNeIi9yvHU5x/vAoGAdU2d1JOq85LHtsO+i9+8pyNnAsJ1YqTDtI5CtK2lqKvewswGIrlxOvi//AchVN3ExZmP0QDyfp31BhAs/T8iOl+Vqf7PzVjX+Eszch5aqwlzadmv3ZepnnamCGVE+hAsmkz0R6tebPjqYC7Ds/HbssQFLc4EyVBD5fwE5ZuR+OMCgYAvGHUYpi0dY9uMHXzL721tIopiwUfKCgLAn3yhSH3p7644NxHBqLLaBLVT2q7JAZQUaZUvXlwiyxU1zvo0xmvcbnB/Vd2qp8KbEUkvHyIYVJkM6Fn+9xBosorcrHv+7B2V1XR9WQcXvppxbN/8farWGuAA0anBD+UGrxd8B0/hHg==";
+        String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnxevClLpYc6c8A9lwmnftPO2BeU+X6aZ/+b/n1Cvq096VJKiHmqcsRgRbrnSlmPDHRrQpDti4en2Ys0L2lx7CObIr/2xP/jJVwjIO1iWHUj/w/NAbjv7dLW/FFY4SeNp8rU+hlgGgviyUxzonfNfr3v+o8grFqQq27/hiZJAofsQRMQu1dEQqoKdJj7eQLkTstiK5miJMyQ+Y3tLztrEUMBz/zRgaCEfGqmFmRZ2diy2X+1dGaX6H4+0QJ2u50eY2QTBkNuvREGbAn6/lttAgvg/+CywPYKGeC4xOfnl5wP8iA1QXYbXrVJRkZjU097nyOmSNhLy9KvJH2BNpojS1QIDAQAB";
+        AlipayConfig alipayConfig = new AlipayConfig();
+        alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
+        alipayConfig.setAppId("2021005196608960");
+        alipayConfig.setPrivateKey(privateKey);
+        alipayConfig.setFormat("json");
+        alipayConfig.setAlipayPublicKey(alipayPublicKey);
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+        return alipayConfig;
+    }
+
+
+
+    @Override
+    public R<String> zftSimplecreate(AlipayZftMerchantSimplecreateDto request) {
+        if (request == null) {
+            return R.fail("请求体不能为空");
+        }
+        if (request.getStoreId() == null) {
+            return R.fail("门店ID不能为空");
+        }
+        R<StorePaymentConfig> cfgCheck = loadConfigByStoreId(request.getStoreId());
+        if (R.isNotSuccess(cfgCheck) || cfgCheck.getData() == null) {
+            return R.fail(cfgCheck.getMsg());
+        }
+        try {
+            AntMerchantExpandIndirectZftSimplecreateRequest api = new AntMerchantExpandIndirectZftSimplecreateRequest();
+            api.setBizModel(toSimplecreateModel(request));
+            if (StringUtils.isNotBlank(request.getNotifyUrl())) {
+                api.setNotifyUrl(request.getNotifyUrl());
+            }
+            applyAlipayCommonParams(api, request.getAppAuthToken(), request.getApiVersion());
+            String appId = resolveAppId(cfgCheck.getData(), request.getAppId());
+            if (StringUtils.isBlank(appId)) {
+                return R.fail("app_id 不能为空,请传 app_id 或在门店支付配置中维护应用 ID");
+            }
+            AlipayClient client = new DefaultAlipayClient(buildAlipayConfig(cfgCheck.getData(), appId));
+            AntMerchantExpandIndirectZftSimplecreateResponse response = client.certificateExecute(api);
+            if (response == null) {
+                return R.fail("支付宝返回为空");
+            }
+            if (!response.isSuccess()) {
+                String sub = response.getSubMsg();
+                String msg = StringUtils.isNotBlank(sub) ? sub : "调用失败";
+                log.warn("支付宝 simplecreate 失败 subCode={} subMsg={}", response.getSubCode(), response.getSubMsg());
+                return R.fail(msg);
+            }
+            return R.data(response.getBody());
+        } catch (ReflectiveOperationException e) {
+            log.error("支付宝 simplecreate 公共参数设置失败", e);
+            return R.fail("构建请求失败:" + e.getMessage());
+        } catch (AlipayApiException e) {
+            log.error("支付宝 simplecreate 异常", e);
+            return R.fail(StringUtils.isNotBlank(e.getErrMsg()) ? e.getErrMsg() : e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> orderQuery(String orderId) {
+
+        if (StringUtils.isBlank(orderId)) {
+            return R.fail("orderId 不能为空");
+        }
+//        R<StorePaymentConfig> cfgCheck = loadConfigByStoreId(storeId);
+//        if (R.isNotSuccess(cfgCheck) || cfgCheck.getData() == null) {
+//            return R.fail(cfgCheck.getMsg());
+//        }
+        try {
+            // 初始化SDK
+            AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+            AntMerchantExpandIndirectZftorderQueryRequest request = new AntMerchantExpandIndirectZftorderQueryRequest();
+            AntMerchantExpandIndirectZftorderQueryModel model = new AntMerchantExpandIndirectZftorderQueryModel();
+            model.setOrderId(orderId.trim());
+            request.setBizModel(model);
+//            String appId = resolveAppId(cfgCheck.getData(), null);
+//            if (StringUtils.isBlank(appId)) {
+//                return R.fail("app_id 不能为空,请在门店支付配置中维护应用 ID");
+//            }
+            AntMerchantExpandIndirectZftorderQueryResponse response = alipayClient.execute(request);
+//            AntMerchantExpandIndirectZftorderQueryResponse response = client.certificateExecute(request);
+            if (response == null) {
+                return R.fail("支付宝返回为空");
+            }
+            if (!response.isSuccess()) {
+                String sub = response.getSubMsg();
+                String msg = StringUtils.isNotBlank(sub) ? sub : "调用失败";
+                log.warn("支付宝 order.query 失败 subCode={} subMsg={}", response.getSubCode(), response.getSubMsg());
+                return R.fail(msg);
+            }
+            return R.data(response.getBody());
+        } catch (AlipayApiException e) {
+            log.error("支付宝 order.query 异常 orderId={}", orderId, e);
+            return R.fail(StringUtils.isNotBlank(e.getErrMsg()) ? e.getErrMsg() : e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> zftModify(AlipayZftBizRequestDto request1) throws AlipayApiException {
+
+                AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+// 直付通二级商户作废接口:ant.merchant.expand.indirect.zft.delete
+        AntMerchantExpandIndirectZftDeleteRequest request = new AntMerchantExpandIndirectZftDeleteRequest();
+        request.setBizContent("{"
+                + "\"smid\":\"2088480335620690\"," // 二级商户号
+                + "\"external_id\":\"你的平台商户ID\"" // 可选,平台内部商户标识
+                + "}");
+        AntMerchantExpandIndirectZftDeleteResponse response = alipayClient.execute(request);
+        if (response.isSuccess()) {
+            return R.data("调用成功", response.getBody());
+        } else {
+            return R.data("调用失败", response.getBody());
+            // sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
+            // String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
+            // System.out.println(diagnosisUrl);
+        }
+    }
+
+    @Override
+    public R<String> supplementCreate(AlipayZftBizRequestDto request) {
+        AntMerchantExpandIndirectSupplementCreateRequest api = new AntMerchantExpandIndirectSupplementCreateRequest();
+        return invoke(request, api);
+    }
+
+    @Override
+    public R<String> imageUpload(Integer storeId, String imageType, MultipartFile imageContent,
+                                 String appId, String appAuthToken, String apiVersion) throws AlipayApiException {
+        // 初始化SDK
+        AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+//        if (storeId == null) {
+//            return R.fail("门店ID不能为空");
+//        }
+        if (StringUtils.isBlank(imageType)) {
+            return R.fail("imageType 不能为空");
+        }
+        if (imageContent == null || imageContent.isEmpty()) {
+            return R.fail("imageContent 不能为空");
+        }
+        if (imageContent.getSize() > 10L * 1024 * 1024) {
+            return R.fail("图片大小不能超过10MB");
+        }
+//        R<StorePaymentConfig> cfgCheck = loadConfigByStoreId(storeId);
+//        if (R.isNotSuccess(cfgCheck) || cfgCheck.getData() == null) {
+//            return R.fail(cfgCheck.getMsg());
+//        }
+        try {
+            AntMerchantExpandIndirectImageUploadRequest request = new AntMerchantExpandIndirectImageUploadRequest();
+            request.setImageType(imageType.trim().toLowerCase());
+            String fileName = StringUtils.defaultIfBlank(imageContent.getOriginalFilename(), "image_upload.bin");
+            request.setImageContent(new FileItem(fileName, imageContent.getBytes()));
+            applyAlipayCommonParams(request, appAuthToken, apiVersion);
+//            String resolvedAppId = resolveAppId(cfgCheck.getData(), appId);
+//            if (StringUtils.isBlank(resolvedAppId)) {
+//                return R.fail("app_id 不能为空,请传 app_id 或在门店支付配置中维护应用 ID");
+//            }
+//            AlipayClient client = new DefaultAlipayClient(buildAlipayConfig(cfgCheck.getData(), resolvedAppId));
+            AntMerchantExpandIndirectImageUploadResponse response = alipayClient.execute(request);
+            if (response == null) {
+                return R.fail("支付宝返回为空");
+            }
+            if (!response.isSuccess()) {
+                String sub = response.getSubMsg();
+                return R.fail(StringUtils.isNotBlank(sub) ? sub : "图片上传失败");
+            }
+            return R.data(response.getBody());
+        } catch (ReflectiveOperationException e) {
+            log.error("支付宝图片上传公共参数设置失败 storeId={}", storeId, e);
+            return R.fail("构建请求失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("支付宝图片上传异常 storeId={}", storeId, e);
+            if (e instanceof AlipayApiException) {
+                AlipayApiException ex = (AlipayApiException) e;
+                return R.fail(StringUtils.isNotBlank(ex.getErrMsg()) ? ex.getErrMsg() : ex.getMessage());
+            }
+            return R.fail("图片上传失败:" + e.getMessage());
+        }
+    }
+
+    private <T extends AlipayResponse> R<String> invoke(AlipayZftBizRequestDto dto, AlipayRequest<T> apiRequest) {
+        if (dto == null) {
+            return R.fail("请求体不能为空");
+        }
+        R<StorePaymentConfig> cfgCheck = loadConfig(dto);
+        if (R.isNotSuccess(cfgCheck) || cfgCheck.getData() == null) {
+            return R.fail(cfgCheck.getMsg());
+        }
+        Map<String, Object> biz = dto.getBizContent();
+        String bizJson = (biz == null || biz.isEmpty()) ? "{}" : JSON.toJSONString(biz);
+        // 各 *Request 类实现 AlipayRequest 接口,自带 bizContent/notifyUrl 字段与 setter,并非继承 AlipayObject
+        try {
+            applyBizContentAndNotify(dto, apiRequest, bizJson);
+            applyAlipayCommonParams(apiRequest, dto.getAppAuthToken(), dto.getApiVersion());
+        } catch (ReflectiveOperationException e) {
+            log.error("设置支付宝请求参数失败", e);
+            return R.fail("构建请求失败:" + e.getMessage());
+        }
+        try {
+            String appId = resolveAppId(cfgCheck.getData(), dto.getAppId());
+            if (StringUtils.isBlank(appId)) {
+                return R.fail("app_id 不能为空,请传 app_id 或在门店支付配置中维护应用 ID");
+            }
+            AlipayClient client = new DefaultAlipayClient(buildAlipayConfig(cfgCheck.getData(), appId));
+            T response = client.certificateExecute(apiRequest);
+            if (response == null) {
+                return R.fail("支付宝返回为空");
+            }
+            if (!response.isSuccess()) {
+                String sub = response.getSubMsg();
+                String msg = StringUtils.isNotBlank(sub) ? sub : "调用失败";
+                log.warn("支付宝进件接口失败 method={} subCode={} subMsg={}",
+                        apiRequest.getApiMethodName(), response.getSubCode(), response.getSubMsg());
+                return R.fail(msg);
+            }
+            return R.data(response.getBody());
+        } catch (AlipayApiException e) {
+            log.error("支付宝进件接口异常 method={}", apiRequest.getApiMethodName(), e);
+            return R.fail(StringUtils.isNotBlank(e.getErrMsg()) ? e.getErrMsg() : e.getMessage());
+        }
+    }
+
+    private R<StorePaymentConfig> loadConfig(AlipayZftBizRequestDto dto) {
+        if (dto == null || dto.getStoreId() == null) {
+            return R.fail("门店ID不能为空");
+        }
+        return loadConfigByStoreId(dto.getStoreId());
+    }
+
+    private R<StorePaymentConfig> loadConfigByStoreId(Integer storeId) {
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            return R.fail("该门店未配置支付参数");
+        }
+        if (StringUtils.isBlank(config.getAppSecretCert())
+                || config.getAppPublicCert() == null || config.getAppPublicCert().length == 0
+                || config.getAlipayPublicCert() == null || config.getAlipayPublicCert().length == 0
+                || config.getAlipayRootCert() == null || config.getAlipayRootCert().length == 0) {
+            return R.fail("门店支付配置不完整(缺少应用私钥或证书)");
+        }
+        return R.data(config);
+    }
+
+    /**
+     * @param appId 已解析出的应用 ID(请求覆盖或门店配置)
+     */
+    private com.alipay.api.AlipayConfig buildAlipayConfig(StorePaymentConfig config, String appId) {
+        com.alipay.api.AlipayConfig alipayConfig = new com.alipay.api.AlipayConfig();
+        alipayConfig.setServerUrl(aliPayHost);
+        alipayConfig.setAppId(appId);
+        alipayConfig.setPrivateKey(config.getAppSecretCert());
+        alipayConfig.setFormat("json");
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+        if (config.getAppPublicCert() != null && config.getAppPublicCert().length > 0) {
+            alipayConfig.setAppCertContent(new String(config.getAppPublicCert(), StandardCharsets.UTF_8));
+        }
+        if (config.getAlipayPublicCert() != null && config.getAlipayPublicCert().length > 0) {
+            alipayConfig.setAlipayPublicCertContent(new String(config.getAlipayPublicCert(), StandardCharsets.UTF_8));
+        }
+        if (config.getAlipayRootCert() != null && config.getAlipayRootCert().length > 0) {
+            alipayConfig.setRootCertContent(new String(config.getAlipayRootCert(), StandardCharsets.UTF_8));
+        }
+        return alipayConfig;
+    }
+
+    /**
+     * 请求体中的 app_id 优先,否则使用门店支付配置中的应用 ID。
+     */
+    private static String resolveAppId(StorePaymentConfig config, String overrideFromRequest) {
+        if (StringUtils.isNotBlank(overrideFromRequest)) {
+            return overrideFromRequest.trim();
+        }
+        if (config != null && StringUtils.isNotBlank(config.getAppId())) {
+            return config.getAppId().trim();
+        }
+        return null;
+    }
+
+    /**
+     * 支付宝 OpenAPI 的 Request 类均提供 {@code setBizContent(String)}、{@code setNotifyUrl(String)},
+     * 但声明在各自类上,接口 {@link AlipayRequest} 未包含这些方法,故用反射统一设置。
+     */
+    private void applyBizContentAndNotify(AlipayZftBizRequestDto dto, Object apiRequest, String bizJson)
+            throws ReflectiveOperationException {
+        Class<?> clazz = apiRequest.getClass();
+        Method setBiz = clazz.getMethod("setBizContent", String.class);
+        setBiz.invoke(apiRequest, bizJson);
+        if (StringUtils.isNotBlank(dto.getNotifyUrl())) {
+            Method setNotify = clazz.getMethod("setNotifyUrl", String.class);
+            setNotify.invoke(apiRequest, dto.getNotifyUrl());
+        }
+    }
+
+    /**
+     * 支付宝开放平台公共参数中需业务侧显式传入的部分:<br/>
+     * - {@code app_auth_token}:第三方代调用时通过 {@code putOtherTextParam} 追加;<br/>
+     * - {@code api_version}:若请求类支持 {@code setApiVersion} 则设置。<br/>
+     * 其余如 {@code app_id}、{@code method}、{@code format}、{@code charset}、{@code sign_type}、签名、{@code timestamp}、{@code version}
+     * 由 {@link DefaultAlipayClient} 与 {@link com.alipay.api.AlipayConfig} 自动处理。
+     */
+    private void applyAlipayCommonParams(Object apiRequest, String appAuthToken, String apiVersion)
+            throws ReflectiveOperationException {
+        if (apiRequest == null) {
+            return;
+        }
+        Class<?> clazz = apiRequest.getClass();
+        if (StringUtils.isNotBlank(appAuthToken)) {
+            Method put = clazz.getMethod("putOtherTextParam", String.class, String.class);
+            put.invoke(apiRequest, "app_auth_token", appAuthToken.trim());
+        }
+        if (StringUtils.isNotBlank(apiVersion)) {
+            try {
+                Method setVer = clazz.getMethod("setApiVersion", String.class);
+                setVer.invoke(apiRequest, apiVersion.trim());
+            } catch (NoSuchMethodException ignored) {
+                log.debug("当前请求类无 setApiVersion:{}", clazz.getName());
+            }
+        }
+    }
+
+    /**
+     * @return 错误文案;null 表示校验通过
+     */
+    private String validateZftCreateRequest(AlipayZftMerchantCreateDto dto) {
+        if (dto == null) {
+            return "请求体不能为空";
+        }
+        if (dto.getStoreId() == null) {
+            return "门店ID不能为空";
+        }
+        Map<String, Object> biz = buildZftCreateBizContent(dto);
+        return validateZftCreateBizMap(biz);
+    }
+
+    /**
+     * 校验合并后的 biz_content(字段可全部来自 biz_content,或来自顶层 + extraBiz)。
+     */
+    private static String validateZftCreateBizMap(Map<String, Object> biz) {
+        if (biz == null || biz.isEmpty()) {
+            return "biz_content 不能为空:请提供 biz_content,或提供 external_id、merchant_type、name、alias_name、mcc、service 等顶层字段";
+        }
+        String externalId = bizString(biz, "external_id");
+        if (StringUtils.isBlank(externalId)) {
+            return "external_id 不能为空";
+        }
+        if (externalId.length() > 64) {
+            return "external_id 长度不能超过64";
+        }
+        String merchantType = bizString(biz, "merchant_type");
+        if (StringUtils.isBlank(merchantType)) {
+            return "merchant_type 不能为空";
+        }
+        if (merchantType.length() > 20) {
+            return "merchant_type 长度不能超过20";
+        }
+        String name = bizString(biz, "name");
+        if (StringUtils.isBlank(name)) {
+            return "name 不能为空";
+        }
+        if (name.length() > 128) {
+            return "name 长度不能超过128";
+        }
+        String aliasName = bizString(biz, "alias_name");
+        if (StringUtils.isBlank(aliasName)) {
+            return "alias_name 不能为空";
+        }
+        if (aliasName.length() > 128) {
+            return "alias_name 长度不能超过128";
+        }
+        String mcc = bizString(biz, "mcc");
+        if (StringUtils.isBlank(mcc)) {
+            return "mcc 不能为空";
+        }
+        if (mcc.length() > 10) {
+            return "mcc 长度不能超过10";
+        }
+        if (!bizHasNonEmptyService(biz)) {
+            return "service 不能为空";
+        }
+        return null;
+    }
+
+    private static String bizString(Map<String, Object> biz, String key) {
+        Object v = biz == null ? null : biz.get(key);
+        if (v == null) {
+            return null;
+        }
+        if (v instanceof String) {
+            return ((String) v).trim();
+        }
+        return String.valueOf(v).trim();
+    }
+
+    private static boolean bizHasNonEmptyService(Map<String, Object> biz) {
+        Object v = biz.get("service");
+        if (v == null) {
+            return false;
+        }
+        if (v instanceof List) {
+            return !((List<?>) v).isEmpty();
+        }
+        if (v instanceof String) {
+            return StringUtils.isNotBlank((String) v);
+        }
+        if (v instanceof String[]) {
+            return ((String[]) v).length > 0;
+        }
+        return true;
+    }
+
+    /**
+     * 合并顺序:biz_content → extraBiz → 非空顶层字段(后者覆盖同名键)。
+     */
+    private Map<String, Object> buildZftCreateBizContent(AlipayZftMerchantCreateDto dto) {
+        Map<String, Object> biz = new LinkedHashMap<>();
+        if (dto.getBizContent() != null && !dto.getBizContent().isEmpty()) {
+            biz.putAll(dto.getBizContent());
+        }
+        if (dto.getExtraBiz() != null && !dto.getExtraBiz().isEmpty()) {
+            biz.putAll(dto.getExtraBiz());
+        }
+        if (StringUtils.isNotBlank(dto.getExternalId())) {
+            biz.put("external_id", dto.getExternalId().trim());
+        }
+        if (StringUtils.isNotBlank(dto.getMerchantType())) {
+            biz.put("merchant_type", dto.getMerchantType().trim());
+        }
+        if (StringUtils.isNotBlank(dto.getName())) {
+            biz.put("name", dto.getName().trim());
+        }
+        if (StringUtils.isNotBlank(dto.getAliasName())) {
+            biz.put("alias_name", dto.getAliasName().trim());
+        }
+        if (StringUtils.isNotBlank(dto.getMcc())) {
+            biz.put("mcc", dto.getMcc().trim());
+        }
+        if (dto.getService() != null && !dto.getService().isEmpty()) {
+            biz.put("service", dto.getService());
+        }
+        return biz;
+    }
+
+    private AntMerchantExpandIndirectZftSimplecreateModel toSimplecreateModel(AlipayZftMerchantSimplecreateDto d) {
+        AntMerchantExpandIndirectZftSimplecreateModel m = new AntMerchantExpandIndirectZftSimplecreateModel();
+        m.setExternalId(d.getExternalId());
+        m.setBindingAlipayLogonId(d.getBindingAlipayLogonId());
+        m.setAliasName(d.getAliasName());
+        m.setServicePhone(d.getServicePhone());
+        m.setName(d.getName());
+        m.setMcc(d.getMcc());
+        m.setService(d.getService());
+        m.setAlipayLogonId(d.getAlipayLogonId());
+        m.setOutDoorImages(d.getOutDoorImages());
+        m.setInDoorImages(d.getInDoorImages());
+        m.setLicenseAuthLetterImage(d.getLicenseAuthLetterImage());
+        if (d.getContactInfos() != null) {
+            m.setContactInfos(toContactInfo(d.getContactInfos()));
+        }
+        if (d.getDefaultSettleRule() != null) {
+            m.setDefaultSettleRule(toDefaultSettleRule(d.getDefaultSettleRule()));
+        }
+        if (d.getBizCards() != null) {
+            m.setBizCards(toSettleCardInfo(d.getBizCards()));
+        }
+        if (d.getBusinessAddress() != null) {
+            m.setBusinessAddress(toAddressInfo(d.getBusinessAddress()));
+        }
+        return m;
+    }
+
+    private static ContactInfo toContactInfo(AlipayZftMerchantSimplecreateDto.ContactPart p) {
+        ContactInfo c = new ContactInfo();
+        c.setName(p.getName());
+        c.setMobile(p.getMobile());
+        c.setIdCardNo(p.getIdCardNo());
+        c.setPhone(p.getPhone());
+        c.setEmail(p.getEmail());
+        return c;
+    }
+
+    private static DefaultSettleRule toDefaultSettleRule(AlipayZftMerchantSimplecreateDto.DefaultSettleRulePart p) {
+        DefaultSettleRule r = new DefaultSettleRule();
+        r.setDefaultSettleType(p.getDefaultSettleType());
+        r.setDefaultSettleTarget(p.getDefaultSettleTarget());
+        return r;
+    }
+
+    private static SettleCardInfo toSettleCardInfo(AlipayZftMerchantSimplecreateDto.SettleCardPart p) {
+        SettleCardInfo s = new SettleCardInfo();
+        s.setAccountInstName(p.getAccountInstName());
+        s.setBankCode(p.getBankCode());
+        s.setAccountType(p.getAccountType());
+        s.setUsageType(p.getUsageType());
+        s.setAccountHolderName(p.getAccountHolderName());
+        s.setAccountInstCity(p.getAccountInstCity());
+        s.setAccountInstId(p.getAccountInstId());
+        s.setAccountNo(p.getAccountNo());
+        s.setAccountBranchName(p.getAccountBranchName());
+        s.setAccountInstProvince(p.getAccountInstProvince());
+        return s;
+    }
+
+    private static AddressInfo toAddressInfo(AlipayZftMerchantSimplecreateDto.AddressPart p) {
+        AddressInfo a = new AddressInfo();
+        a.setAddress(p.getAddress());
+        a.setDistrictCode(p.getDistrictCode());
+        a.setLatitude(p.getLatitude());
+        a.setCityCode(p.getCityCode());
+        a.setPoiid(p.getPoiid());
+        a.setProvinceCode(p.getProvinceCode());
+        a.setLongitude(p.getLongitude());
+        a.setType(p.getType());
+        return a;
+    }
+}

+ 119 - 0
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -22,6 +22,8 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.CommonCommentVo;
 import shop.alien.entity.store.vo.CommonCommentVo;
 import shop.alien.entity.store.vo.CommonRatingVo;
 import shop.alien.entity.store.vo.CommonRatingVo;
+import shop.alien.entity.store.vo.MyMergedReviewItemVo;
+import shop.alien.entity.store.vo.OrderReviewVo;
 import shop.alien.entity.store.vo.StoreInfoScoreVo;
 import shop.alien.entity.store.vo.StoreInfoScoreVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.entity.storePlatform.StoreOperationalActivity;
 import shop.alien.entity.storePlatform.StoreOperationalActivity;
@@ -31,6 +33,7 @@ import shop.alien.util.common.Constants;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.CommonRatingService;
+import shop.alien.store.service.OrderReviewService;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 import shop.alien.store.util.ai.AiContentModerationUtil;
@@ -88,6 +91,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
     private final StoreOperationalActivityMapper storeOperationalActivityMapper;
     private final StoreOperationalActivityMapper storeOperationalActivityMapper;
     private final ReceiptAuditUtil receiptAuditUtil;
     private final ReceiptAuditUtil receiptAuditUtil;
     private final StoreClockInMapper storeClockInMapper;
     private final StoreClockInMapper storeClockInMapper;
+    private final OrderReviewService orderReviewService;
 
 
     public static final List<String> SERVICES_LIST = ImmutableList.of(
     public static final List<String> SERVICES_LIST = ImmutableList.of(
             TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
             TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
@@ -1132,6 +1136,121 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         return iPageR;
         return iPageR;
     }
     }
 
 
+    @Override
+    public R<IPage<MyMergedReviewItemVo>> getMyMergedRatingList(Integer pageNum, Integer pageSize, Integer businessType,
+                                                                Long userId, Integer auditStatus, Integer currentUserId) {
+        if (userId == null) {
+            return R.fail("userId不能为空");
+        }
+        int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        int ps = pageSize == null || pageSize < 1 ? 10 : pageSize;
+        int perSource = Math.min(pn * ps, 1000);
+        int biz = businessType != null ? businessType : RatingBusinessTypeEnum.STORE_RATING.getBusinessType();
+
+        List<MyMergedReviewItemVo> merged = new ArrayList<>();
+
+        R storeR = getMyRatingList(1, perSource, biz, userId, auditStatus);
+        if (R.isSuccess(storeR) && storeR.getData() != null) {
+            IPage<?> sp = (IPage<?>) storeR.getData();
+            for (Object o : sp.getRecords()) {
+                if (o instanceof CommonRatingVo) {
+                    merged.add(toMergedStoreItem((CommonRatingVo) o));
+                }
+            }
+        }
+
+        R<IPage<OrderReviewVo>> lawyerR = orderReviewService.getMyReviewList(1, perSource, userId.intValue(), currentUserId);
+        if (R.isSuccess(lawyerR) && lawyerR.getData() != null && lawyerR.getData().getRecords() != null) {
+            for (OrderReviewVo vo : lawyerR.getData().getRecords()) {
+                merged.add(toMergedLawyerItem(vo));
+            }
+        }
+
+        merged.sort(Comparator.comparing(MyMergedReviewItemVo::getPublishTime,
+                Comparator.nullsLast(Comparator.naturalOrder())).reversed());
+
+        long storeTotal = 0L;
+        if (R.isSuccess(storeR) && storeR.getData() != null) {
+            storeTotal = ((IPage<?>) storeR.getData()).getTotal();
+        }
+        long lawyerTotal = 0L;
+        if (R.isSuccess(lawyerR) && lawyerR.getData() != null) {
+            lawyerTotal = lawyerR.getData().getTotal();
+        }
+        long total = storeTotal + lawyerTotal;
+
+        int from = (pn - 1) * ps;
+        List<MyMergedReviewItemVo> pageSlice;
+        if (from >= merged.size()) {
+            pageSlice = Collections.emptyList();
+        } else {
+            int to = Math.min(from + ps, merged.size());
+            pageSlice = merged.subList(from, to);
+        }
+
+        Page<MyMergedReviewItemVo> page = new Page<>(pn, ps, total);
+        page.setRecords(pageSlice);
+        return R.data(page);
+    }
+
+    private static MyMergedReviewItemVo toMergedStoreItem(CommonRatingVo vo) {
+        MyMergedReviewItemVo m = new MyMergedReviewItemVo();
+        m.setItemType("STORE");
+        m.setStoreRatingId(vo.getId());
+        m.setPublishTime(vo.getCreatedTime());
+        m.setScore(vo.getScore());
+        m.setContent(vo.getContent());
+        m.setMediaUrls(splitImageUrlsToMediaList(vo.getImageUrls()));
+        m.setStoreId(vo.getBusinessId());
+        m.setStoreName(vo.getStoreName());
+        m.setStoreIcon(null);
+        m.setBusinessType(vo.getBusinessType());
+        return m;
+    }
+
+    private static MyMergedReviewItemVo toMergedLawyerItem(OrderReviewVo vo) {
+        MyMergedReviewItemVo m = new MyMergedReviewItemVo();
+        m.setItemType("LAWYER");
+        m.setLawyerReviewId(vo.getId());
+        m.setPublishTime(vo.getCreatedTime());
+        m.setScore(vo.getOverallRating());
+        m.setContent(vo.getReviewContent());
+        m.setMediaUrls(vo.getReviewImages() != null ? new ArrayList<>(vo.getReviewImages()) : new ArrayList<>());
+        m.setLawyerName(vo.getLawyerName());
+        m.setLawyerAvatar(vo.getLawyerAvatar());
+        m.setLawyerFirm(vo.getLawFirmName());
+        m.setRatingTag(lawyerRatingTagText(vo.getOverallRating()));
+        m.setLawyerUserId(vo.getLawyerUserId());
+        m.setOrderId(vo.getOrderId());
+        return m;
+    }
+
+    private static List<String> splitImageUrlsToMediaList(String imageUrls) {
+        if (imageUrls == null || imageUrls.trim().isEmpty()) {
+            return new ArrayList<>();
+        }
+        return Arrays.stream(imageUrls.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .collect(Collectors.toList());
+    }
+
+    private static String lawyerRatingTagText(Double overallRating) {
+        if (overallRating == null) {
+            return "我给出评价";
+        }
+        if (overallRating >= 4.5D) {
+            return "我给出超赞";
+        }
+        if (overallRating >= 3.5D) {
+            return "我给出好评";
+        }
+        if (overallRating >= 2.5D) {
+            return "我给出中评";
+        }
+        return "我给出差评";
+    }
+
 /*
 /*
     @Override
     @Override
     public Double getAverageScore(Integer businessType, Long businessId) {
     public Double getAverageScore(Integer businessType, Long businessId) {

+ 279 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPersonalizationSettingServiceImpl.java

@@ -0,0 +1,279 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+import shop.alien.mapper.LifeUserPersonalizationSettingMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.LifeUserPersonalizationSettingService;
+
+@Service
+@Slf4j
+public class LifeUserPersonalizationSettingServiceImpl
+        extends ServiceImpl<LifeUserPersonalizationSettingMapper, LifeUserPersonalizationSetting>
+        implements LifeUserPersonalizationSettingService {
+
+    private static final String CACHE_KEY_PREFIX = "alien:store:life:user:personalization:";
+    private static final long CACHE_TTL_SECONDS = 7 * 24 * 3600L;
+
+    @Autowired
+    private BaseRedisService baseRedisService;
+
+    @Override
+    public R<String> add(LifeUserPersonalizationSetting setting) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.add, param={}", setting);
+        R<String> validated = validateAndApplyFontRule(setting, null);
+        if (validated != null) {
+            return validated;
+        }
+        boolean result = this.save(setting);
+        if (result) {
+            invalidatePersonalizationCache(setting.getUserId());
+            return R.success("新增成功");
+        }
+        return R.fail("新增失败");
+    }
+
+    @Override
+    public R<String> deleteById(Integer id) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.deleteById, id={}", id);
+        LifeUserPersonalizationSetting old = id == null ? null : this.getById(id);
+        boolean result = this.removeById(id);
+        if (result) {
+            if (old != null) {
+                invalidatePersonalizationCache(old.getUserId());
+            }
+            return R.success("删除成功");
+        }
+        return R.fail("删除失败");
+    }
+
+    @Override
+    public R<String> update(LifeUserPersonalizationSetting setting) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.update, param={}", setting);
+        LifeUserPersonalizationSetting existing;
+
+        if (setting.getId() != null) {
+            existing = this.getById(setting.getId());
+            if (existing == null) {
+                return R.fail("记录不存在");
+            }
+            if (setting.getUserId() != null && !setting.getUserId().equals(existing.getUserId())) {
+                return R.fail("userId与记录不一致");
+            }
+        } else if (setting.getUserId() != null) {
+            LambdaQueryWrapper<LifeUserPersonalizationSetting> w = new LambdaQueryWrapper<>();
+            w.eq(LifeUserPersonalizationSetting::getUserId, setting.getUserId());
+            existing = this.getOne(w);
+            if (existing == null) {
+                LifeUserPersonalizationSetting created = buildDefaultForUser(setting.getUserId());
+                R<String> validatedInit = validateAndApplyFontRule(created, null);
+                if (validatedInit != null) {
+                    return validatedInit;
+                }
+                try {
+                    this.save(created);
+                    existing = created;
+                } catch (DataIntegrityViolationException e) {
+                    log.warn("LifeUserPersonalizationSetting update 并发初始化 userId={}", setting.getUserId(), e);
+                    existing = this.getOne(w);
+                    if (existing == null) {
+                        return R.fail("记录不存在");
+                    }
+                }
+            }
+            setting.setId(existing.getId());
+        } else {
+            return R.fail("id与userId至少填写一个");
+        }
+
+        R<String> validated = validateAndApplyFontRule(setting, existing);
+        if (validated != null) {
+            return validated;
+        }
+        boolean result = this.updateById(setting);
+        if (result) {
+            Integer uid = setting.getUserId() != null ? setting.getUserId() : existing.getUserId();
+            invalidatePersonalizationCache(uid);
+            return R.success("更新成功");
+        }
+        return R.fail("更新失败");
+    }
+
+    /**
+     * followSystemFont=1 跟随系统:不采纳请求中的 chatFontLevel,沿用库中已有值(新增时为 0)。
+     * followSystemFont=0:chatFontLevel 必须在 0~4(缺省按 0)。
+     */
+    private R<String> validateAndApplyFontRule(LifeUserPersonalizationSetting incoming, LifeUserPersonalizationSetting existing) {
+        int followSys = incoming.getFollowSystemFont() != null
+                ? incoming.getFollowSystemFont()
+                : (existing != null && existing.getFollowSystemFont() != null ? existing.getFollowSystemFont() : 1);
+        if (followSys != 0 && followSys != 1) {
+            return R.fail("followSystemFont 只能为 0 或 1");
+        }
+        if (followSys == 1) {
+            Integer keep = existing != null ? existing.getChatFontLevel() : null;
+            incoming.setChatFontLevel(keep != null ? keep : 0);
+            return null;
+        }
+        int level = incoming.getChatFontLevel() != null
+                ? incoming.getChatFontLevel()
+                : (existing != null && existing.getChatFontLevel() != null ? existing.getChatFontLevel() : 0);
+        if (level < 0 || level > 4) {
+            return R.fail("chatFontLevel 取值 0~4");
+        }
+        incoming.setChatFontLevel(level);
+        return null;
+    }
+
+    @Override
+    public R<LifeUserPersonalizationSetting> getInfoById(Integer id) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.getInfoById, id={}", id);
+        return R.data(this.getById(id));
+    }
+
+    @Override
+    public R<LifeUserPersonalizationSetting> getByUserId(Integer userId) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.getByUserId, userId={}", userId);
+        if (userId == null) {
+            return R.fail("userId不能为空");
+        }
+        LambdaQueryWrapper<LifeUserPersonalizationSetting> w = new LambdaQueryWrapper<>();
+        w.eq(LifeUserPersonalizationSetting::getUserId, userId);
+        LifeUserPersonalizationSetting one = this.getOne(w);
+        if (one != null) {
+            return R.data(one);
+        }
+        LifeUserPersonalizationSetting created = buildDefaultForUser(userId);
+        R<String> validated = validateAndApplyFontRule(created, null);
+        if (validated != null) {
+            return R.fail(validated.getMsg());
+        }
+        try {
+            this.save(created);
+            return R.data(created);
+        } catch (DataIntegrityViolationException e) {
+            log.warn("LifeUserPersonalizationSetting getByUserId 并发插入 userId={}", userId, e);
+            LifeUserPersonalizationSetting again = this.getOne(w);
+            if (again != null) {
+                return R.data(again);
+            }
+            return R.fail("初始化个性化设置失败");
+        }
+    }
+
+    @Override
+    public LifeUserPersonalizationSetting getByUserIdCacheAside(Integer userId) {
+        if (userId == null) {
+            return null;
+        }
+        String key = cacheKey(userId);
+        String cached = baseRedisService.getString(key);
+        if (StringUtils.isNotEmpty(cached)) {
+            try {
+                LifeUserPersonalizationSetting parsed = JSON.parseObject(cached, LifeUserPersonalizationSetting.class);
+                if (parsed != null) {
+                    return parsed;
+                }
+            } catch (Exception e) {
+                log.warn("LifeUserPersonalizationSetting 缓存反序列化失败,删除 key,userId={}", userId, e);
+                baseRedisService.delete(key);
+            }
+        }
+        R<LifeUserPersonalizationSetting> r = getByUserId(userId);
+        if (!R.isSuccess(r) || r.getData() == null) {
+            return null;
+        }
+        LifeUserPersonalizationSetting data = r.getData();
+        try {
+            baseRedisService.setString(key, JSON.toJSONString(data), CACHE_TTL_SECONDS);
+        } catch (Exception e) {
+            log.warn("LifeUserPersonalizationSetting 写入缓存失败,userId={}", userId, e);
+        }
+        return data;
+    }
+
+    @Override
+    public boolean shouldSuppressLikeRelatedNotice(Integer userId) {
+        if (userId == null) {
+            return false;
+        }
+        LifeUserPersonalizationSetting s = getByUserIdCacheAside(userId);
+        if (s == null) {
+            return false;
+        }
+        Integer nrm = s.getNotifyReceiveMessage();
+        Integer nl = s.getNotifyLike();
+        if (nrm != null && nrm == 0) {
+            return true;
+        }
+        return nrm != null && nrm == 1 && nl != null && nl == 0;
+    }
+
+    @Override
+    public boolean shouldSuppressFollowRelatedNotice(Integer userId) {
+        if (userId == null) {
+            return false;
+        }
+        LifeUserPersonalizationSetting s = getByUserIdCacheAside(userId);
+        if (s == null) {
+            return false;
+        }
+        Integer nrm = s.getNotifyReceiveMessage();
+        Integer nf = s.getNotifyFollow();
+        if (nrm != null && nrm == 0) {
+            return true;
+        }
+        return nrm != null && nrm == 1 && nf != null && nf == 0;
+    }
+
+    private static String cacheKey(Integer userId) {
+        return CACHE_KEY_PREFIX + userId;
+    }
+
+    private void invalidatePersonalizationCache(Integer userId) {
+        if (userId == null) {
+            return;
+        }
+        baseRedisService.delete(cacheKey(userId));
+    }
+
+    /** 与表默认值一致,便于无记录时落库 */
+    private static LifeUserPersonalizationSetting buildDefaultForUser(Integer userId) {
+        LifeUserPersonalizationSetting s = new LifeUserPersonalizationSetting();
+        s.setUserId(userId);
+        s.setPersonalizedRecommendation(1);
+        s.setOnlyFollowersComment(1);
+        s.setHideFansList(1);
+        s.setHideFollowList(1);
+        s.setNotifyReceiveMessage(1);
+        s.setNotifyLike(1);
+        s.setNotifyFollow(1);
+        s.setNotifyComment(1);
+        s.setFollowSystemFont(1);
+        s.setChatFontLevel(0);
+        s.setAutoRefresh(1);
+        return s;
+    }
+
+    @Override
+    public R<IPage<LifeUserPersonalizationSetting>> list(Integer pageNum, Integer pageSize, Integer userId) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.list, pageNum={}, pageSize={}, userId={}", pageNum, pageSize, userId);
+        Page<LifeUserPersonalizationSetting> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<LifeUserPersonalizationSetting> w = new LambdaQueryWrapper<>();
+        if (userId != null) {
+            w.eq(LifeUserPersonalizationSetting::getUserId, userId);
+        }
+        w.orderByDesc(LifeUserPersonalizationSetting::getUpdatedTime);
+        return R.data(this.page(page, w));
+    }
+}

+ 194 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPushDeviceServiceImpl.java

@@ -0,0 +1,194 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LawyerUser;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.LifeUserPushDevice;
+import shop.alien.entity.store.PushDeviceOwnerType;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.mapper.LawyerUserMapper;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.LifeUserPushDeviceMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.dto.LifeUserPushBindDto;
+import shop.alien.store.service.LifeUserPushDeviceService;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LifeUserPushDeviceServiceImpl implements LifeUserPushDeviceService {
+
+    private final LifeUserPushDeviceMapper lifeUserPushDeviceMapper;
+    private final LifeUserMapper lifeUserMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final LawyerUserMapper lawyerUserMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> bind(UserLoginInfo login, LifeUserPushBindDto dto) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        PushDeviceOwnerType ownerType = PushDeviceOwnerType.fromJwtUserType(login.getType());
+        if (ownerType == null) {
+            return R.fail("当前登录类型不支持绑定推送设备");
+        }
+        if (dto == null || StringUtils.isBlank(dto.getPushClientId())) {
+            return R.fail("pushClientId 不能为空");
+        }
+        String cid = StringUtils.trim(dto.getPushClientId());
+        if (cid.length() > 128) {
+            return R.fail("pushClientId 过长");
+        }
+
+        R<String> valid = validateOwnerExists(ownerType, login.getUserId());
+        if (valid != null) {
+            return valid;
+        }
+
+        LifeUserPushDevice existing = lifeUserPushDeviceMapper.selectOne(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getPushClientId, cid)
+                        .last("LIMIT 1"));
+
+        Date now = new Date();
+        String platform = StringUtils.trimToNull(dto.getPlatform());
+        String appId = StringUtils.trimToNull(dto.getDcloudAppId());
+        if (platform != null && platform.length() > 32) {
+            platform = platform.substring(0, 32);
+        }
+        if (appId != null && appId.length() > 64) {
+            appId = appId.substring(0, 64);
+        }
+
+        String typeCode = ownerType.getCode();
+        if (existing != null) {
+            existing.setUserId(login.getUserId());
+            existing.setOwnerType(typeCode);
+            existing.setPlatform(platform);
+            existing.setDcloudAppId(appId);
+            existing.setUpdatedTime(now);
+            lifeUserPushDeviceMapper.updateById(existing);
+            log.info("更新推送设备绑定, ownerType={}, userId={}, cid={}", typeCode, login.getUserId(), cid);
+        } else {
+            LifeUserPushDevice row = new LifeUserPushDevice();
+            row.setUserId(login.getUserId());
+            row.setOwnerType(typeCode);
+            row.setPushClientId(cid);
+            row.setPlatform(platform);
+            row.setDcloudAppId(appId);
+            row.setCreatedTime(now);
+            row.setUpdatedTime(now);
+            lifeUserPushDeviceMapper.insert(row);
+            log.info("新增推送设备绑定, ownerType={}, userId={}, cid={}", typeCode, login.getUserId(), cid);
+        }
+        return R.success("绑定成功");
+    }
+
+    /**
+     * @return null 表示校验通过
+     */
+    private R<String> validateOwnerExists(PushDeviceOwnerType ownerType, int businessUserId) {
+        switch (ownerType) {
+            case USER:
+                LifeUser user = lifeUserMapper.selectById(businessUserId);
+                if (user == null) {
+                    return R.fail("用户不存在");
+                }
+                break;
+            case STORE:
+                StoreUser su = storeUserMapper.selectById(businessUserId);
+                if (su == null || (su.getDeleteFlag() != null && su.getDeleteFlag() != 0)) {
+                    return R.fail("门店用户不存在或已删除");
+                }
+                break;
+            case LAWYER:
+                LawyerUser lu = lawyerUserMapper.selectById(businessUserId);
+                if (lu == null || (lu.getDeleteFlag() != null && lu.getDeleteFlag() != 0)) {
+                    return R.fail("律师用户不存在或已删除");
+                }
+                break;
+            default:
+                return R.fail("不支持的归属类型");
+        }
+        return null;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> unbindByCid(UserLoginInfo login, String pushClientId) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        PushDeviceOwnerType ownerType = PushDeviceOwnerType.fromJwtUserType(login.getType());
+        if (ownerType == null) {
+            return R.fail("当前登录类型不支持解绑推送设备");
+        }
+        if (StringUtils.isBlank(pushClientId)) {
+            return R.fail("pushClientId 不能为空");
+        }
+        String cid = StringUtils.trim(pushClientId);
+        int n = lifeUserPushDeviceMapper.delete(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getOwnerType, ownerType.getCode())
+                        .eq(LifeUserPushDevice::getUserId, login.getUserId())
+                        .eq(LifeUserPushDevice::getPushClientId, cid));
+        if (n <= 0) {
+            return R.fail("未找到绑定记录");
+        }
+        return R.success("解绑成功");
+    }
+
+    @Override
+    public R<List<LifeUserPushDevice>> listMine(UserLoginInfo login) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        PushDeviceOwnerType ownerType = PushDeviceOwnerType.fromJwtUserType(login.getType());
+        if (ownerType == null) {
+            return R.fail("当前登录类型不支持查询推送设备");
+        }
+        List<LifeUserPushDevice> list = lifeUserPushDeviceMapper.selectList(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getOwnerType, ownerType.getCode())
+                        .eq(LifeUserPushDevice::getUserId, login.getUserId())
+                        .orderByDesc(LifeUserPushDevice::getUpdatedTime));
+        return R.data(list);
+    }
+
+    @Override
+    public List<String> listPushClientIdsByUserId(int userId, String ownerType) {
+        if (userId <= 0) {
+            return Collections.emptyList();
+        }
+        PushDeviceOwnerType ot = PushDeviceOwnerType.fromCode(ownerType);
+        if (ot == null) {
+            return Collections.emptyList();
+        }
+        List<LifeUserPushDevice> list = lifeUserPushDeviceMapper.selectList(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getOwnerType, ot.getCode())
+                        .eq(LifeUserPushDevice::getUserId, userId)
+                        .select(LifeUserPushDevice::getPushClientId));
+        if (list == null || list.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return list.stream()
+                .map(LifeUserPushDevice::getPushClientId)
+                .filter(StringUtils::isNotBlank)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+}

+ 105 - 0
alien-store/src/main/java/shop/alien/store/service/impl/ShortLinkServiceImpl.java

@@ -0,0 +1,105 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.store.dto.ShortLinkShortenResponse;
+import shop.alien.store.service.ShortLinkService;
+import shop.alien.store.util.Base62Util;
+import shop.alien.store.util.ShortLinkUrlValidator;
+
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ShortLinkServiceImpl implements ShortLinkService {
+
+    private static final String REDIS_KEY_PREFIX = "short:link:";
+    private static final int MAX_GENERATE_RETRY = 12;
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 对外展示的短链前缀(不含末尾斜杠),如 https://m.example.com 或 https://api.example.com/app
+     */
+    @Value("${short-link.public-base-url:}")
+    private String publicBaseUrl;
+
+    /**
+     * 逗号分隔的允许长链域名白名单(小写 host),为空则不校验域名仅拦截内网
+     */
+    @Value("${short-link.allowed-hosts:}")
+    private String allowedHostsCsv;
+
+    /**
+     * 仅允许 https 长链,默认 true
+     */
+    @Value("${short-link.https-only:true}")
+    private boolean httpsOnly;
+
+    /**
+     * 是否允许长链指向 localhost/内网(仅本地调试;生产必须为 false)
+     */
+    @Value("${short-link.allow-local-network-target:false}")
+    private boolean allowLocalNetworkTarget;
+
+    /**
+     * Redis 过期天数,默认 30
+     */
+    @Value("${short-link.ttl-days:30}")
+    private int ttlDays;
+
+    @Override
+    public ShortLinkShortenResponse shorten(String longUrl) {
+        Set<String> allowed = ShortLinkUrlValidator.parseAllowedHostsCsv(allowedHostsCsv);
+        String normalized = ShortLinkUrlValidator.validateAndNormalize(longUrl, allowed, httpsOnly, allowLocalNetworkTarget);
+        long ttlSeconds = Math.max(1L, (long) ttlDays * 24L * 3600L);
+
+        String prefix = normalizePublicBaseUrl(publicBaseUrl);
+        if (!StringUtils.hasText(prefix)) {
+            throw new IllegalArgumentException("请配置 short-link.public-base-url(对外完整域名前缀,无尾斜杠,例如 https://m.example.com)");
+        }
+
+        for (int i = 0; i < MAX_GENERATE_RETRY; i++) {
+            long id = ThreadLocalRandom.current().nextLong(1L, Long.MAX_VALUE);
+            String code = Base62Util.encodePositiveLong(id);
+            String key = REDIS_KEY_PREFIX + code;
+            Boolean created = stringRedisTemplate.opsForValue().setIfAbsent(key, normalized, ttlSeconds, TimeUnit.SECONDS);
+            if (Boolean.TRUE.equals(created)) {
+                String path = "/store/short-link/s/" + code;
+                String shortUrl = prefix + path;
+                return new ShortLinkShortenResponse(shortUrl, code);
+            }
+        }
+        throw new IllegalStateException("短链生成失败,请稍后重试");
+    }
+
+    @Override
+    public String resolveLongUrl(String code) {
+        if (!StringUtils.hasText(code)) {
+            return null;
+        }
+        String trimmed = code.trim();
+        if (trimmed.length() > 64 || !trimmed.matches("[0-9A-Za-z]+")) {
+            return null;
+        }
+        return stringRedisTemplate.opsForValue().get(REDIS_KEY_PREFIX + trimmed);
+    }
+
+    private static String normalizePublicBaseUrl(String raw) {
+        if (!StringUtils.hasText(raw)) {
+            return "";
+        }
+        String s = raw.trim();
+        while (s.endsWith("/")) {
+            s = s.substring(0, s.length() - 1);
+        }
+        return s;
+    }
+}

+ 10 - 10
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingSettingsServiceImpl.java

@@ -487,11 +487,11 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
         }
         }
         
         
         // 比较结束时间:预约时间的结束时间不能晚于门店营业时间的结束时间
         // 比较结束时间:预约时间的结束时间不能晚于门店营业时间的结束时间
-        if (compareTime(bookingEndTime, storeEndTime) > 0) {
-            log.error("预约时间的正常时间结束时间晚于门店营业时间的正常时间,storeId={}, bookingEndTime={}, storeEndTime={}", 
-                    storeId, bookingEndTime, storeEndTime);
-            throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
-        }
+//        if (compareTime(bookingEndTime, storeEndTime) > 0) {
+//            log.error("预约时间的正常时间结束时间晚于门店营业时间的正常时间,storeId={}, bookingEndTime={}, storeEndTime={}",
+//                    storeId, bookingEndTime, storeEndTime);
+//            throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
+//        }
         
         
         log.info("正常营业时间校验通过,storeId={}", storeId);
         log.info("正常营业时间校验通过,storeId={}", storeId);
     }
     }
@@ -592,11 +592,11 @@ public class StoreBookingSettingsServiceImpl extends ServiceImpl<StoreBookingSet
             }
             }
             
             
             // 比较结束时间:预约时间的结束时间不能晚于门店营业时间的结束时间
             // 比较结束时间:预约时间的结束时间不能晚于门店营业时间的结束时间
-            if (compareTime(bookingEndTime, storeEndTime) > 0) {
-                log.error("预约时间的特殊时间结束时间晚于门店营业时间的特殊时间,storeId={}, bookingEndTime={}, storeEndTime={}", 
-                        storeId, bookingEndTime, storeEndTime);
-                throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
-            }
+//            if (compareTime(bookingEndTime, storeEndTime) > 0) {
+//                log.error("预约时间的特殊时间结束时间晚于门店营业时间的特殊时间,storeId={}, bookingEndTime={}, storeEndTime={}",
+//                        storeId, bookingEndTime, storeEndTime);
+//                throw new RuntimeException("预订时间与营业时间冲突,请重新设置");
+//            }
         }
         }
         
         
         log.info("特殊营业时间校验通过,storeId={}", storeId);
         log.info("特殊营业时间校验通过,storeId={}", storeId);

+ 6 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java

@@ -458,6 +458,7 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
         StoreInfo storeInfo = storeInfoMapper.selectById(storeClockIn.getStoreId());
         StoreInfo storeInfo = storeInfoMapper.selectById(storeClockIn.getStoreId());
         // 店铺名称
         // 店铺名称
         storeClockInVo.setStoreName(storeInfo.getStoreName());
         storeClockInVo.setStoreName(storeInfo.getStoreName());
+        storeClockInVo.setStoreBlurb(storeInfo.getStoreBlurb());
         // 店铺头像
         // 店铺头像
         LambdaQueryWrapper<StoreImg> eq = new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 10).eq(StoreImg::getStoreId, storeInfo.getId());
         LambdaQueryWrapper<StoreImg> eq = new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 10).eq(StoreImg::getStoreId, storeInfo.getId());
         StoreImg storeImg = storeImgMapper.selectOne(eq);
         StoreImg storeImg = storeImgMapper.selectOne(eq);
@@ -488,6 +489,11 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
                 storeClockInVo.setIsCollect("1");
                 storeClockInVo.setIsCollect("1");
             }
             }
         }
         }
+
+        if(storeInfo.getBusinessSection() != null){
+            storeClockInVo.setBusinessSection(storeInfo.getBusinessSection().toString());
+        }
+        storeClockInVo.setBusinessSectionName(storeInfo.getBusinessSectionName());
         return storeClockInVo;
         return storeClockInVo;
     }
     }
 }
 }

+ 9 - 1
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -3401,7 +3401,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     }
     }
 
 
     @Override
     @Override
-    public void aiApproveStoreInfo(AiApproveStoreInfo aiApproveStoreInfo) {
+    public AiApproveStoreInfoResultVo aiApproveStoreInfo(AiApproveStoreInfo aiApproveStoreInfo) {
         HttpHeaders aiHeaders = new HttpHeaders();
         HttpHeaders aiHeaders = new HttpHeaders();
         aiHeaders.setContentType(MediaType.APPLICATION_JSON);
         aiHeaders.setContentType(MediaType.APPLICATION_JSON);
         aiHeaders.set("Authorization", "Bearer " + aiAuthTokenUtil.getAccessToken());
         aiHeaders.set("Authorization", "Bearer " + aiAuthTokenUtil.getAccessToken());
@@ -3421,6 +3421,10 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                         log.info("AI门店审核接口返回 data 为空, userId={}", aiApproveStoreInfo.getUserId());
                         log.info("AI门店审核接口返回 data 为空, userId={}", aiApproveStoreInfo.getUserId());
                         throw new RuntimeException("AI门店审核接口返回 data 为空");
                         throw new RuntimeException("AI门店审核接口返回 data 为空");
                     }
                     }
+                    String registrationNo = data.getString("registration_no");
+                    if (StringUtils.isEmpty(registrationNo)) {
+                        registrationNo = data.getString("registrationNo");
+                    }
                     List<StoreInfo> storeInfos = storeInfoMapper.selectList(new LambdaQueryWrapper<StoreInfo>()
                     List<StoreInfo> storeInfos = storeInfoMapper.selectList(new LambdaQueryWrapper<StoreInfo>()
                             .eq(StoreInfo::getCreatedUserId, aiApproveStoreInfo.getUserId()).eq(StoreInfo::getStoreApplicationStatus, 0).eq(StoreInfo::getDeleteFlag, 0));
                             .eq(StoreInfo::getCreatedUserId, aiApproveStoreInfo.getUserId()).eq(StoreInfo::getStoreApplicationStatus, 0).eq(StoreInfo::getDeleteFlag, 0));
                     String status = data.getString("status");
                     String status = data.getString("status");
@@ -3448,6 +3452,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                             log.warn("AI门店审核返回未知状态: status={}, storeId={}", status, storeInfo.getId());
                             log.warn("AI门店审核返回未知状态: status={}, storeId={}", status, storeInfo.getId());
                         }
                         }
                     }
                     }
+                    AiApproveStoreInfoResultVo resultVo = new AiApproveStoreInfoResultVo();
+                    resultVo.setRegistrationNo(registrationNo);
+                    return resultVo;
                 } else {
                 } else {
                     log.error("AI门店审核接口调用失败, userId={}, code={}", aiApproveStoreInfo.getUserId(), jsonObject.getInteger("code"));
                     log.error("AI门店审核接口调用失败, userId={}, code={}", aiApproveStoreInfo.getUserId(), jsonObject.getInteger("code"));
                     throw new RuntimeException("AI门店审核接口调用失败 code:" + jsonObject.getInteger("code"));
                     throw new RuntimeException("AI门店审核接口调用失败 code:" + jsonObject.getInteger("code"));
@@ -3457,6 +3464,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             log.error("调用门店审核接口异常, userId={},{}", aiApproveStoreInfo.getUserId(), e);
             log.error("调用门店审核接口异常, userId={},{}", aiApproveStoreInfo.getUserId(), e);
             throw new RuntimeException("调用门店审核接口异常", e);
             throw new RuntimeException("调用门店审核接口异常", e);
         }
         }
+        return new AiApproveStoreInfoResultVo();
     }
     }
 
 
     @Override
     @Override

+ 11 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StorePaymentConfigServiceImpl.java

@@ -45,6 +45,17 @@ public class StorePaymentConfigServiceImpl extends ServiceImpl<StorePaymentConfi
     }
     }
 
 
     @Override
     @Override
+    public StorePaymentConfig getByAppId(String appId) {
+        if (!StringUtils.hasText(appId)) {
+            return null;
+        }
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StorePaymentConfig::getAppId, appId);
+        wrapper.last("LIMIT 1");
+        return this.getOne(wrapper);
+    }
+
+    @Override
     public IPage<StorePaymentConfig> pageList(Page<StorePaymentConfig> page, Integer storeId, String appId) {
     public IPage<StorePaymentConfig> pageList(Page<StorePaymentConfig> page, Integer storeId, String appId) {
         LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
         LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
         if (storeId != null) {
         if (storeId != null) {

+ 1 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java

@@ -1737,6 +1737,7 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
                     .filter(staff -> staff != null 
                     .filter(staff -> staff != null 
                             && CommonConstant.DELETE_FLAG_UNDELETE.equals(staff.getDeleteFlag())
                             && CommonConstant.DELETE_FLAG_UNDELETE.equals(staff.getDeleteFlag())
                             && storeId.equals(staff.getStoreId())
                             && storeId.equals(staff.getStoreId())
+                            && 0 ==(staff.getOnlineStatus())
                             && "1".equals(staff.getStatus()))
                             && "1".equals(staff.getStatus()))
                     .collect(Collectors.toList());
                     .collect(Collectors.toList());
 
 

+ 56 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/PaymentStrategy.java

@@ -3,6 +3,7 @@ package shop.alien.store.strategy.payment;
 
 
 import shop.alien.entity.result.R;
 import shop.alien.entity.result.R;
 
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Map;
 import java.util.Map;
 
 
 /**
 /**
@@ -23,6 +24,37 @@ public interface PaymentStrategy {
     R createPrePayOrder(String price, String subject) throws Exception;
     R createPrePayOrder(String price, String subject) throws Exception;
 
 
 
 
+    R createPreAliPayOrder(String orderId, String amount, String subject, String smid) throws Exception;
+
+    /**
+     * 创建预支付订单(可选门店维度:微信服务商等策略需传 storeId 以解析子商户号)
+     *
+     * @param storeId 门店主键,非服务商策略可忽略
+     */
+    default R createPrePayOrder(String price, String subject, Integer storeId) throws Exception {
+        return createPrePayOrder(price, subject);
+    }
+
+    /**
+     * 创建预支付订单(扩展参数:与点餐小程序服务商策略对齐,用于绑定业务订单号、回写 store_order)
+     * <p>
+     * 默认实现忽略扩展字段,仅调用 {@link #createPrePayOrder(String, String, Integer)}。
+     * </p>
+     *
+     * @param payer         预留(APP 服务商下单可不传)
+     * @param orderNo       业务订单号,有值时尝试关联 store_order
+     * @param couponId      优惠券
+     * @param payerId       支付用户
+     * @param serviceFee    服务费(与接口层 tablewareFee 等含义对齐时由调用方传入)
+     * @param discountAmount 优惠金额
+     * @param payAmount     实付金额
+     */
+    default R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId,
+                                Integer couponId, Integer payerId, String serviceFee, String discountAmount,
+                                String payAmount) throws Exception {
+        return createPrePayOrder(price, subject, storeId);
+    }
+
     /**
     /**
      * 处理支付通知
      * 处理支付通知
      *
      *
@@ -32,6 +64,14 @@ public interface PaymentStrategy {
      */
      */
     R handleNotify(String notifyData) throws Exception;
     R handleNotify(String notifyData) throws Exception;
 
 
+    /**
+     * 处理支付通知(微信 APIv3 需从请求头读取 Wechatpay-Serial 等参与验签)
+     * <p>默认实现转调 {@link #handleNotify(String)}。</p>
+     */
+    default R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        return handleNotify(notifyData);
+    }
+
      /**
      /**
      * 查询订单状态
      * 查询订单状态
      *
      *
@@ -41,6 +81,13 @@ public interface PaymentStrategy {
      */
      */
     R searchOrderByOutTradeNoPath(String transactionId) throws Exception;
     R searchOrderByOutTradeNoPath(String transactionId) throws Exception;
 
 
+    /**
+     * 按商户订单号查单(可选门店维度:微信服务商需传 storeId 以解析子商户号)
+     */
+    default R searchOrderByOutTradeNoPath(String transactionId, Integer storeId) throws Exception {
+        return searchOrderByOutTradeNoPath(transactionId);
+    }
+
      /**
      /**
      * 处理退款请求
      * 处理退款请求
      *
      *
@@ -59,6 +106,15 @@ public interface PaymentStrategy {
      */
      */
     R searchRefundRecordByOutRefundNo(String outRefundNo ) throws Exception;
     R searchRefundRecordByOutRefundNo(String outRefundNo ) throws Exception;
 
 
+    /**
+     * 创建预支付订单(可选门店维度:支付宝服务商等策略需传 storeId 以解析子商户号)
+     *
+     * @param
+     */
+    default R createPreAliPayOrder(String orderId, String amount, String subject, String smid, Integer storeId) throws Exception {
+        return createPreAliPayOrder(orderId, amount, subject, smid);
+    }
+
 
 
     /**
     /**
      * 获取策略类型字符串
      * 获取策略类型字符串

+ 277 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/AlipayPartnerPaymentStrategyImpl.java

@@ -0,0 +1,277 @@
+package shop.alien.store.strategy.payment.impl;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.*;
+import com.alipay.api.request.AlipayTradeAppPayRequest;
+import com.alipay.api.request.AlipayTradeQueryRequest;
+import com.alipay.api.response.AlipayTradeAppPayResponse;
+import com.alipay.api.response.AlipayTradeQueryResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.map.HashedMap;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.mapper.AlipayZftCreateRecordMapper;
+import shop.alien.store.strategy.payment.PaymentStrategy;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AlipayPartnerPaymentStrategyImpl implements PaymentStrategy {
+
+    private final AlipayZftCreateRecordMapper alipayZftCreateRecordMapper;
+
+
+
+    /**
+     * 生成支付宝直付通二级商户支付预订单
+     */
+    public R createAlipayZftPreOrder(String orderNo, String amount, String subject, String smid) throws AlipayApiException {
+        // 初始化SDK
+        AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+
+
+
+        // 构造请求参数以调用接口
+        AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
+        AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
+
+        // 设置商户订单号
+        // 生成随机订单号
+        orderNo = "alipay_zft_" + System.currentTimeMillis() + new Random().nextInt(999999);
+        model.setOutTradeNo(orderNo);
+
+        // 设置订单总金额
+        // 除以100
+        BigDecimal total = new BigDecimal(amount);
+        model.setTotalAmount(total.toPlainString());
+
+        // 设置订单标题
+        model.setSubject(subject);
+        // 1. 构建二级商户信息
+        SubMerchant subMerchant = new SubMerchant();
+        subMerchant.setMerchantId(smid); // 替换为实际的二级商户编号
+        // 【关键】设置二级商户信息(直付通必传)
+        model.setSubMerchant(subMerchant);
+
+        // 设置产品码
+//        model.setProductCode("QUICK_MSECURITY_PAY");
+
+        // 设置订单包含的商品列表信息
+//        List<GoodsDetail> goodsDetail = new ArrayList<GoodsDetail>();
+//        GoodsDetail goodsDetail0 = new GoodsDetail();
+//        goodsDetail0.setGoodsName("ipad");
+//        goodsDetail0.setAlipayGoodsId("20010001");
+//        goodsDetail0.setQuantity(1L);
+//        goodsDetail0.setPrice("2000");
+//        goodsDetail0.setGoodsId("apple-01");
+//        goodsDetail0.setGoodsCategory("34543238");
+//        goodsDetail0.setCategoriesTree("124868003|126232002|126252004");
+//        goodsDetail0.setShowUrl("http://www.alipay.com/xxx.jpg");
+//        goodsDetail.add(goodsDetail0);
+//        model.setGoodsDetail(goodsDetail);
+
+        // 设置订单绝对超时时间
+//        model.setTimeExpire("2016-12-31 10:05:00");
+
+        // 设置业务扩展参数
+//        ExtendParams extendParams = new ExtendParams();
+//        extendParams.setSysServiceProviderId("2088511833207846");
+//        extendParams.setHbFqSellerPercent("100");
+//        extendParams.setHbFqNum("3");
+//        extendParams.setIndustryRefluxInfo("{\"scene_code\":\"metro_tradeorder\",\"channel\":\"xxxx\",\"scene_data\":{\"asset_name\":\"ALIPAY\"}}");
+//        extendParams.setRoyaltyFreeze("true");
+//        extendParams.setCardType("S0JP0000");
+//        model.setExtendParams(extendParams);
+
+        // 设置公用回传参数
+//        model.setPassbackParams("merchantBizType%3d3C%26merchantBizNo%3d2016010101111");
+
+        // 设置商户的原始订单号
+//        model.setMerchantOrderNo("20161008001");
+
+        // 设置外部指定买家
+//        ExtUserInfo extUserInfo = new ExtUserInfo();
+//        extUserInfo.setCertType("IDENTITY_CARD");
+//        extUserInfo.setCertNo("362334768769238881");
+//        extUserInfo.setMobile("16587658765");
+//        extUserInfo.setName("李明");
+//        extUserInfo.setMinAge("18");
+//        extUserInfo.setNeedCheckInfo("F");
+//        extUserInfo.setIdentityHash("27bfcd1dee4f22c8fe8a2374af9b660419d1361b1c207e9b41a754a113f38fcc");
+//        model.setExtUserInfo(extUserInfo);
+
+        // 设置通知参数选项
+//        List<String> queryOptions = new ArrayList<String>();
+//        queryOptions.add("hyb_amount");
+//        queryOptions.add("enterprise_pay_info");
+//        model.setQueryOptions(queryOptions);
+        SettleInfo settleInfo = new SettleInfo();
+        SettleDetailInfo settleDetailInfo = new SettleDetailInfo();
+        settleDetailInfo.setTransInType("defaultSettle");
+        settleDetailInfo.setAmount(total.toString());
+        List<SettleDetailInfo>  settleDetailInfos = new ArrayList<>();
+        settleDetailInfos.add(settleDetailInfo);
+        settleInfo.setSettleDetailInfos(settleDetailInfos);
+        model.setSettleInfo(settleInfo);
+
+        request.setNotifyUrl("https://frp-off.com:40279/alienStore/alipayPartnerNotify");
+        request.setBizModel(model);
+
+        // 第三方代调用模式下请设置app_auth_token
+        // request.putOtherTextParam("app_auth_token", "<-- 请填写应用授权令牌 -->");
+//        model.setProductCode("QUICK_MSECURITY_PAY");
+        AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
+        String orderStr = response.getBody();
+        System.out.println(orderStr);
+
+        if (response.isSuccess()) {
+            Map<String, Object> analyzeRequest = new HashedMap<>();
+            analyzeRequest.put("orderStr", orderStr);
+            analyzeRequest.put("orderNumber", orderNo);
+            return R.data(analyzeRequest);
+        } else {
+            return R.fail("调用失败");
+            // sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
+            // String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
+            // System.out.println(diagnosisUrl);
+        }
+    }
+
+    private static AlipayConfig getAlipayConfig() {
+        String privateKey  = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCBTOvScNrk0mw+WbR6Qtx2+HrPxB5IU/V2HSPGZUTDNiCiSU9UZcuXlo0UNwMksFYR43btk4BHW9OsZETdr3uJzbEEko4jQPrY49V6V8d3yFUiexcbLKf9W2X+h7EwdPDahHk6wmM73Lm500xzv8JxdGeo/d2uGTh+qD66HcPW+QYj1YbjRV1JyCOiJ2OiwLcb28arNNLOgj6PktQ69FPSWS4hWrM7nl6DLRr6SdOlyfBT47gwPtW0BQ96/2b9O+qxKBPfrcqdRDH323HvrGFDwa8GoWTdchnKwFjmfIFHMvf+eq3tzilFi8H7vQRtzVRrndwrNa0z+1ss07fcWFDRAgMBAAECggEAItgU0OAizPk7vE22SiBMgy8RAX5rXrhpdIwDwQo3Tpf+kV1KKIdKJy6mFCWDDlcKysVOnlVag2BmmZVnzYnls8wfgQjxjuSK9Pno5JBVK51r+9/J6UPOfYMs6Duu700EPw7mEISj81TXJBGiD6tEfgiNisfm/mzDgbZbORKeXQbaTyrtB+GZn6FNSyyHA1vraARMrgfMEGNzQ4AbtfcUxGO+mejdTFs0PxAq6lovHBY3fYYHI1Nx6kf9iPoom/G4UrcMO67W6QU+1tOCZCXjy4bD2y421z/8XD73+WDyYp+Tjy0hTLqVZc7TpYAOximo1vMIUe23EdJJngdlkdpDFQKBgQDFyETL0knwBSakLfAe2BmFb2x++B4YXUnt4dGbCFBnVooxf5i06GVt/CrfkJhYK6hBSowOScIRf8P6BOSQptRZb2/I1ngQm4vcpAZw6EjUTlgOj/J3XJ+ApUNQnRqE28jDrE4m2RHg4BkQo6yA3DizJAluPCtFoCYDm1a7dV7u7wKBgQCnXEH5sD8VSxURv02/gn80g/uZIP/EOU3ycjBEqZdRGkNINwXT+zqrlZIGYb+bxLvU/R2OqKC5vhcyAL3T1A8ORYqPu5KLnAxg7C+rHuVilUWwCEH7POpCk+ETPXCZwcNvLNa5PIqBH/gdV9Y9PBTef6J4rN6V6TDFgosf5by8PwKBgDpVG71Fk1sAGep4RgbC05wgRc6Y3T9wXDqVzJ098YDY7D83E9HfbPLoWbjAS75Nef1vwCkCpgNFPIbD5KmpGp4aGM0SPC0hwzlbAy9PwxMi3CPHXsrHfZ+SnmzrOQQQUoErk40vnm9FiP74VwtWaD6llUZ25ohNeIi9yvHU5x/vAoGAdU2d1JOq85LHtsO+i9+8pyNnAsJ1YqTDtI5CtK2lqKvewswGIrlxOvi//AchVN3ExZmP0QDyfp31BhAs/T8iOl+Vqf7PzVjX+Eszch5aqwlzadmv3ZepnnamCGVE+hAsmkz0R6tebPjqYC7Ds/HbssQFLc4EyVBD5fwE5ZuR+OMCgYAvGHUYpi0dY9uMHXzL721tIopiwUfKCgLAn3yhSH3p7644NxHBqLLaBLVT2q7JAZQUaZUvXlwiyxU1zvo0xmvcbnB/Vd2qp8KbEUkvHyIYVJkM6Fn+9xBosorcrHv+7B2V1XR9WQcXvppxbN/8farWGuAA0anBD+UGrxd8B0/hHg==";
+        String alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnxevClLpYc6c8A9lwmnftPO2BeU+X6aZ/+b/n1Cvq096VJKiHmqcsRgRbrnSlmPDHRrQpDti4en2Ys0L2lx7CObIr/2xP/jJVwjIO1iWHUj/w/NAbjv7dLW/FFY4SeNp8rU+hlgGgviyUxzonfNfr3v+o8grFqQq27/hiZJAofsQRMQu1dEQqoKdJj7eQLkTstiK5miJMyQ+Y3tLztrEUMBz/zRgaCEfGqmFmRZ2diy2X+1dGaX6H4+0QJ2u50eY2QTBkNuvREGbAn6/lttAgvg/+CywPYKGeC4xOfnl5wP8iA1QXYbXrVJRkZjU097nyOmSNhLy9KvJH2BNpojS1QIDAQAB";
+        AlipayConfig alipayConfig = new AlipayConfig();
+        alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
+        alipayConfig.setAppId("2021005196608960");
+        alipayConfig.setPrivateKey(privateKey);
+        alipayConfig.setFormat("json");
+        alipayConfig.setAlipayPublicKey(alipayPublicKey);
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+        return alipayConfig;
+    }
+
+    @Override
+    public R createPreAliPayOrder(String orderNo, String price, String subject, String smid) throws Exception {
+        return createAlipayZftPreOrder(orderNo, price, subject, smid);
+    }
+
+    @Override
+    public R createPrePayOrder(String price, String subject) throws Exception {
+        return null;
+    }
+
+    @Override
+    public R handleNotify(String notifyData) throws Exception {
+        log.info("处理支付宝支付通知,通知数据:{}", notifyData);
+        // 通过订单ID查询记录中对应数据,取得storeId
+        // 通过storeId查询storeInfo对应数据,
+        //更新smid
+        return null;
+    }
+
+    @Override
+    public R searchOrderByOutTradeNoPath(String transactionId) throws Exception {
+        try {
+            AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
+            AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
+            AlipayTradeQueryModel model = new AlipayTradeQueryModel();
+            model.setOutTradeNo(transactionId);
+            List<String> queryOptions = new ArrayList<>();
+            queryOptions.add("trade_settle_info");
+            model.setQueryOptions(queryOptions);
+            request.setBizModel(model);
+
+            AlipayTradeQueryResponse response = alipayClient.execute(request);
+            log.debug("支付宝订单查询 rawBody 长度={}", response.getBody() != null ? response.getBody().length() : 0);
+
+            Map<String, Object> data = buildAlipayTradeQueryData(response);
+            if (response.isSuccess()) {
+                return R.success("订单状态:" + data.get("tradeStatus"));
+            }
+            String sub = response.getSubMsg() != null ? response.getSubMsg() : "";
+            return R.fail("订单查询失败:" + response.getMsg() + "(" + sub + ")");
+        } catch (AlipayApiException e) {
+            log.error("支付宝订单查询异常, transactionId={}", transactionId, e);
+            return R.fail("查询异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 将 {@link AlipayTradeQueryResponse} 转为与前端约定一致的 Map(与支付宝网关返回字段对齐,便于序列化为示例 JSON 结构)。
+     */
+    private static Map<String, Object> buildAlipayTradeQueryData(AlipayTradeQueryResponse response) {
+        Map<String, Object> data = new LinkedHashMap<>();
+        data.put("code", response.getCode());
+        data.put("msg", response.getMsg());
+        data.put("body", response.getBody());
+        if (response.getParams() != null && !response.getParams().isEmpty()) {
+            data.put("params", response.getParams());
+        }
+        data.put("buyerLogonId", response.getBuyerLogonId());
+        data.put("buyerPayAmount", response.getBuyerPayAmount());
+        data.put("buyerUserId", response.getBuyerUserId());
+
+        List<TradeFundBill> fundBillList = response.getFundBillList();
+        if (fundBillList != null && !fundBillList.isEmpty()) {
+            List<Map<String, String>> bills = new ArrayList<>();
+            for (TradeFundBill fb : fundBillList) {
+                if (fb == null) {
+                    continue;
+                }
+                Map<String, String> row = new LinkedHashMap<>();
+                row.put("amount", fb.getAmount());
+                row.put("fundChannel", fb.getFundChannel());
+                bills.add(row);
+            }
+            data.put("fundBillList", bills);
+        }
+
+        data.put("invoiceAmount", response.getInvoiceAmount());
+        data.put("outTradeNo", response.getOutTradeNo());
+        data.put("pointAmount", response.getPointAmount());
+        data.put("receiptAmount", response.getReceiptAmount());
+        data.put("sendPayDate", response.getSendPayDate());
+        data.put("totalAmount", response.getTotalAmount());
+        data.put("tradeNo", response.getTradeNo());
+
+        TradeSettleInfo tradeSettleInfo = response.getTradeSettleInfo();
+        if (tradeSettleInfo != null) {
+            Map<String, String> settle = new LinkedHashMap<>();
+            settle.put("tradeUnsettledAmount", tradeSettleInfo.getTradeUnsettledAmount());
+            data.put("tradeSettleInfo", settle);
+        }
+
+        data.put("tradeStatus", response.getTradeStatus());
+        data.put("errorCode", response.getCode());
+        data.put("success", response.isSuccess());
+        return data;
+    }
+
+    @Override
+    public String handleRefund(Map<String, String> params) throws Exception {
+        return "";
+    }
+
+    @Override
+    public R searchRefundRecordByOutRefundNo(String outRefundNo) throws Exception {
+        return null;
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.ALIPAY_PARTNER.getType();
+    }
+}

+ 5 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/AlipayPaymentStrategyImpl.java

@@ -183,6 +183,11 @@ public class AlipayPaymentStrategyImpl implements PaymentStrategy {
     }
     }
 
 
     @Override
     @Override
+    public R createPreAliPayOrder(String orderId, String amount, String subject, String smid) throws Exception {
+        return null;
+    }
+
+    @Override
     public R<Object> handleNotify(String notifyData) throws Exception {
     public R<Object> handleNotify(String notifyData) throws Exception {
         log.info("处理支付宝支付通知,通知数据:{}", notifyData);
         log.info("处理支付宝支付通知,通知数据:{}", notifyData);
         // TODO: 实现支付通知处理逻辑
         // TODO: 实现支付通知处理逻辑

+ 740 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPartnerPaymentStrategyImpl.java

@@ -0,0 +1,740 @@
+package shop.alien.store.strategy.payment.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.google.gson.annotations.SerializedName;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreOrder;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.LifeDiscountCouponUser;
+import shop.alien.entity.store.RefundRecord;
+import shop.alien.mapper.LifeDiscountCouponUserMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreOrderMapper;
+import shop.alien.store.feign.DiningServiceFeign;
+import shop.alien.store.service.RefundRecordService;
+import shop.alien.store.service.StorePaymentConfigService;
+import shop.alien.store.strategy.payment.PaymentStrategy;
+import shop.alien.store.util.WXPayUtility;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.DiscountCouponEnum;
+import shop.alien.util.common.constant.PaymentEnum;
+import shop.alien.util.system.OSUtil;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * 微信支付 — 服务商模式(特约商户 / partner)
+ * <p>
+ * 使用微信支付 APIv3 服务商接口:下单、查单、退款均走 sp_mchid + sub_mchid。
+ * </p>
+ * <p>
+ * <b>【需您提供并写入配置】</b>(前缀 {@code payment.wechatPartnerPay},见各字段注释)
+ * </p>
+ * <ul>
+ *   <li>服务商商户号 {@code sp_mchid}、子商户号 {@code sub_mchid}</li>
+ *   <li>服务商 AppID {@code sp_appid};若子商户有独立移动应用 AppID 则配置 {@code sub_appid}</li>
+ *   <li>API 证书私钥、证书序列号、平台公钥、APIv3 密钥、回调 URL 等(与直连类似,但商户号为服务商)</li>
+ *   <li>各接口路径若微信有变更,以官方文档为准,可通过配置覆盖默认值</li>
+ * </ul>
+ *
+ * @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012062547">服务商模式产品文档</a>
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class WeChatPartnerPaymentStrategyImpl implements PaymentStrategy {
+
+    private final RefundRecordService refundRecordService;
+    /** 按门店读取 store_info.wechat_sub_mchid(特约商户号),与前端传入的 storeId 对应 */
+    private final StoreInfoMapper storeInfoMapper;
+    /** 预下单时按业务订单号更新 store_order */
+    private final StoreOrderMapper storeOrderMapper;
+    /** 按门店解析子商户 AppID(sub_appid),与小程序策略一致 */
+    private final StorePaymentConfigService storePaymentConfigService;
+    /** 支付成功回调:核销优惠券(与小程序服务商策略一致) */
+    private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
+    /** 支付成功回调:重置餐桌/购物车逻辑在 alien-dining,通过 Feign 调用 */
+    private final DiningServiceFeign diningServiceFeign;
+    /** 复用直连实现中的退款单模型、HTTP 工具与 RefundRecord 构建逻辑(构建后覆盖 payType 为服务商类型) */
+    private final WeChatPaymentStrategyImpl weChatPaymentStrategy;
+
+    // ———— 【需您配置】基础域名(一般保持默认) ————
+    @Value("${payment.wechatPartnerPay.host:https://api.mch.weixin.qq.com}")
+    private String wechatPayApiHost;
+
+    /** 【需您配置】服务商 APP 下单路径,默认服务商 APP 支付 */
+    @Value("${payment.wechatPartnerPay.prePayPath:/v3/pay/partner/transactions/app}")
+    private String prePayPath;
+
+    /** 【需您配置】按商户订单号查单(路径模板中的 {out_trade_no} 与直连一致) */
+    @Value("${payment.wechatPartnerPay.searchOrderByOutTradeNoPath:/v3/pay/partner/transactions/out-trade-no/{out_trade_no}}")
+    private String searchOrderByOutTradeNoPath;
+
+    /** 【需您配置】退款申请路径(与直连相同) */
+    @Value("${payment.wechatPartnerPay.refundPath:/v3/refund/domestic/refunds}")
+    private String refundPath;
+
+    // ———— 【需您配置】服务商与子商户标识 ————
+    /** 【需您配置】服务商 AppID(sp_appid) */
+    @Value("${payment.wechatPartnerPay.business.spAppId}")
+    private String spAppId;
+
+    /** 【需您配置】服务商商户号(sp_mchid),请求 Authorization 与查单参数均使用 */
+    @Value("${payment.wechatPartnerPay.business.spMchId}")
+    private String spMchId;
+
+    /**
+     * 子商户号 sub_mchid 从 {@link StoreInfo#getWechatSubMchid()}(表 store_info.wechat_sub_mchid)按前端传入的 storeId 查询,不再使用全局配置。
+     */
+
+    /**
+     * 【可选配置】子商户 AppID(sub_appid)。
+     * 若与服务商共用或仅 sp_appid 调起支付,可留空;调起客户端支付包签名时优先用此值。
+     */
+    @Value("${payment.wechatPartnerPay.business.subAppId:}")
+    private String subAppId;
+
+    // ———— 【需您配置】证书与密钥(与直连结构一致,一般为服务商商户证书) ————
+    @Value("${payment.wechatPartnerPay.business.win.privateKeyPath}")
+    private String privateWinKeyPath;
+
+    @Value("${payment.wechatPartnerPay.business.linux.privateKeyPath}")
+    private String privateLinuxKeyPath;
+
+    @Value("${payment.wechatPartnerPay.business.win.wechatPayPublicKeyFilePath}")
+    private String wechatWinPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPartnerPay.business.linux.wechatPayPublicKeyFilePath}")
+    private String wechatLinuxPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPartnerPay.business.merchantSerialNumber}")
+    private String merchantSerialNumber;
+
+    @Value("${payment.wechatPartnerPay.business.wechatPayPublicKeyId}")
+    private String wechatPayPublicKeyId;
+
+    /** 【需您配置】支付结果通知 URL */
+    @Value("${payment.wechatPartnerPay.business.prePayNotifyUrl}")
+    private String prePayNotifyUrl;
+
+    /** 【需您配置】退款结果通知 URL */
+    @Value("${payment.wechatPartnerPay.business.refundNotifyUrl}")
+    private String refundNotifyUrl;
+
+    /** 支付结果通知 resource 解密(与小程序服务商策略一致,回调验签通过后解密) */
+    @Value("${payment.wechatPartnerPay.business.apiV3Key:}")
+    private String apiV3Key;
+
+    private PrivateKey privateKey;
+    private PublicKey wechatPayPublicKey;
+
+    private static final String POST_METHOD = "POST";
+    private static final String GET_METHOD = "GET";
+
+    @PostConstruct
+    public void loadPartnerCertificates() {
+        String privateKeyPath;
+        String wechatPayPublicKeyFilePath;
+        if ("windows".equals(OSUtil.getOsName())) {
+            privateKeyPath = privateWinKeyPath;
+            wechatPayPublicKeyFilePath = wechatWinPayPublicKeyFilePath;
+        } else {
+            privateKeyPath = privateLinuxKeyPath;
+            wechatPayPublicKeyFilePath = wechatLinuxPayPublicKeyFilePath;
+        }
+        log.info("[WeChatPartner] 加载服务商商户证书私钥与平台公钥,os={}, keyPath={}", OSUtil.getOsName(), privateKeyPath);
+        this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
+        this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
+    }
+
+    @Override
+    public R createPrePayOrder(String price, String subject) throws Exception {
+        return createPrePayOrder(price, subject, null, null, null, null, null, null, null, null);
+    }
+
+    @Override
+    public R createPreAliPayOrder(String orderId, String amount, String subject, String smid) throws Exception {
+        return null;
+    }
+
+    @Override
+    public R createPrePayOrder(String price, String subject, Integer storeId) throws Exception {
+        return createPrePayOrder(price, subject, null, null, storeId, null, null, null, null, null);
+    }
+
+    /**
+     * 与 {}
+     * 参数对齐:有 orderNo 时刷新 {@link StoreOrder} 的 pay_trade_no 及费用字段,商户单号与微信 out_trade_no 一致。
+     */
+    @Override
+    public R createPrePayOrder(String price, String subject, String payer, String orderNo, Integer storeId,
+                               Integer couponId, Integer payerId, String serviceFee, String discountAmount,
+                               String payAmount) throws Exception {
+        String subMchid = resolveSubMchidFromStore(storeId);
+        log.info("[WeChatPartner] 创建预支付订单,price={}, subject={}, spMchId={}, storeId={}, orderNo={}, subMchid={}",
+                price, subject, spMchId, storeId, orderNo, subMchid != null ? subMchid : "(未解析)");
+        // 与小程序策略入参一致;服务商 APP 下单无需 sp_openid,仅记录便于与前端联调对照
+        log.debug("[WeChatPartner] 扩展参数 payer(APP 不使用)={}, couponId={}, payerId={}, serviceFee={}, discountAmount={}, payAmount={}",
+                payer, couponId, payerId, serviceFee, discountAmount, payAmount);
+        if (subMchid == null) {
+            return R.fail("请传入门店 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid");
+        }
+        if (price == null || price.trim().isEmpty()) {
+            return R.fail("价格不能为空");
+        }
+        if (subject == null || subject.trim().isEmpty()) {
+            return R.fail("订单描述不能为空");
+        }
+        try {
+            BigDecimal amount = new BigDecimal(price);
+            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
+                return R.fail("价格必须大于0");
+            }
+        } catch (NumberFormatException e) {
+            return R.fail("价格格式不正确");
+        }
+
+        String wechatOutTradeNo;
+        if (StringUtils.isNotBlank(orderNo)) {
+            wechatOutTradeNo = orderNo.trim();
+            LambdaQueryWrapper<StoreOrder> ow = new LambdaQueryWrapper<>();
+            ow.eq(StoreOrder::getOrderNo, wechatOutTradeNo);
+            ow.eq(StoreOrder::getDeleteFlag, 0);
+            StoreOrder storeOrder = storeOrderMapper.selectOne(ow);
+            log.info("[WeChatPartner] createPrePayOrder orderNo={}, storeOrderFound={}, subMchid={}", orderNo, storeOrder != null, subMchid);
+
+            if (storeOrder != null) {
+                if (storeOrder.getPayStatus() != null && storeOrder.getPayStatus() == 1) {
+                    return R.fail("订单已支付");
+                }
+                if (storeOrder.getPayTradeNo() != null) {
+                    try {
+                        WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse wxOrder =
+                                partnerSearchOrderRun(storeOrder.getPayTradeNo(), subMchid);
+                        if (wxOrder != null && "SUCCESS".equals(wxOrder.tradeState)) {
+                            return R.fail("该支付单已在微信侧支付成功,请勿重复发起支付");
+                        }
+                    } catch (WXPayUtility.ApiException e) {
+                        if (e.getStatusCode() != 404 && !"ORDER_NOT_EXIST".equals(e.getErrorCode())) {
+                            log.error("[WeChatPartner] 预支付前查单失败 payTradeNo={}, code={}, msg={}",
+                                    storeOrder.getPayTradeNo(), e.getErrorCode(), e.getErrorMessage());
+                            return R.fail("查询微信支付订单失败:" + (e.getErrorMessage() != null ? e.getErrorMessage() : e.getMessage()));
+                        }
+                    }
+                }
+                String newPayTradeNo = "WX" + storeOrder.getId() + "_" + System.currentTimeMillis();
+                storeOrder.setPayTradeNo(newPayTradeNo);
+                wechatOutTradeNo = newPayTradeNo;
+                log.info("[WeChatPartner] 换新商户单号 orderNo={}, payTradeNo={}", orderNo, newPayTradeNo);
+                storeOrder.setCouponId(couponId);
+                storeOrder.setPayUserId(payerId);
+//                storeOrder.setServiceFee(parseAmountOrZero(serviceFee));
+                storeOrder.setDiscountAmount(parseAmountOrZero(discountAmount));
+                storeOrder.setPayAmount(parseAmountOrZero(payAmount));
+                if (storeOrderMapper.updateById(storeOrder) <= 0) {
+                    log.error("[WeChatPartner] 更新订单失败 orderNo={}", orderNo);
+                    return R.fail("更新订单失败");
+                }
+            }
+        } else {
+            wechatOutTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        }
+
+        PartnerAppPrepayRequest request = new PartnerAppPrepayRequest();
+        request.spAppid = spAppId;
+        request.spMchid = spMchId;
+        request.subMchid = subMchid;
+        request.description = subject;
+        request.outTradeNo = wechatOutTradeNo;
+        request.notifyUrl = prePayNotifyUrl;
+        request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
+        request.amount.total = new BigDecimal(price).multiply(new BigDecimal(100)).longValue();
+        request.amount.currency = "CNY";
+
+        try {
+            WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = partnerPrePayOrderRun(request);
+            log.info("[WeChatPartner] 预下单成功 prepayId={}, outTradeNo={}", response.prepayId, request.outTradeNo);
+
+            Map<String, String> result = new HashMap<>();
+            result.put("prepayId", response.prepayId);
+            result.put("appId", spAppId);
+            result.put("spAppId", spAppId);
+            result.put("spMchId", spMchId);
+            result.put("subMchId", subMchid);
+            result.put("orderNo", request.outTradeNo);
+
+            long timestamp = System.currentTimeMillis() / 1000;
+            String nonce = WXPayUtility.createNonce(32);
+            String prepayId = response.prepayId;
+            String message = String.format("%s\n%s\n%s\n", timestamp, nonce, prepayId);
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initSign(privateKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            result.put("sign", Base64.getEncoder().encodeToString(sign.sign()));
+            result.put("timestamp", String.valueOf(timestamp));
+            result.put("nonce", nonce);
+            return R.data(result);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("[WeChatPartner] 预下单失败 code={}, body={}", e.getErrorCode(), e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * 子商户 AppID:门店支付配置优先,否则使用全局 {@code payment.wechatPartnerPay.business.subAppId}。
+     */
+    private String resolveSubAppId(StorePaymentConfig config) {
+        if (config != null) {
+            if (org.springframework.util.StringUtils.hasText(config.getWechatMiniAppId())) {
+                return config.getWechatMiniAppId().trim();
+            }
+            if (org.springframework.util.StringUtils.hasText(config.getWechatAppId())) {
+                return config.getWechatAppId().trim();
+            }
+        }
+        if (StringUtils.isNotBlank(subAppId)) {
+            return subAppId.trim();
+        }
+        return null;
+    }
+
+    /** 解析金额字符串,空或非法时记日志并返回 0,避免更新订单时 NPE */
+    private static BigDecimal parseAmountOrZero(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            return BigDecimal.ZERO;
+        }
+        try {
+            return new BigDecimal(raw.trim());
+        } catch (NumberFormatException e) {
+            return BigDecimal.ZERO;
+        }
+    }
+
+    @Override
+    public R handleNotify(String notifyData) throws Exception {
+        log.warn("[WeChatPartner回调] 缺少 HttpServletRequest,无法进行 APIv3 验签,请使用 POST /payment/weChatPartnerNotify");
+        return R.fail("请使用微信支付回调专用接口(需携带 Wechatpay-* 请求头)");
+    }
+
+    /**
+     * 与 {} 对齐:
+     * 验签、解密 resource,异步更新订单/优惠券,并通过 Feign 调用 dining 重置餐桌。
+     */
+    @Override
+    public R handleNotify(String notifyData, HttpServletRequest request) throws Exception {
+        log.info("[WeChatPartner回调] 进入 handleNotify, len={}", notifyData != null ? notifyData.length() : 0);
+        if (request == null) {
+            return R.fail("请求上下文缺失");
+        }
+        String serial = request.getHeader("Wechatpay-Serial");
+        String signature = request.getHeader("Wechatpay-Signature");
+        String timestamp = request.getHeader("Wechatpay-Timestamp");
+        String nonce = request.getHeader("Wechatpay-Nonce");
+
+        if (serial == null || signature == null || timestamp == null || nonce == null) {
+            log.warn("[WeChatPartner回调] 验签参数缺失 serial={}, signature={}, timestamp={}, nonce={}",
+                    serial, signature != null, timestamp, nonce);
+            return R.fail("验签参数缺失");
+        }
+
+        if (signature.startsWith("WECHATPAY/SIGNTEST/")) {
+            log.info("[WeChatPartner回调] 签名探测请求,直接成功");
+            return R.success("OK");
+        }
+
+        if (!StringUtils.equals(serial, wechatPayPublicKeyId)) {
+            log.warn("[WeChatPartner回调] Wechatpay-Serial 与配置不符 serial={}, expected={}", serial, wechatPayPublicKeyId);
+            return R.fail("公钥序列号不匹配");
+        }
+        if (wechatPayPublicKey == null) {
+            log.error("[WeChatPartner回调] 平台公钥未加载");
+            return R.fail("平台公钥未就绪");
+        }
+
+        Headers okHeaders = buildOkHttpHeaders(request);
+
+        if (!org.springframework.util.StringUtils.hasText(apiV3Key)) {
+            log.error("[WeChatPartner回调] 未配置 payment.wechatPartnerPay.business.apiV3Key,无法解密");
+            return R.fail("APIv3 密钥未配置");
+        }
+
+        try {
+            WXPayUtility.Notification parsed = WXPayUtility.parseNotification(apiV3Key, wechatPayPublicKeyId,
+                    wechatPayPublicKey, okHeaders, notifyData);
+            String plaintext = parsed.getPlaintext();
+            if (plaintext == null || plaintext.isEmpty()) {
+                log.warn("[WeChatPartner回调] 解密后业务数据为空");
+                return R.fail("解密结果为空");
+            }
+            final String plainCopy = plaintext;
+            CompletableFuture.runAsync(() -> processPartnerNotifyBusiness(plainCopy));
+            return R.success("OK");
+        } catch (IllegalArgumentException e) {
+            log.error("[WeChatPartner回调] 验签或解密失败: {}", e.getMessage());
+            return R.fail(e.getMessage() != null ? e.getMessage() : "处理失败");
+        } catch (Exception e) {
+            log.error("[WeChatPartner回调] 处理异常", e);
+            return R.fail(e.getMessage() != null ? e.getMessage() : "处理异常");
+        }
+    }
+
+    private static Headers buildOkHttpHeaders(HttpServletRequest request) {
+        Headers.Builder b = new Headers.Builder();
+        Enumeration<String> names = request.getHeaderNames();
+        while (names != null && names.hasMoreElements()) {
+            String name = names.nextElement();
+            Enumeration<String> values = request.getHeaders(name);
+            while (values != null && values.hasMoreElements()) {
+                b.add(name, values.nextElement());
+            }
+        }
+        return b.build();
+    }
+
+    /**
+     * 异步:解析支付成功报文,更新 store_order、优惠券,并通知 dining 重置餐桌(与小程序服务商回调业务一致)
+     */
+    private void processPartnerNotifyBusiness(String plaintext) {
+        try {
+            JSONObject jsonObject = JSONObject.parseObject(plaintext);
+            String tradeState = jsonObject.getString("trade_state");
+            if (!"SUCCESS".equals(tradeState)) {
+                log.info("[WeChatPartner回调] trade_state 非 SUCCESS: {}", tradeState);
+                return;
+            }
+            String outTradeNo = jsonObject.getString("out_trade_no");
+            if (outTradeNo == null || outTradeNo.isEmpty()) {
+                log.warn("[WeChatPartner回调] 缺少 out_trade_no");
+                return;
+            }
+
+            LambdaQueryWrapper<StoreOrder> byOrderNo = new LambdaQueryWrapper<>();
+            byOrderNo.eq(StoreOrder::getOrderNo, outTradeNo).eq(StoreOrder::getDeleteFlag, 0);
+            StoreOrder storeOrder = storeOrderMapper.selectOne(byOrderNo);
+            if (storeOrder == null) {
+                LambdaQueryWrapper<StoreOrder> byPayTrade = new LambdaQueryWrapper<>();
+                byPayTrade.eq(StoreOrder::getPayTradeNo, outTradeNo).eq(StoreOrder::getDeleteFlag, 0);
+                storeOrder = storeOrderMapper.selectOne(byPayTrade);
+            }
+
+            if (storeOrder != null && !Integer.valueOf(1).equals(storeOrder.getPayStatus())) {
+                storeOrder.setPayStatus(1);
+                storeOrder.setOrderStatus(1);
+                storeOrder.setPayType(1);
+                storeOrder.setPayTime(new Date());
+                int rows = storeOrderMapper.updateById(storeOrder);
+                if (rows > 0) {
+                    log.info("[WeChatPartner回调] 更新订单支付成功 outTradeNo={}", outTradeNo);
+                    if (storeOrder.getCouponId() != null && storeOrder.getPayUserId() != null) {
+                        LambdaQueryWrapper<LifeDiscountCouponUser> cw = new LambdaQueryWrapper<>();
+                        cw.eq(LifeDiscountCouponUser::getUserId, storeOrder.getPayUserId());
+                        cw.eq(LifeDiscountCouponUser::getCouponId, storeOrder.getCouponId());
+                        cw.eq(LifeDiscountCouponUser::getStatus, Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
+                        cw.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+                        cw.orderByDesc(LifeDiscountCouponUser::getCreatedTime);
+                        cw.last("LIMIT 1");
+                        LifeDiscountCouponUser couponUser = lifeDiscountCouponUserMapper.selectOne(cw);
+                        if (couponUser != null) {
+                            couponUser.setStatus(Integer.parseInt(DiscountCouponEnum.HAVE_BEEN_USED.getValue()));
+                            couponUser.setUseTime(new Date());
+                            lifeDiscountCouponUserMapper.updateById(couponUser);
+                            log.info("[WeChatPartner回调] 优惠券已使用 id={}", couponUser.getId());
+                        }
+                    }
+                    try {
+                        R<Void> feignRet =
+                                diningServiceFeign.resetTableAfterPaymentInternal(storeOrder.getTableId());
+                        if (!R.isSuccess(feignRet)) {
+                            log.error("[WeChatPartner回调] Feign 重置餐桌失败 tableId={}, msg={}",
+                                    storeOrder.getTableId(), feignRet != null ? feignRet.getMsg() : "null");
+                        } else {
+                            log.info("[WeChatPartner回调] 已请求 dining 重置餐桌 tableId={}", storeOrder.getTableId());
+                        }
+                    } catch (Exception e) {
+                        log.error("[WeChatPartner回调] Feign 重置餐桌异常 tableId={}", storeOrder.getTableId(), e);
+                    }
+                } else {
+                    log.warn("[WeChatPartner回调] 更新订单影响行数为 0, orderId={}", storeOrder.getId());
+                }
+            }
+        } catch (Exception e) {
+            log.error("[WeChatPartner回调] 异步处理异常", e);
+        }
+    }
+
+    @Override
+    public R searchOrderByOutTradeNoPath(String outTradeNo) throws Exception {
+        return searchOrderByOutTradeNoPath(outTradeNo, null);
+    }
+
+    @Override
+    public R searchOrderByOutTradeNoPath(String outTradeNo, Integer storeId) throws Exception {
+        String subMchid = resolveSubMchidFromStore(storeId);
+        log.info("[WeChatPartner] 查单 outTradeNo={}, storeId={}, subMchid={}", outTradeNo, storeId,
+                subMchid != null ? subMchid : "(未解析)");
+        if (subMchid == null) {
+            return R.fail("请传入门店 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid");
+        }
+        try {
+            WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = partnerSearchOrderRun(outTradeNo, subMchid);
+            return R.data(response);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("[WeChatPartner] 查单失败 code={}, msg={}", e.getErrorCode(), e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @Override
+    public String handleRefund(Map<String, String> params) throws Exception {
+        Integer refundStoreId = parseStoreIdFromParams(params);
+        String subMchid = resolveSubMchidFromStore(refundStoreId);
+        log.info("[WeChatPartner] 退款请求 storeId={}, subMchid={}, outTradeNo={}",
+                refundStoreId, subMchid != null ? subMchid : "(未解析)", params.get("outTradeNo"));
+        if (subMchid == null) {
+            return "退款失败:请在参数中传入 storeId,且门店需在 store_info 中维护微信特约商户号 wechat_sub_mchid";
+        }
+        PartnerRefundCreateRequest request = new PartnerRefundCreateRequest();
+        request.subMchid = subMchid;
+        request.outTradeNo = params.get("outTradeNo");
+        request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        request.reason = params.get("reason");
+        request.notifyUrl = refundNotifyUrl;
+        request.amount = new WeChatPaymentStrategyImpl.AmountReq();
+        request.amount.refund = new BigDecimal(params.get("refundAmount")).longValue();
+        request.amount.total = new BigDecimal(params.get("totalAmount")).longValue();
+        request.amount.currency = "CNY";
+
+        log.info("[WeChatPartner] 退款 outTradeNo={}, outRefundNo={}, refundFen={}, totalFen={}",
+                request.outTradeNo, request.outRefundNo, request.amount.refund, request.amount.total);
+
+        try {
+            WeChatPaymentStrategyImpl.Refund response = partnerRefundRun(request);
+            String status = response.status != null ? response.status.name() : "UNKNOWN";
+            if ("SUCCESS".equals(status) || "PROCESSING".equals(status)) {
+                saveRefundRecord(() -> patchPayType(weChatPaymentStrategy.buildRefundRecordFromWeChatResponse(response,
+                        request, params)));
+                return "调用成功";
+            }
+            log.error("[WeChatPartner] 退款未成功 status={}, outTradeNo={}", status, request.outTradeNo);
+            saveRefundRecord(() -> patchPayType(weChatPaymentStrategy.buildRefundRecordFromWeChatError(response, request, params, status)));
+            return "退款失败";
+        } catch (Exception e) {
+            log.error("[WeChatPartner] 退款异常 outTradeNo={}", request.outTradeNo, e);
+            saveRefundRecord(() -> patchPayType(weChatPaymentStrategy.buildRefundRecordFromWeChatException(request, params, e)));
+            return "退款处理异常:" + e.getMessage();
+        }
+    }
+
+    private RefundRecord patchPayType(RefundRecord record) {
+        if (record != null) {
+            record.setPayType(PaymentEnum.WECHAT_PAY_PARTNER.getType());
+        }
+        return record;
+    }
+
+    private void saveRefundRecord(java.util.function.Supplier<RefundRecord> supplier) {
+        try {
+            RefundRecord refundRecord = supplier.get();
+            if (refundRecord == null) {
+                return;
+            }
+            long count = refundRecordService.lambdaQuery()
+                    .eq(RefundRecord::getOutRefundNo, refundRecord.getOutRefundNo())
+                    .count();
+            if (count == 0) {
+                refundRecordService.save(refundRecord);
+                log.info("[WeChatPartner] 已写入 RefundRecord,outRefundNo={}", refundRecord.getOutRefundNo());
+            }
+        } catch (Exception e) {
+            log.error("[WeChatPartner] 保存 RefundRecord 失败", e);
+        }
+    }
+
+    @Override
+    public R searchRefundRecordByOutRefundNo(String outRefundNo) throws Exception {
+        return null;
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.WECHAT_PAY_PARTNER.getType();
+    }
+
+    /**
+     * 根据门店主键查询特约商户号;缺失或空则返回 null 并打日志,便于排查配置问题。
+     */
+    private String resolveSubMchidFromStore(Integer storeId) {
+        if (storeId == null) {
+            log.warn("[WeChatPartner] storeId 为空,无法解析 wechat_sub_mchid");
+            return null;
+        }
+        StoreInfo info = storeInfoMapper.selectById(storeId);
+        if (info == null) {
+            log.warn("[WeChatPartner] 门店不存在 storeId={}", storeId);
+            return null;
+        }
+        if (StringUtils.isBlank(info.getWechatSubMchid())) {
+            log.warn("[WeChatPartner] 门店未配置 wechat_sub_mchid storeId={}", storeId);
+            return null;
+        }
+        return info.getWechatSubMchid().trim();
+    }
+
+    private static Integer parseStoreIdFromParams(Map<String, String> params) {
+        if (params == null) {
+            return null;
+        }
+        String raw = params.get("storeId");
+        if (raw == null || raw.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(raw.trim());
+        } catch (NumberFormatException e) {
+            log.warn("[WeChatPartner] 退款参数 storeId 非整数: {}", raw);
+            return null;
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse partnerPrePayOrderRun(PartnerAppPrepayRequest request) {
+        String uri = prePayPath;
+        String reqBody = WXPayUtility.toJson(request);
+        log.debug("[WeChatPartner] POST {} bodyLen={}", uri, reqBody.length());
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, POST_METHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+        reqBuilder.method(POST_METHOD, body);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartner prepay request failed: " + uri, e);
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse partnerSearchOrderRun(String outTradeNo, String subMchid) {
+        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(outTradeNo));
+        Map<String, Object> args = new HashMap<>();
+        args.put("sp_mchid", spMchId);
+        args.put("sub_mchid", subMchid);
+        String queryString = WXPayUtility.urlEncode(args);
+        if (!queryString.isEmpty()) {
+            uri = uri + "?" + queryString;
+        }
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, GET_METHOD, uri, null));
+        reqBuilder.method(GET_METHOD, null);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartner query order failed: " + uri, e);
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.Refund partnerRefundRun(PartnerRefundCreateRequest request) {
+        String uri = refundPath;
+        String reqBody = WXPayUtility.toJson(request);
+
+        Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
+        reqBuilder.addHeader("Accept", "application/json");
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, privateKey, POST_METHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+        reqBuilder.method(POST_METHOD, body);
+        Request httpRequest = reqBuilder.build();
+
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(httpRequest).execute()) {
+            String respBody = WXPayUtility.extractBody(httpResponse);
+            if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey, httpResponse.headers(), respBody);
+                return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.Refund.class);
+            }
+            throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+        } catch (IOException e) {
+            throw new UncheckedIOException("WeChatPartner refund failed: " + uri, e);
+        }
+    }
+
+    /**
+     * 服务商 APP 下单请求体(字段名与微信 APIv3 一致)
+     */
+    public static class PartnerAppPrepayRequest {
+        @SerializedName("sp_appid")
+        public String spAppid;
+
+        @SerializedName("sp_mchid")
+        public String spMchid;
+
+        @SerializedName("sub_appid")
+        public String subAppid;
+
+        @SerializedName("sub_mchid")
+        public String subMchid;
+
+        @SerializedName("description")
+        public String description;
+
+        @SerializedName("out_trade_no")
+        public String outTradeNo;
+
+        @SerializedName("notify_url")
+        public String notifyUrl;
+
+        @SerializedName("amount")
+        public WeChatPaymentStrategyImpl.CommonAmountInfo amount;
+    }
+
+    /**
+     * 退款请求:在直连 {@link WeChatPaymentStrategyImpl.CreateRequest} 基础上增加 sub_mchid(服务商退款必填)
+     */
+    public static class PartnerRefundCreateRequest extends WeChatPaymentStrategyImpl.CreateRequest {
+        @SerializedName("sub_mchid")
+        public String subMchid;
+    }
+}

+ 5 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPaymentMininProgramStrategyImpl.java

@@ -210,6 +210,11 @@ public class WeChatPaymentMininProgramStrategyImpl implements PaymentStrategy {
     }
     }
 
 
     @Override
     @Override
+    public R createPreAliPayOrder(String orderId, String amount, String subject, String smid) throws Exception {
+        return null;
+    }
+
+    @Override
     public R handleNotify(String notifyData) throws Exception {
     public R handleNotify(String notifyData) throws Exception {
         return null;
         return null;
     }
     }

+ 6 - 0
alien-store/src/main/java/shop/alien/store/strategy/payment/impl/WeChatPaymentStrategyImpl.java

@@ -228,6 +228,12 @@ public class WeChatPaymentStrategyImpl implements PaymentStrategy {
     }
     }
 
 
     @Override
     @Override
+    public R createPreAliPayOrder(String orderId, String amount, String subject, String smid) throws Exception {
+        return null;
+    }
+
+
+    @Override
     public R handleNotify(String notifyData) throws Exception {
     public R handleNotify(String notifyData) throws Exception {
         /**
         /**
          * 目前没用,先写着
          * 目前没用,先写着

+ 53 - 0
alien-store/src/main/java/shop/alien/store/util/Base62Util.java

@@ -0,0 +1,53 @@
+package shop.alien.store.util;
+
+/**
+ * 正整数的 Base62 编解码(0-9A-Za-z),用于短链码生成。
+ */
+public final class Base62Util {
+
+    private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+    private static final int BASE = ALPHABET.length();
+
+    private Base62Util() {
+    }
+
+    /**
+     * 将正整数编码为 Base62 字符串(不含符号位,{@code value == 0} 时结果为单个首字符)。
+     */
+    public static String encodePositiveLong(long value) {
+        if (value < 0) {
+            throw new IllegalArgumentException("value must be non-negative");
+        }
+        if (value == 0) {
+            return String.valueOf(ALPHABET.charAt(0));
+        }
+        StringBuilder sb = new StringBuilder();
+        long v = value;
+        while (v > 0) {
+            sb.append(ALPHABET.charAt((int) (v % BASE)));
+            v /= BASE;
+        }
+        return sb.reverse().toString();
+    }
+
+    /**
+     * Base62 解码为 long;非法字符时抛出 {@link IllegalArgumentException}。
+     */
+    public static long decodeToPositiveLong(String code) {
+        if (code == null || code.isEmpty()) {
+            throw new IllegalArgumentException("code is empty");
+        }
+        long result = 0;
+        for (int i = 0; i < code.length(); i++) {
+            int digit = ALPHABET.indexOf(code.charAt(i));
+            if (digit < 0) {
+                throw new IllegalArgumentException("invalid base62 char: " + code.charAt(i));
+            }
+            result = result * BASE + digit;
+            if (result < 0) {
+                throw new IllegalArgumentException("overflow");
+            }
+        }
+        return result;
+    }
+}

+ 133 - 0
alien-store/src/main/java/shop/alien/store/util/ShortLinkUrlValidator.java

@@ -0,0 +1,133 @@
+package shop.alien.store.util;
+
+import java.net.IDN;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 短链目标地址校验:HTTPS(可配置是否允许 HTTP)、长度、内网/本地地址、可选域名白名单。
+ */
+public final class ShortLinkUrlValidator {
+
+    private static final int MAX_URL_LENGTH = 2048;
+
+    private ShortLinkUrlValidator() {
+    }
+
+    /**
+     * @param longUrl                   原始 URL
+     * @param allowedHosts              允许的 host(小写),如 {@code www.example.com};为空表示不限制 host(仍会拦截内网地址,除非 {@code allowLocalNetworkTarget})
+     * @param httpsOnly                 true 时仅允许 https
+     * @param allowLocalNetworkTarget   true 时不拦截 localhost/内网 IP(仅用于本地开发,禁止线上开启)
+     * @return 规范化后的 URL(trim)
+     */
+    public static String validateAndNormalize(String longUrl, Set<String> allowedHosts, boolean httpsOnly,
+                                              boolean allowLocalNetworkTarget) {
+        if (longUrl == null || longUrl.trim().isEmpty()) {
+            throw new IllegalArgumentException("长链接不能为空");
+        }
+        String trimmed = longUrl.trim();
+        if (trimmed.length() > MAX_URL_LENGTH) {
+            throw new IllegalArgumentException("长链接过长");
+        }
+        URI uri;
+        try {
+            uri = new URI(trimmed);
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("长链接格式无效");
+        }
+        String scheme = uri.getScheme();
+        if (scheme == null) {
+            throw new IllegalArgumentException("长链接需包含协议(https)");
+        }
+        String s = scheme.toLowerCase(Locale.ROOT);
+        if (httpsOnly) {
+            if (!"https".equals(s)) {
+                throw new IllegalArgumentException("仅支持 https 长链接");
+            }
+        } else {
+            if (!"https".equals(s) && !"http".equals(s)) {
+                throw new IllegalArgumentException("仅支持 http / https 长链接");
+            }
+        }
+        String host = uri.getHost();
+        if (host == null || host.isEmpty()) {
+            throw new IllegalArgumentException("长链接缺少主机名");
+        }
+        String hostLower = IDN.toUnicode(host).toLowerCase(Locale.ROOT);
+        if (!allowLocalNetworkTarget && isBlockedHost(hostLower)) {
+            throw new IllegalArgumentException("不允许使用该主机地址");
+        }
+        if (allowedHosts != null && !allowedHosts.isEmpty()) {
+            if (!hostMatchesAllowlist(hostLower, allowedHosts)) {
+                throw new IllegalArgumentException("长链接域名不在允许列表内");
+            }
+        }
+        return trimmed;
+    }
+
+    public static Set<String> parseAllowedHostsCsv(String csv) {
+        if (csv == null || csv.trim().isEmpty()) {
+            return Collections.emptySet();
+        }
+        return Arrays.stream(csv.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .map(s -> s.toLowerCase(Locale.ROOT))
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    private static boolean hostMatchesAllowlist(String host, Set<String> allowedHosts) {
+        for (String allowed : allowedHosts) {
+            if (allowed == null || allowed.isEmpty()) {
+                continue;
+            }
+            if (host.equals(allowed) || host.endsWith("." + allowed)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isBlockedHost(String hostLower) {
+        if ("localhost".equals(hostLower)) {
+            return true;
+        }
+        if ("0.0.0.0".equals(hostLower) || "::1".equals(hostLower)) {
+            return true;
+        }
+        // IPv4 简单规则:127.*、10.*、192.168.*、169.254.*、172.16-31.*
+        if (hostLower.startsWith("127.")) {
+            return true;
+        }
+        if (hostLower.startsWith("10.")) {
+            return true;
+        }
+        if (hostLower.startsWith("192.168.")) {
+            return true;
+        }
+        if (hostLower.startsWith("169.254.")) {
+            return true;
+        }
+        if (hostLower.startsWith("172.")) {
+            String[] parts = hostLower.split("\\.");
+            if (parts.length >= 2) {
+                try {
+                    int second = Integer.parseInt(parts[1]);
+                    if (second >= 16 && second <= 31) {
+                        return true;
+                    }
+                } catch (NumberFormatException ignored) {
+                    // ignore
+                }
+            }
+        }
+        return false;
+    }
+}

+ 7 - 1
alien-util/src/main/java/shop/alien/util/common/constant/PaymentEnum.java

@@ -9,10 +9,16 @@ package shop.alien.util.common.constant;
 public enum PaymentEnum {
 public enum PaymentEnum {
     /** 支付宝 */
     /** 支付宝 */
     ALIPAY("alipay"),
     ALIPAY("alipay"),
+    /** 支付宝服务商支付 */
+    ALIPAY_PARTNER("aliPayPartner"),
     /** 微信支付 */
     /** 微信支付 */
     WECHAT_PAY("wechatPay"),
     WECHAT_PAY("wechatPay"),
-    /** 微信支付小程序 */
+    /** 微信支付小程序 废弃 */
     WECHAT_PAY_MININ_PROGRAM("wechatPayMininProgram"),
     WECHAT_PAY_MININ_PROGRAM("wechatPayMininProgram"),
+    /** 微信支付(服务商模式 / 特约商户) */
+    WECHAT_PAY_PARTNER("wechatPayPartner"),
+    /** 微信支付小程序(服务商模式 / 特约商户 JSAPI) */
+    WECHAT_PAY_PARTNER_MININ_PROGRAM("wechatPayPartnerMininProgram"),
     /** 银联支付 */
     /** 银联支付 */
     UNION_PAY("unionPay");
     UNION_PAY("unionPay");