zhangchen 2 недель назад
Родитель
Сommit
8b7d386a85
55 измененных файлов с 3622 добавлено и 0 удалено
  1. 72 0
      alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembership.java
  2. 107 0
      alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembershipOrder.java
  3. 99 0
      alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembershipPaymentOrder.java
  4. 115 0
      alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembershipPlan.java
  5. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/PlatformShopMembershipDeleteRequest.java
  6. 27 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/PlatformShopMembershipOrderCreateRequest.java
  7. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/PlatformShopMembershipPlanDeleteRequest.java
  8. 31 0
      alien-entity/src/main/java/shop/alien/entity/store/excelVo/util/ExcelExporter.java
  9. 45 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipAdminRowVo.java
  10. 47 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipBenefitsVo.java
  11. 27 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipOrderCreateResultVo.java
  12. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipOrderItemVo.java
  13. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipRemainingDetailVo.java
  14. 43 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipStatusVo.java
  15. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreMainInfoVo.java
  16. 26 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipAdminMapper.java
  17. 9 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipMapper.java
  18. 17 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipOrderMapper.java
  19. 9 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipPaymentOrderMapper.java
  20. 9 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipPlanMapper.java
  21. 17 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTrackEventMapper.java
  22. 79 0
      alien-entity/src/main/resources/mapper/PlatformShopMembershipAdminMapper.xml
  23. 28 0
      alien-entity/src/main/resources/mapper/PlatformShopMembershipOrderMapper.xml
  24. 79 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipAdminController.java
  25. 83 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipController.java
  26. 106 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipPaymentController.java
  27. 87 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipPlanPlatformController.java
  28. 23 0
      alien-store/src/main/java/shop/alien/store/platformmembership/PlatformShopMembershipDateUtil.java
  29. 11 0
      alien-store/src/main/java/shop/alien/store/service/PlatformMembershipPaymentQueryService.java
  30. 16 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipAdminMutationService.java
  31. 23 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipAdminQueryService.java
  32. 17 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderCreateDebounceService.java
  33. 17 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderPaymentTimeoutService.java
  34. 30 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderService.java
  35. 33 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPaymentOrderService.java
  36. 26 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPlanPlatformService.java
  37. 16 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPlanService.java
  38. 18 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipService.java
  39. 5 0
      alien-store/src/main/java/shop/alien/store/service/TrackEventService.java
  40. 85 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformMembershipPaymentQueryServiceImpl.java
  41. 40 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipAdminMutationServiceImpl.java
  42. 57 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipAdminQueryServiceImpl.java
  43. 88 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderCreateDebounceServiceImpl.java
  44. 83 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderPaymentTimeoutServiceImpl.java
  45. 227 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderServiceImpl.java
  46. 121 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPaymentOrderServiceImpl.java
  47. 230 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPlanPlatformServiceImpl.java
  48. 40 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPlanServiceImpl.java
  49. 264 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipServiceImpl.java
  50. 2 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  51. 17 0
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  52. 23 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/PlatformMembershipPaymentStrategy.java
  53. 33 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/PlatformMembershipPaymentStrategyFactory.java
  54. 334 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/impl/PlatformMembershipAlipayPaymentStrategyImpl.java
  55. 440 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/impl/PlatformMembershipWechatPaymentStrategyImpl.java

+ 72 - 0
alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembership.java

@@ -0,0 +1,72 @@
+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("platform_shop_membership")
+@ApiModel(value = "PlatformShopMembership", description = "店铺平台会员卡状态")
+public class PlatformShopMembership {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("store_id")
+    @ApiModelProperty("门店ID")
+    private Integer storeId;
+
+    @TableField("first_open_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("首次开通时间")
+    private Date firstOpenTime;
+
+    @TableField("current_period_start")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("当前会员周期开始")
+    private Date currentPeriodStart;
+
+    @TableField("expiry_date")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("当前到期时间")
+    private Date expiryDate;
+
+    @TableField("current_plan_id")
+    @ApiModelProperty("当前有效会员对应的方案ID(支付成功写入,与 platform_shop_membership_plan.id 对应)")
+    private Integer currentPlanId;
+
+    @TableField("weekly_promotion_remaining")
+    @ApiModelProperty("每周推广剩余次数")
+    private Integer weeklyPromotionRemaining;
+
+    @TableField("monthly_computing_power_remaining")
+    @ApiModelProperty("每月算力剩余")
+    private Integer monthlyComputingPowerRemaining;
+
+    @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("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 107 - 0
alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembershipOrder.java

@@ -0,0 +1,107 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 平台店铺会员卡订单
+ */
+@Data
+@JsonInclude
+@TableName("platform_shop_membership_order")
+@ApiModel(value = "PlatformShopMembershipOrder", description = "平台店铺会员卡订单")
+public class PlatformShopMembershipOrder {
+
+    public static final int PAY_STATUS_PENDING = 0;
+    public static final int PAY_STATUS_PAID = 1;
+    public static final int PAY_STATUS_CLOSED = 2;
+    /** 已退款(支付单退款成功后回写) */
+    public static final int PAY_STATUS_REFUNDED = 3;
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("order_sn")
+    @ApiModelProperty("业务订单号")
+    private String orderSn;
+
+    @TableField("store_id")
+    @ApiModelProperty("门店ID")
+    private Integer storeId;
+
+    @TableField("plan_id")
+    @ApiModelProperty("方案ID")
+    private Integer planId;
+
+    @TableField("plan_name")
+    @ApiModelProperty("方案名称快照")
+    private String planName;
+
+    @TableField("duration_months")
+    @ApiModelProperty("时长(月)快照")
+    private Integer durationMonths;
+
+    @TableField("pay_amount")
+    @ApiModelProperty("应付金额(元)")
+    private BigDecimal payAmount;
+
+    @TableField("pay_status")
+    @ApiModelProperty("支付状态 0待支付 1已支付 2已关闭 3已退款")
+    private Integer payStatus;
+
+    @TableField("payment_method")
+    @ApiModelProperty("支付方式")
+    private String paymentMethod;
+
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("支付完成时间")
+    private Date payTime;
+
+    @TableField("out_trade_no")
+    @ApiModelProperty("商户订单号")
+    private String outTradeNo;
+
+    @TableField("trade_no")
+    @ApiModelProperty("第三方交易号")
+    private String tradeNo;
+
+    @TableField("validity_start")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("本笔权益开始")
+    private Date validityStart;
+
+    @TableField("validity_end")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("本笔权益结束")
+    private Date validityEnd;
+
+    @TableField("payer_user_id")
+    @ApiModelProperty("付款人门店用户ID")
+    private Integer payerUserId;
+
+    @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("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 99 - 0
alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembershipPaymentOrder.java

@@ -0,0 +1,99 @@
+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 org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 平台店铺会员卡 — 支付流水(独立表,不写入 merchant_payment_order)
+ */
+@Data
+@JsonInclude
+@TableName("platform_shop_membership_payment_order")
+@ApiModel(value = "PlatformShopMembershipPaymentOrder", description = "平台店铺会员卡支付单")
+public class PlatformShopMembershipPaymentOrder {
+
+    public static final int PAY_STATUS_PENDING = 0;
+    public static final int PAY_STATUS_PAID = 1;
+    public static final int PAY_STATUS_CLOSED = 2;
+    public static final int PAY_STATUS_REFUNDED = 3;
+
+    @ApiModelProperty("主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty("支付单号(业务唯一)")
+    @TableField("payment_no")
+    private String paymentNo;
+
+    @ApiModelProperty("会员卡业务订单ID platform_shop_membership_order.id")
+    @TableField("membership_order_id")
+    private Integer membershipOrderId;
+
+    @ApiModelProperty("会员卡业务订单号")
+    @TableField("order_sn")
+    private String orderSn;
+
+    @ApiModelProperty("门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty("支付方式 alipay/wechatPay")
+    @TableField("pay_type")
+    private String payType;
+
+    @ApiModelProperty("商户订单号(传支付宝/微信)")
+    @TableField("out_trade_no")
+    private String outTradeNo;
+
+    @ApiModelProperty("第三方交易号")
+    @TableField("trade_no")
+    private String tradeNo;
+
+    @ApiModelProperty("支付金额(元)")
+    @TableField("pay_amount")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty("支付状态 0待支付 1已支付 2已关闭 3已退款 4退款中")
+    @TableField("pay_status")
+    private Integer payStatus;
+
+    @ApiModelProperty("支付完成时间")
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    @ApiModelProperty("付款人门店用户ID")
+    @TableField("payer_user_id")
+    private Integer payerUserId;
+
+    @ApiModelProperty("订单描述/商品标题")
+    @TableField("subject")
+    private String subject;
+
+    @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("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 115 - 0
alien-entity/src/main/java/shop/alien/entity/store/PlatformShopMembershipPlan.java

@@ -0,0 +1,115 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 平台店铺会员卡方案(后台配置)
+ */
+@Data
+@JsonInclude
+@TableName("platform_shop_membership_plan")
+@ApiModel(value = "PlatformShopMembershipPlan", description = "平台店铺会员卡方案")
+public class PlatformShopMembershipPlan {
+
+    @TableId(type = IdType.AUTO)
+    @ApiModelProperty("主键")
+    private Integer id;
+
+    @TableField("plan_name")
+    @ApiModelProperty("方案名称")
+    private String planName;
+
+    @TableField("duration_months")
+    @ApiModelProperty("时长(月)")
+    private Integer durationMonths;
+
+    @TableField("price")
+    @ApiModelProperty("价格(元)")
+    private BigDecimal price;
+
+    @TableField("description")
+    @ApiModelProperty("描述")
+    private String description;
+
+    @TableField("enabled")
+    @ApiModelProperty("是否上架 0否 1是")
+    private Integer enabled;
+
+    @TableField("sort_order")
+    @ApiModelProperty("排序")
+    private Integer sortOrder;
+
+    @TableField("recommended")
+    @ApiModelProperty("是否推荐 0否 1是")
+    private Integer recommended;
+
+    @TableField("benefit_advanced_data_analysis")
+    @ApiModelProperty("高级数据分析工具 0否1是")
+    private Integer benefitAdvancedDataAnalysis;
+
+    @TableField("benefit_marketing_priority")
+    @ApiModelProperty("营销活动优先参与权 0否1是")
+    private Integer benefitMarketingPriority;
+
+    @TableField("benefit_friend_coupon_manage")
+    @ApiModelProperty("好友赠券管理 0否1是")
+    private Integer benefitFriendCouponManage;
+
+    @TableField("benefit_ordering_manage")
+    @ApiModelProperty("点餐管理 0否1是")
+    private Integer benefitOrderingManage;
+
+    @TableField("benefit_friend_relation_manage")
+    @ApiModelProperty("好友关系管理 0否1是")
+    private Integer benefitFriendRelationManage;
+
+    @TableField("benefit_comment_appeal_delete")
+    @ApiModelProperty("评论申诉删除功能 0否1是")
+    private Integer benefitCommentAppealDelete;
+
+    @TableField("benefit_operating_data")
+    @ApiModelProperty("经营数据 0否1是")
+    private Integer benefitOperatingData;
+
+    @TableField("benefit_operational_activity")
+    @ApiModelProperty("运营活动 0否1是")
+    private Integer benefitOperationalActivity;
+
+    @TableField("benefit_weekly_homepage_slots")
+    @ApiModelProperty("每周首页推广位次数")
+    private Integer benefitWeeklyHomepageSlots;
+
+    @TableField("benefit_monthly_computing_power")
+    @ApiModelProperty("每月赠送算力")
+    private Integer benefitMonthlyComputingPower;
+
+    @TableField(exist = false)
+    @ApiModelProperty("权益数量(分页接口回填:已开启的权益项个数,非表字段)")
+    private Integer benefitCount;
+
+    @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("created_user_id")
+    private Integer createdUserId;
+
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

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

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+/**
+ * 平台端逻辑删除店铺平台会员卡(按门店 ID)
+ */
+@Data
+@ApiModel("平台端删除店铺会员卡请求")
+public class PlatformShopMembershipDeleteRequest {
+
+    @NotEmpty(message = "门店ID列表不能为空")
+    @ApiModelProperty(value = "门店ID列表,单个删除时传一个元素即可", required = true)
+    private List<Integer> storeIds;
+}

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/PlatformShopMembershipOrderCreateRequest.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 店铺端创建会员卡订单
+ */
+@Data
+@ApiModel("创建平台会员卡订单请求")
+public class PlatformShopMembershipOrderCreateRequest {
+
+    @NotNull
+    @ApiModelProperty(value = "门店ID", required = true)
+    private Integer storeId;
+
+    @NotNull
+    @ApiModelProperty(value = "方案ID", required = true)
+    private Integer planId;
+
+    @NotNull
+    @ApiModelProperty(value = "付款门店用户ID", required = true)
+    private Integer payerUserId;
+}

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

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+/**
+ * 平台端逻辑删除会员卡方案(platform_shop_membership_plan)
+ */
+@Data
+@ApiModel("平台端删除会员卡方案请求")
+public class PlatformShopMembershipPlanDeleteRequest {
+
+    @NotEmpty(message = "方案ID列表不能为空")
+    @ApiModelProperty(value = "会员卡方案主键 id 列表,单个删除时传一个元素", required = true)
+    private List<Integer> ids;
+}

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/store/excelVo/util/ExcelExporter.java

@@ -6,6 +6,7 @@ import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.util.StringUtils;
 import shop.alien.entity.store.LifeClassManage;
+import shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo;
 import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.entity.store.vo.StoreDynamicDiscountVo;
 import shop.alien.entity.store.vo.StoreInfoVo;
@@ -26,6 +27,7 @@ public class ExcelExporter {
             headerRow.createCell(i).setCellValue(headers[i]);
         }
         SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        SimpleDateFormat dateOnly = new SimpleDateFormat("yyyy-MM-dd");
 
         int rowNum = 1;
         for (T item : data) {
@@ -68,6 +70,35 @@ public class ExcelExporter {
                 if (!StringUtils.isEmpty(manage.getClassState())) {
                     row.createCell(6).setCellValue(manage.getClassState().equals("1") ? "正常" : "隐藏");
                 }
+            } else if (item instanceof PlatformShopMembershipAdminRowVo) {
+                PlatformShopMembershipAdminRowVo vo = (PlatformShopMembershipAdminRowVo) item;
+                row.createCell(0).setCellValue(vo.getStoreName() != null ? vo.getStoreName() : "");
+                row.createCell(1).setCellValue(vo.getContactName() != null ? vo.getContactName() : "");
+                row.createCell(2).setCellValue(vo.getContactPhone() != null ? vo.getContactPhone() : "");
+                if (vo.getFirstOpenDate() != null) {
+                    row.createCell(3).setCellValue(dateOnly.format(vo.getFirstOpenDate()));
+                } else {
+                    row.createCell(3).setCellValue("");
+                }
+                if (vo.getExpiryDate() != null) {
+                    row.createCell(4).setCellValue(dateOnly.format(vo.getExpiryDate()));
+                } else {
+                    row.createCell(4).setCellValue("");
+                }
+                String statusText = "";
+                Integer st = vo.getMembershipUiStatus();
+                if (st != null) {
+                    if (st == 0) {
+                        statusText = "正常";
+                    } else if (st == 1) {
+                        statusText = "即将到期";
+                    } else if (st == 2) {
+                        statusText = "已过期";
+                    } else {
+                        statusText = String.valueOf(st);
+                    }
+                }
+                row.createCell(5).setCellValue(statusText);
             } else if (item instanceof StoreDynamicDiscountVo) {
                 StoreDynamicDiscountVo dynamicDiscountVo = (StoreDynamicDiscountVo) item;
                 row.createCell(0).setCellValue(dynamicDiscountVo.getActivityNo());

+ 45 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipAdminRowVo.java

@@ -0,0 +1,45 @@
+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;
+
+/**
+ * 平台端-已购会员卡店铺列表行
+ */
+@Data
+@ApiModel("平台端店铺会员列表行")
+public class PlatformShopMembershipAdminRowVo {
+
+    @ApiModelProperty("门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty("门店名称")
+    private String storeName;
+
+    @ApiModelProperty("门店头图 URL(首张;单图模式 type20 / 多图模式 type21,按 img_sort、id 排序)")
+    private String storeHeadImgUrl;
+
+    @ApiModelProperty("联系人")
+    private String contactName;
+
+    @ApiModelProperty("联系电话")
+    private String contactPhone;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @ApiModelProperty("开通日期(首次)")
+    private Date firstOpenDate;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("到期时间")
+    private Date expiryDate;
+
+    /**
+     * 0 正常 1 即将到期(未过期且到期在一个月内)2 已过期
+     */
+    @ApiModelProperty("状态 0正常 1即将到期(一个月内) 2已过期")
+    private Integer membershipUiStatus;
+}

+ 47 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipBenefitsVo.java

@@ -0,0 +1,47 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/** 当前会员周期对应的方案权益(字段与平台方案表权益列一致) */
+@Data
+@ApiModel("平台会员卡当前权益快照")
+public class PlatformShopMembershipBenefitsVo {
+
+    @ApiModelProperty("方案 ID")
+    private Integer planId;
+
+    @ApiModelProperty("方案名称")
+    private String planName;
+
+    @ApiModelProperty("高级数据分析工具 0否1是")
+    private Integer benefitAdvancedDataAnalysis;
+
+    @ApiModelProperty("营销活动优先参与权 0否1是")
+    private Integer benefitMarketingPriority;
+
+    @ApiModelProperty("好友赠券管理 0否1是")
+    private Integer benefitFriendCouponManage;
+
+    @ApiModelProperty("点餐管理 0否1是")
+    private Integer benefitOrderingManage;
+
+    @ApiModelProperty("好友关系管理 0否1是")
+    private Integer benefitFriendRelationManage;
+
+    @ApiModelProperty("评论申诉删除功能 0否1是")
+    private Integer benefitCommentAppealDelete;
+
+    @ApiModelProperty("经营数据 0否1是")
+    private Integer benefitOperatingData;
+
+    @ApiModelProperty("运营活动 0否1是")
+    private Integer benefitOperationalActivity;
+
+    @ApiModelProperty("每周首页推广位次数")
+    private Integer benefitWeeklyHomepageSlots;
+
+    @ApiModelProperty("每月赠送算力")
+    private Integer benefitMonthlyComputingPower;
+}

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipOrderCreateResultVo.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("创建会员卡订单结果")
+public class PlatformShopMembershipOrderCreateResultVo {
+
+    @ApiModelProperty("订单ID,用于发起支付")
+    private Integer orderId;
+
+    @ApiModelProperty("业务订单号")
+    private String orderSn;
+
+    @ApiModelProperty("应付金额(元)")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty("方案名称")
+    private String planName;
+
+    @ApiModelProperty("时长(月)")
+    private Integer durationMonths;
+}

+ 50 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipOrderItemVo.java

@@ -0,0 +1,50 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 店铺端购买记录
+ */
+@Data
+@ApiModel("平台会员卡订单明细")
+public class PlatformShopMembershipOrderItemVo {
+
+    @ApiModelProperty("订单ID")
+    private Integer id;
+
+    @ApiModelProperty("订单号")
+    private String orderSn;
+
+    @ApiModelProperty("方案名称")
+    private String planName;
+
+    @ApiModelProperty("时长(月)")
+    private Integer durationMonths;
+
+    @ApiModelProperty("实付金额(元)")
+    private BigDecimal payAmount;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("本笔权益开始")
+    private Date validityStart;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("本笔权益结束")
+    private Date validityEnd;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("支付/下单时间")
+    private Date purchaseTime;
+
+    @ApiModelProperty("支付状态文案")
+    private String payStatusText;
+
+    @ApiModelProperty("支付方式")
+    private String paymentMethod;
+}

+ 48 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipRemainingDetailVo.java

@@ -0,0 +1,48 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 店铺会员卡剩余时长与当前连续购买周期(与会员表 current_period_start / expiry_date 一致)
+ */
+@Data
+@ApiModel("会员卡剩余时间与连续购买周期")
+public class PlatformShopMembershipRemainingDetailVo {
+
+    @ApiModelProperty("是否存在会员档案(platform_shop_membership 有该行且已写入到期时间)")
+    private Boolean hasMembershipRecord;
+
+    @ApiModelProperty("当前是否在有效期内(到期时间晚于当前时刻)")
+    private Boolean membershipActive;
+
+    @ApiModelProperty("剩余天数:未开通或已过期为 0;算法与 /status 接口 remainingDays 一致(按毫秒向上取整到天)")
+    private Integer remainingDays;
+
+    @ApiModelProperty("当前连续购买周期总天数:连续周期开始日 0 点 至 到期日 之间的天数(与剩余天数同一套按毫秒折算方式);无有效起止时间为 null")
+    private Integer consecutivePurchaseTotalDays;
+
+    @ApiModelProperty("连续周期开始日期,格式 yyyy/MM/dd;无则为 null(老数据无 current_period_start 时可能回退为首次开通日)")
+    private String consecutivePeriodStartYmd;
+
+    @ApiModelProperty("当前会员卡到期日期,格式 yyyy/MM/dd")
+    private String currentMembershipEndYmd;
+
+    @ApiModelProperty("每周推广次数总额:当前有效方案 benefit_weekly_homepage_slots;非有效会员为 0")
+    private Integer weeklyPromotionTotalPerWeek;
+
+    @ApiModelProperty("本周剩余推广次数:会员表 weekly_promotion_remaining;非有效会员为 0")
+    private Integer weeklyPromotionRemainingThisWeek;
+
+    @ApiModelProperty("上一自然月全月店铺访问次数(埋点 TRAFFIC + VIEW,与店铺总浏览量口径一致)")
+    private Long lastMonthVisitCount;
+
+    @ApiModelProperty("本自然月从 1 日 0 点(上海时区)至当前时刻的店铺访问次数(埋点 TRAFFIC + VIEW)")
+    private Long currentMonthVisitCount;
+
+    @ApiModelProperty("月环比变化百分比:((本月至今 - 上月全月) / 上月全月) × 100,保留两位小数;正为提升负为下降;上月访问数为 0 时为 null(无法计算)")
+    private BigDecimal visitCountMoMChangePercent;
+}

+ 43 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipStatusVo.java

@@ -0,0 +1,43 @@
+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;
+
+/**
+ * 店铺端会员中心状态
+ */
+@Data
+@ApiModel("平台会员卡当前状态")
+public class PlatformShopMembershipStatusVo {
+
+    @ApiModelProperty("是否有效会员")
+    private Boolean active;
+
+    @ApiModelProperty("剩余天数(无效为0)")
+    private Integer remainingDays;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("当前周期开始")
+    private Date currentPeriodStart;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("当前到期时间")
+    private Date expiryDate;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("首次开通时间")
+    private Date firstOpenTime;
+
+    @ApiModelProperty("是否签约(基于 store_info.renew_contract_status == 1)")
+    private Boolean signed;
+
+    @ApiModelProperty("是否入驻(基于 store_info.store_application_status == 1)")
+    private Boolean settled;
+
+    @ApiModelProperty("当前有效会员对应的方案权益;非会员或无法解析方案时为 null")
+    private PlatformShopMembershipBenefitsVo benefits;
+}

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

@@ -130,4 +130,7 @@ public class StoreMainInfoVo extends StoreInfo {
     @ApiModelProperty(value = "是否提供预约")
     private Integer bookingService;
 
+    @ApiModelProperty(value = "平台会员状态(迁移自 /platform-shop-membership/status)")
+    private PlatformShopMembershipStatusVo platformMembershipStatus;
+
 }

+ 26 - 0
alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipAdminMapper.java

@@ -0,0 +1,26 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo;
+
+import java.util.List;
+
+@Mapper
+public interface PlatformShopMembershipAdminMapper {
+
+    IPage<PlatformShopMembershipAdminRowVo> selectStoreMembershipPage(
+            Page<?> page,
+            @Param("keyword") String keyword,
+            @Param("membershipUiStatus") Integer membershipUiStatus,
+            @Param("expiryStart") String expiryStart,
+            @Param("expiryEnd") String expiryEnd);
+
+    List<PlatformShopMembershipAdminRowVo> selectStoreMembershipList(
+            @Param("keyword") String keyword,
+            @Param("membershipUiStatus") Integer membershipUiStatus,
+            @Param("expiryStart") String expiryStart,
+            @Param("expiryEnd") String expiryEnd);
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipMapper.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.PlatformShopMembership;
+
+@Mapper
+public interface PlatformShopMembershipMapper extends BaseMapper<PlatformShopMembership> {
+}

+ 17 - 0
alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipOrderMapper.java

@@ -0,0 +1,17 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderItemVo;
+
+@Mapper
+public interface PlatformShopMembershipOrderMapper extends BaseMapper<PlatformShopMembershipOrder> {
+
+    IPage<PlatformShopMembershipOrderItemVo> selectHistoryPage(
+            Page<?> page,
+            @Param("storeId") Integer storeId);
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipPaymentOrderMapper.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.PlatformShopMembershipPaymentOrder;
+
+@Mapper
+public interface PlatformShopMembershipPaymentOrderMapper extends BaseMapper<PlatformShopMembershipPaymentOrder> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipPlanMapper.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.PlatformShopMembershipPlan;
+
+@Mapper
+public interface PlatformShopMembershipPlanMapper extends BaseMapper<PlatformShopMembershipPlan> {
+}

+ 17 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreTrackEventMapper.java

@@ -148,4 +148,21 @@ public interface StoreTrackEventMapper extends BaseMapper<StoreTrackEvent> {
             "AND event_category = 'TRAFFIC' " +
             "AND event_type = 'VIEW'")
     Long countStoreViewCount(@Param("storeId") Integer storeId);
+
+    /**
+     * 区间内店铺访问(浏览)次数,口径与 {@link #countStoreViewCount} 一致:TRAFFIC + VIEW
+     *
+     * @param startInclusive 含
+     * @param endInclusive   含
+     */
+    @Select("SELECT COUNT(*) FROM store_track_event " +
+            "WHERE store_id = #{storeId} " +
+            "AND delete_flag = 0 " +
+            "AND event_category = 'TRAFFIC' " +
+            "AND event_type = 'VIEW' " +
+            "AND event_time >= #{startInclusive} " +
+            "AND event_time <= #{endInclusive}")
+    Long countStoreViewCountBetweenInclusive(@Param("storeId") Integer storeId,
+                                             @Param("startInclusive") Date startInclusive,
+                                             @Param("endInclusive") Date endInclusive);
 }

+ 79 - 0
alien-entity/src/main/resources/mapper/PlatformShopMembershipAdminMapper.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.PlatformShopMembershipAdminMapper">
+
+    <sql id="storeMembershipFromWhere">
+        FROM platform_shop_membership m
+        INNER JOIN store_info s ON s.id = m.store_id AND s.delete_flag = 0
+        WHERE m.delete_flag = 0
+        <if test="keyword != null and keyword != ''">
+            AND (
+                s.store_name LIKE CONCAT('%', #{keyword}, '%')
+                OR EXISTS (
+                    SELECT 1 FROM store_user u2
+                    WHERE u2.store_id = m.store_id AND u2.delete_flag = 0
+                      AND (u2.name LIKE CONCAT('%', #{keyword}, '%') OR u2.phone LIKE CONCAT('%', #{keyword}, '%'))
+                )
+            )
+        </if>
+        <if test="membershipUiStatus != null">
+            AND (
+                CASE
+                    WHEN m.expiry_date IS NULL OR m.expiry_date &lt; NOW() THEN 2
+                    WHEN m.expiry_date &lt; DATE_ADD(NOW(), INTERVAL 1 MONTH) THEN 1
+                    ELSE 0
+                END
+            ) = #{membershipUiStatus}
+        </if>
+        <if test="expiryStart != null and expiryStart != ''">
+            AND m.expiry_date &gt;= CONCAT(#{expiryStart}, ' 00:00:00')
+        </if>
+        <if test="expiryEnd != null and expiryEnd != ''">
+            AND m.expiry_date &lt;= CONCAT(#{expiryEnd}, ' 23:59:59')
+        </if>
+    </sql>
+
+    <select id="selectStoreMembershipPage" resultType="shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo">
+        SELECT
+            m.store_id AS storeId,
+            s.store_name AS storeName,
+            (SELECT i.img_url FROM store_img i
+             WHERE i.store_id = m.store_id AND i.delete_flag = 0
+               AND i.img_type = (CASE WHEN s.img_mode = 0 THEN 20 ELSE 21 END)
+             ORDER BY IFNULL(i.img_sort, 0) ASC, i.id ASC
+             LIMIT 1) AS storeHeadImgUrl,
+            (SELECT u.name FROM store_user u WHERE u.store_id = m.store_id AND u.delete_flag = 0 AND u.account_type = 1 ORDER BY u.id LIMIT 1) AS contactName,
+            (SELECT u.phone FROM store_user u WHERE u.store_id = m.store_id AND u.delete_flag = 0 AND u.account_type = 1 ORDER BY u.id LIMIT 1) AS contactPhone,
+            DATE(m.first_open_time) AS firstOpenDate,
+            m.expiry_date AS expiryDate,
+            CASE
+                WHEN m.expiry_date IS NULL OR m.expiry_date &lt; NOW() THEN 2
+                WHEN m.expiry_date &lt; DATE_ADD(NOW(), INTERVAL 1 MONTH) THEN 1
+                ELSE 0
+            END AS membershipUiStatus
+        <include refid="storeMembershipFromWhere"/>
+        ORDER BY m.expiry_date IS NULL, m.expiry_date ASC
+    </select>
+
+    <select id="selectStoreMembershipList" resultType="shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo">
+        SELECT
+            m.store_id AS storeId,
+            s.store_name AS storeName,
+            (SELECT i.img_url FROM store_img i
+             WHERE i.store_id = m.store_id AND i.delete_flag = 0
+               AND i.img_type = (CASE WHEN s.img_mode = 0 THEN 20 ELSE 21 END)
+             ORDER BY IFNULL(i.img_sort, 0) ASC, i.id ASC
+             LIMIT 1) AS storeHeadImgUrl,
+            (SELECT u.name FROM store_user u WHERE u.store_id = m.store_id AND u.delete_flag = 0 AND u.account_type = 1 ORDER BY u.id LIMIT 1) AS contactName,
+            (SELECT u.phone FROM store_user u WHERE u.store_id = m.store_id AND u.delete_flag = 0 AND u.account_type = 1 ORDER BY u.id LIMIT 1) AS contactPhone,
+            DATE(m.first_open_time) AS firstOpenDate,
+            m.expiry_date AS expiryDate,
+            CASE
+                WHEN m.expiry_date IS NULL OR m.expiry_date &lt; NOW() THEN 2
+                WHEN m.expiry_date &lt; DATE_ADD(NOW(), INTERVAL 1 MONTH) THEN 1
+                ELSE 0
+            END AS membershipUiStatus
+        <include refid="storeMembershipFromWhere"/>
+        ORDER BY m.expiry_date IS NULL, m.expiry_date ASC
+    </select>
+</mapper>

+ 28 - 0
alien-entity/src/main/resources/mapper/PlatformShopMembershipOrderMapper.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.PlatformShopMembershipOrderMapper">
+
+    <select id="selectHistoryPage" resultType="shop.alien.entity.store.vo.PlatformShopMembershipOrderItemVo">
+        SELECT
+            o.id,
+            o.order_sn AS orderSn,
+            o.plan_name AS planName,
+            o.duration_months AS durationMonths,
+            o.pay_amount AS payAmount,
+            o.validity_start AS validityStart,
+            o.validity_end AS validityEnd,
+            COALESCE(o.pay_time, o.created_time) AS purchaseTime,
+            o.payment_method AS paymentMethod,
+            CASE o.pay_status
+                WHEN 0 THEN '待支付'
+                WHEN 1 THEN '交易成功'
+                WHEN 2 THEN '已关闭'
+                ELSE '未知'
+            END AS payStatusText
+        FROM platform_shop_membership_order o
+        WHERE o.delete_flag = 0
+          AND o.store_id = #{storeId}
+          AND o.pay_status = 1
+        ORDER BY o.pay_time DESC, o.created_time DESC
+    </select>
+</mapper>

+ 79 - 0
alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipAdminController.java

@@ -0,0 +1,79 @@
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.PlatformShopMembershipDeleteRequest;
+import shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo;
+import shop.alien.store.service.PlatformShopMembershipAdminMutationService;
+import shop.alien.store.service.PlatformShopMembershipAdminQueryService;
+
+import javax.validation.Valid;
+
+/**
+ * 平台端 — 已开通会员卡的店铺列表
+ */
+@Slf4j
+@Api(tags = {"平台-店铺会员卡商户列表"})
+@CrossOrigin
+@RestController
+@RequestMapping("/platformShopMembershipAdmin")
+@RequiredArgsConstructor
+public class PlatformShopMembershipAdminController {
+
+    private final PlatformShopMembershipAdminQueryService adminQueryService;
+    private final PlatformShopMembershipAdminMutationService adminMutationService;
+
+    @ApiOperation("分页查询(门店名称/联系人/电话模糊;状态:0正常 1即将到期(一个月内) 2已过期)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "keyword", value = "门店名、联系人、电话", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "membershipUiStatus", value = "0正常 1即将到期(一个月内) 2已过期", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "expiryStart", value = "到期时间起 yyyy-MM-dd", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "expiryEnd", value = "到期时间止 yyyy-MM-dd", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "pageNum", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "pageSize", paramType = "query", dataType = "int")
+    })
+    @GetMapping("/storePage")
+    public R<IPage<PlatformShopMembershipAdminRowVo>> storePage(
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer membershipUiStatus,
+            @RequestParam(required = false) String expiryStart,
+            @RequestParam(required = false) String expiryEnd,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize) {
+        return R.data(adminQueryService.pageStores(keyword, membershipUiStatus, expiryStart, expiryEnd, pageNum, pageSize));
+    }
+
+    @ApiOperation("导出 Excel(筛选条件与分页列表一致,导出全部匹配行)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "keyword", value = "门店名、联系人、电话", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "membershipUiStatus", value = "0正常 1即将到期(一个月内) 2已过期", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "expiryStart", value = "到期时间起 yyyy-MM-dd", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "expiryEnd", value = "到期时间止 yyyy-MM-dd", paramType = "query", dataType = "String")
+    })
+    @GetMapping("/storeExport")
+    public ResponseEntity<byte[]> storeExport(
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer membershipUiStatus,
+            @RequestParam(required = false) String expiryStart,
+            @RequestParam(required = false) String expiryEnd) {
+        return adminQueryService.exportStores(keyword, membershipUiStatus, expiryStart, expiryEnd);
+    }
+
+    @ApiOperation("逻辑删除店铺会员卡(单个传 1 个 storeId;批量传多个。仅删除 platform_shop_membership 当前权益记录,不删订单)")
+    @PostMapping("/membership/logicDelete")
+    public R<Integer> logicDeleteMembership(@Valid @RequestBody PlatformShopMembershipDeleteRequest body) {
+        try {
+            return R.data(adminMutationService.logicDeleteByStoreIds(body.getStoreIds()));
+        } catch (IllegalArgumentException e) {
+            return R.fail(e.getMessage());
+        }
+    }
+}

+ 83 - 0
alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipController.java

@@ -0,0 +1,83 @@
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+import shop.alien.entity.store.dto.PlatformShopMembershipOrderCreateRequest;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderCreateResultVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderItemVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipRemainingDetailVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipStatusVo;
+import shop.alien.store.service.PlatformShopMembershipOrderService;
+import shop.alien.store.service.PlatformShopMembershipPlanService;
+import shop.alien.store.service.PlatformShopMembershipService;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 店铺端 — 平台会员卡(开通/续费、状态、购买记录)
+ */
+@Slf4j
+@Api(tags = {"店铺-平台会员卡"})
+@RestController
+@RequestMapping("/platform-shop-membership")
+@RequiredArgsConstructor
+public class PlatformShopMembershipController {
+
+    private final PlatformShopMembershipPlanService planService;
+    private final PlatformShopMembershipService membershipService;
+    private final PlatformShopMembershipOrderService orderService;
+
+    @ApiOperation("可购买的会员卡方案列表")
+    @GetMapping("/plans")
+    public R<List<PlatformShopMembershipPlan>> listPlans() {
+        return R.data(planService.listEnabledForStore());
+    }
+
+    @ApiOperation("当前门店会员状态(含剩余天数)")
+    @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int")
+    @GetMapping("/status")
+    public R<PlatformShopMembershipStatusVo> status(@RequestParam Integer storeId) {
+        return R.data(membershipService.buildStatusVo(storeId));
+    }
+
+    @ApiOperation("会员卡剩余天数、当前连续购买周期起止(yyyy/MM/dd)及周期总天数")
+    @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int")
+    @GetMapping("/remaining-detail")
+    public R<PlatformShopMembershipRemainingDetailVo> remainingDetail(@RequestParam Integer storeId) {
+        return R.data(membershipService.buildRemainingDetailVo(storeId));
+    }
+
+    @ApiOperation("购买记录分页")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "pageNum", value = "页码", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "pageSize", value = "每页条数", paramType = "query", dataType = "int")
+    })
+    @GetMapping("/history")
+    public R<IPage<PlatformShopMembershipOrderItemVo>> history(
+            @RequestParam Integer storeId,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize) {
+        return R.data(orderService.pageHistory(storeId, pageNum, pageSize));
+    }
+
+    @ApiOperation("创建待支付订单(随后调用 /platform-shop-membership/payment/prePay)")
+    @PostMapping("/order/create")
+    public R<PlatformShopMembershipOrderCreateResultVo> createOrder(@Valid @RequestBody PlatformShopMembershipOrderCreateRequest request) {
+        try {
+            return R.data(orderService.createPendingOrder(request));
+        } catch (IllegalArgumentException e) {
+            log.warn("创建会员卡订单失败: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        }
+    }
+}

+ 106 - 0
alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipPaymentController.java

@@ -0,0 +1,106 @@
+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.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.PlatformShopMembershipPaymentOrder;
+import shop.alien.store.service.PlatformMembershipPaymentQueryService;
+import shop.alien.store.service.PlatformShopMembershipPaymentOrderService;
+import shop.alien.store.strategy.platformMembershipPayment.PlatformMembershipPaymentStrategy;
+import shop.alien.store.strategy.platformMembershipPayment.PlatformMembershipPaymentStrategyFactory;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Map;
+
+/**
+ * 店铺端 — 平台会员卡支付(与 /user/reservation/payment 平行的新接口)
+ */
+@Slf4j
+@Api(tags = {"店铺-平台会员卡支付"})
+@RestController
+@RequestMapping("/platform-shop-membership/payment")
+@RequiredArgsConstructor
+public class PlatformShopMembershipPaymentController {
+
+    private final PlatformMembershipPaymentStrategyFactory strategyFactory;
+    private final PlatformMembershipPaymentQueryService platformMembershipPaymentQueryService;
+    private final PlatformShopMembershipPaymentOrderService platformShopMembershipPaymentOrderService;
+
+    @Value("${platform.shop-membership.payment.test-refund-enabled:false}")
+    private boolean testRefundEnabled;
+
+    @ApiOperation("会员卡-创建预支付")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "orderId", value = "会员卡订单ID platform_shop_membership_order.id", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "amountYuan", value = "支付金额(元)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "subject", value = "订单描述", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "userId", value = "门店用户ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "payType", value = "alipay / wechatPay", paramType = "query", dataType = "String")
+    })
+    @PostMapping("/prePay")
+    public R<Map<String, Object>> prePay(
+            @RequestParam Integer storeId,
+            @RequestParam Integer orderId,
+            @RequestParam String amountYuan,
+            @RequestParam String subject,
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("PlatformShopMembershipPaymentController.prePay storeId={}, orderId={}, payType={}", storeId, orderId, payType);
+        PlatformMembershipPaymentStrategy strategy = strategyFactory.getStrategy(payType);
+        return strategy.createPrePay(storeId, orderId, amountYuan, subject, userId);
+    }
+
+    @ApiOperation("会员卡-查询支付状态")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "outTradeNo", value = "商户订单号", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "alipay / wechatPay", paramType = "query", dataType = "String")
+    })
+    @GetMapping("/queryStatus")
+    public R<Object> queryStatus(
+            @RequestParam Integer storeId,
+            @RequestParam String outTradeNo,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("PlatformShopMembershipPaymentController.queryStatus storeId={}, outTradeNo={}", storeId, outTradeNo);
+        return platformMembershipPaymentQueryService.queryPayStatusWithRetry(storeId, outTradeNo, payType);
+    }
+
+    @ApiOperation(value = "会员卡-测试退款", notes = "联调/测试:传会员卡业务订单ID + 支付方式;按支付单全额退款。成功后回写支付单与业务订单为已退款(不自动调整店铺会员到期)。需 platform.shop-membership.payment.test-refund-enabled=true")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "orderId", value = "会员卡业务订单ID platform_shop_membership_order.id", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "payType", value = "alipay / wechatPay", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "refundReason", value = "退款原因", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "outTradeNo", value = "可选;指定该订单下某笔已支付流水;不传则取该支付方式最近一笔已支付单", paramType = "query", dataType = "String")
+    })
+    @PostMapping("/test/refund")
+    public R<String> testRefund(
+            @RequestParam Integer orderId,
+            @RequestParam String payType,
+            @RequestParam(required = false) String refundReason,
+            @RequestParam(required = false) String outTradeNo) {
+        if (!testRefundEnabled) {
+            return R.fail("测试退款未开启,请在配置中设置 platform.shop-membership.payment.test-refund-enabled=true");
+        }
+        PlatformShopMembershipPaymentOrder po = platformShopMembershipPaymentOrderService.findPaidPaymentForMembershipTestRefund(
+                orderId, payType, outTradeNo);
+        if (po == null) {
+            return R.fail("会员卡订单不存在,或不存在符合条件的已支付流水(可传 outTradeNo 指定具体支付单)");
+        }
+        if (po.getPayAmount() == null || po.getPayAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("支付单金额异常,无法退款");
+        }
+        String refundAmountYuan = po.getPayAmount().setScale(2, RoundingMode.HALF_UP).toPlainString();
+        log.warn("PlatformShopMembershipPaymentController.testRefund storeId={}, orderId={}, outTradeNo={}, payType={}, refundAmountYuan={}",
+                po.getStoreId(), orderId, po.getOutTradeNo(), payType, refundAmountYuan);
+        PlatformMembershipPaymentStrategy strategy = strategyFactory.getStrategy(payType);
+        return strategy.refundForTest(po, refundAmountYuan, refundReason);
+    }
+}

+ 87 - 0
alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipPlanPlatformController.java

@@ -0,0 +1,87 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+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.entity.store.PlatformShopMembershipPlan;
+import shop.alien.entity.store.dto.PlatformShopMembershipPlanDeleteRequest;
+import shop.alien.store.service.PlatformShopMembershipPlanPlatformService;
+
+import javax.validation.Valid;
+
+/**
+ * 平台端 — 会员卡方案配置
+ */
+@Slf4j
+@Api(tags = {"平台-店铺会员卡方案"})
+@CrossOrigin
+@RestController
+@RequestMapping("/platformShopMembershipPlan")
+@RequiredArgsConstructor
+public class PlatformShopMembershipPlanPlatformController {
+
+    private final PlatformShopMembershipPlanPlatformService planPlatformService;
+
+    @ApiOperation("分页查询方案")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "planName", value = "会员卡名称,模糊匹配,可选", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "enabled", value = "是否上架 0/1,空为全部", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "pageNum", paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "pageSize", paramType = "query", dataType = "int")
+    })
+    @GetMapping("/page")
+    public R<IPage<PlatformShopMembershipPlan>> page(
+            @RequestParam(required = false) String planName,
+            @RequestParam(required = false) Integer enabled,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize) {
+        return R.data(planPlatformService.pagePlans(planName, enabled, pageNum, pageSize));
+    }
+
+    @ApiOperation("新增或修改方案")
+    @PostMapping("/saveOrUpdate")
+    public R<Boolean> saveOrUpdate(@RequestBody PlatformShopMembershipPlan plan) {
+        try {
+            return R.data(planPlatformService.saveOrUpdatePlan(plan));
+        } catch (IllegalArgumentException e) {
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("上架/下架")
+    @PostMapping("/toggleEnabled")
+    public R<Boolean> toggleEnabled(@RequestParam Integer id, @RequestParam Integer enabled) {
+        boolean ok = planPlatformService.toggleEnabled(id, enabled);
+        return ok ? R.data(true) : R.fail("操作失败");
+    }
+
+    @ApiOperation("推荐/取消推荐")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "方案ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "recommended", value = "是否推荐 0否 1是", required = true, paramType = "query", dataType = "int")
+    })
+    @GetMapping("/toggleRecommended")
+    public R<Boolean> toggleRecommended(@RequestParam Integer id, @RequestParam Integer recommended) {
+        boolean ok = planPlatformService.toggleRecommended(id, recommended);
+        return ok ? R.data(true) : R.fail("操作失败");
+    }
+
+    @ApiOperation("详情")
+    @GetMapping("/detail")
+    public R<PlatformShopMembershipPlan> detail(@RequestParam Integer id) {
+        return R.data(planPlatformService.getById(id));
+    }
+
+    @ApiOperation("逻辑删除方案(按方案 id,单个或批量;仅标记 delete_flag,不物理删)")
+    @PostMapping("/logicDelete")
+    public R<Integer> logicDelete(@Valid @RequestBody PlatformShopMembershipPlanDeleteRequest body) {
+        try {
+            return R.data(planPlatformService.logicDeleteByIds(body.getIds()));
+        } catch (IllegalArgumentException e) {
+            return R.fail(e.getMessage());
+        }
+    }
+}

+ 23 - 0
alien-store/src/main/java/shop/alien/store/platformmembership/PlatformShopMembershipDateUtil.java

@@ -0,0 +1,23 @@
+package shop.alien.store.platformmembership;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 会员周期按自然月累加(与 Calendar.MONTH 行为一致)
+ */
+public final class PlatformShopMembershipDateUtil {
+
+    private PlatformShopMembershipDateUtil() {
+    }
+
+    public static Date addMonths(Date from, int months) {
+        if (from == null) {
+            return null;
+        }
+        Calendar c = Calendar.getInstance();
+        c.setTime(from);
+        c.add(Calendar.MONTH, months);
+        return c.getTime();
+    }
+}

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

@@ -0,0 +1,11 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+
+/**
+ * 会员卡支付查单(带重试,与 MerchantPaymentQueryService 平行)
+ */
+public interface PlatformMembershipPaymentQueryService {
+
+    R<Object> queryPayStatusWithRetry(Integer storeId, String outTradeNo, String payType);
+}

+ 16 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipAdminMutationService.java

@@ -0,0 +1,16 @@
+package shop.alien.store.service;
+
+import java.util.List;
+
+/**
+ * 平台端 — 店铺平台会员卡写操作(逻辑删除等)
+ */
+public interface PlatformShopMembershipAdminMutationService {
+
+    /**
+     * 按门店 ID 逻辑删除会员卡记录(platform_shop_membership,{@link com.baomidou.mybatisplus.annotation.TableLogic})。
+     *
+     * @return 实际删除(标记)的记录条数;门店无会员记录则不计入
+     */
+    int logicDeleteByStoreIds(List<Integer> storeIds);
+}

+ 23 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipAdminQueryService.java

@@ -0,0 +1,23 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import org.springframework.http.ResponseEntity;
+import shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo;
+
+public interface PlatformShopMembershipAdminQueryService {
+
+    IPage<PlatformShopMembershipAdminRowVo> pageStores(
+            String keyword,
+            Integer membershipUiStatus,
+            String expiryStart,
+            String expiryEnd,
+            int pageNum,
+            int pageSize);
+
+    /** 导出与分页列表相同筛选条件下的全部记录为 xlsx。 */
+    ResponseEntity<byte[]> exportStores(
+            String keyword,
+            Integer membershipUiStatus,
+            String expiryStart,
+            String expiryEnd);
+}

+ 17 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderCreateDebounceService.java

@@ -0,0 +1,17 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderCreateResultVo;
+
+import java.util.Optional;
+
+/**
+ * 同一门店 + 方案在 15 分钟内重复创建订单时返回缓存结果(不按付款人区分);支付成功或关单后清除。
+ */
+public interface PlatformShopMembershipOrderCreateDebounceService {
+
+    Optional<PlatformShopMembershipOrderCreateResultVo> getReusablePendingOrder(Integer storeId, Integer planId);
+
+    void rememberPendingOrder(Integer storeId, Integer planId, PlatformShopMembershipOrderCreateResultVo vo);
+
+    void clearPendingSlot(Integer storeId, Integer planId);
+}

+ 17 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderPaymentTimeoutService.java

@@ -0,0 +1,17 @@
+package shop.alien.store.service;
+
+/**
+ * 会员卡订单支付超时:通过 Redis key TTL 触发关单(需 Redis 开启 key 过期通知)。
+ */
+public interface PlatformShopMembershipOrderPaymentTimeoutService {
+
+    /**
+     * 创建待支付单成功后调用,15 分钟内未支付则关闭订单。
+     */
+    void scheduleClosePendingOrder(Integer orderId);
+
+    /**
+     * 支付成功时取消超时监听,避免误关单。
+     */
+    void cancelClosePendingOrder(Integer orderId);
+}

+ 30 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderService.java

@@ -0,0 +1,30 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.dto.PlatformShopMembershipOrderCreateRequest;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderCreateResultVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderItemVo;
+
+public interface PlatformShopMembershipOrderService extends IService<PlatformShopMembershipOrder> {
+
+    /**
+     * 创建待支付会员卡订单。完整流程、防抖与 Redis 行为见实现类
+     * {@link shop.alien.store.service.impl.PlatformShopMembershipOrderServiceImpl#createPendingOrder}。
+     */
+    PlatformShopMembershipOrderCreateResultVo createPendingOrder(PlatformShopMembershipOrderCreateRequest request);
+
+    IPage<PlatformShopMembershipOrderItemVo> pageHistory(Integer storeId, int pageNum, int pageSize);
+
+    /**
+     * 支付查单/回调成功后更新订单与店铺会员期限(幂等)
+     */
+    void applyPaySuccess(Integer membershipOrderId, java.util.Date payTime, String outTradeNo,
+                         String tradeNo, String paymentMethod);
+
+    /**
+     * 解析当前会员到期日对应的已支付订单(优先 validity_end 与会员 expiry 一致的最新一单,否则取最近已支付单)
+     */
+    PlatformShopMembershipOrder findPaidOrderForCurrentMembershipExpiry(Integer storeId, java.util.Date membershipExpiryDate);
+}

+ 33 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPaymentOrderService.java

@@ -0,0 +1,33 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.PlatformShopMembershipPaymentOrder;
+
+/**
+ * 平台店铺会员卡支付单(独立表)
+ */
+public interface PlatformShopMembershipPaymentOrderService extends IService<PlatformShopMembershipPaymentOrder> {
+
+    PlatformShopMembershipPaymentOrder getByOutTradeNo(String outTradeNo);
+
+    String generatePaymentNo();
+
+    /**
+     * 同一会员卡订单重新预支付时,逻辑删除该订单下指定支付方式的待支付单
+     */
+    int logicDeletePendingByMembershipOrderIdAndPayType(Integer membershipOrderId, String payType);
+
+    /**
+     * 测试退款:根据会员卡业务订单 ID 解析「已支付」流水(门店从订单读取)。
+     * 若 {@code outTradeNo} 非空则必须与该订单、支付方式一致且已支付;否则取该订单该支付方式下最近一笔已支付单。
+     *
+     * @return 匹配到的支付单;无匹配时返回 null
+     */
+    PlatformShopMembershipPaymentOrder findPaidPaymentForMembershipTestRefund(
+            Integer membershipOrderId, String payType, String outTradeNo);
+
+    /**
+     * 退款成功后:支付单标为已退款,对应会员卡业务订单标为已退款(不调整 platform_shop_membership 权益周期,需运营另行处理)。
+     */
+    void markPaymentAndMembershipOrderRefunded(PlatformShopMembershipPaymentOrder paymentOrder);
+}

+ 26 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPlanPlatformService.java

@@ -0,0 +1,26 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+
+import java.util.List;
+
+public interface PlatformShopMembershipPlanPlatformService extends IService<PlatformShopMembershipPlan> {
+
+    IPage<PlatformShopMembershipPlan> pagePlans(String planName, Integer enabled, int pageNum, int pageSize);
+
+    boolean saveOrUpdatePlan(PlatformShopMembershipPlan plan);
+
+    boolean toggleEnabled(Integer id, Integer enabled);
+
+    /** recommended:0 否 1 是 */
+    boolean toggleRecommended(Integer id, Integer recommended);
+
+    /**
+     * 按方案主键 id 逻辑删除({@link com.baomidou.mybatisplus.annotation.TableLogic}),支持单个与批量。
+     *
+     * @return 实际删除(标记)条数;已删除或不存在的 id 不计入
+     */
+    int logicDeleteByIds(List<Integer> ids);
+}

+ 16 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPlanService.java

@@ -0,0 +1,16 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+
+import java.util.List;
+
+public interface PlatformShopMembershipPlanService extends IService<PlatformShopMembershipPlan> {
+
+    /**
+     * 店铺端:已上架方案列表
+     */
+    List<PlatformShopMembershipPlan> listEnabledForStore();
+
+    PlatformShopMembershipPlan getEnabledById(Integer planId);
+}

+ 18 - 0
alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipService.java

@@ -0,0 +1,18 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.PlatformShopMembership;
+import shop.alien.entity.store.vo.PlatformShopMembershipRemainingDetailVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipStatusVo;
+
+public interface PlatformShopMembershipService extends IService<PlatformShopMembership> {
+
+    PlatformShopMembership getByStoreId(Integer storeId);
+
+    PlatformShopMembershipStatusVo buildStatusVo(Integer storeId);
+
+    /**
+     * 按门店查询:剩余天数、当前连续购买周期起止(年/月/日)及该周期总天数
+     */
+    PlatformShopMembershipRemainingDetailVo buildRemainingDetailVo(Integer storeId);
+}

+ 5 - 0
alien-store/src/main/java/shop/alien/store/service/TrackEventService.java

@@ -43,4 +43,9 @@ public interface TrackEventService {
      * @return 浏览量
      */
     Long getStoreViewCount(Integer storeId);
+
+    /**
+     * 店铺浏览(访问)次数,区间两端均闭区间,口径与 {@link #getStoreViewCount} 一致
+     */
+    Long countStoreViewEventsBetweenInclusive(Integer storeId, Date startInclusive, Date endInclusive);
 }

+ 85 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformMembershipPaymentQueryServiceImpl.java

@@ -0,0 +1,85 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.PlatformMembershipPaymentQueryService;
+import shop.alien.store.strategy.platformMembershipPayment.PlatformMembershipPaymentStrategy;
+import shop.alien.store.strategy.platformMembershipPayment.PlatformMembershipPaymentStrategyFactory;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PlatformMembershipPaymentQueryServiceImpl implements PlatformMembershipPaymentQueryService {
+
+    private static final int MAX_ATTEMPTS = 3;
+    private static final long RETRY_DELAY_MS = 1500L;
+
+    private static final String[] NON_RETRYABLE_MSGS = {
+            "交易已关闭",
+            "等待用户付款",
+            "等待买家付款",
+            "交易不存在",
+            "会员卡订单不存在",
+            "支付单不存在",
+            "该门店未配置支付参数",
+            "门店ID和商户订单号不能为空",
+            "支付成功"
+    };
+
+    private final PlatformMembershipPaymentStrategyFactory factory;
+
+    @Override
+    public R<Object> queryPayStatusWithRetry(Integer storeId, String outTradeNo, String payType) {
+        PlatformMembershipPaymentStrategy strategy = factory.getStrategy(payType);
+        R<Object> last = null;
+        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
+            last = strategy.queryPayStatus(storeId, outTradeNo);
+            if (last == null) {
+                if (attempt < MAX_ATTEMPTS) {
+                    sleep(RETRY_DELAY_MS);
+                }
+                continue;
+            }
+            if (R.isSuccess(last)) {
+                return last;
+            }
+            String msg = last.getMsg();
+            if (msg != null && isNonRetryable(msg)) {
+                return last;
+            }
+            if (attempt < MAX_ATTEMPTS) {
+                log.warn("会员卡支付查单可重试失败 attempt={}/{} storeId={} outTradeNo={} msg={}",
+                        attempt, MAX_ATTEMPTS, storeId, outTradeNo, msg);
+                sleep(RETRY_DELAY_MS);
+            }
+        }
+        if (last != null && !R.isSuccess(last)) {
+            String msg = last.getMsg();
+            if (msg != null && (msg.contains("查询失败") || msg.contains("查询异常"))) {
+                return R.fail("查询失败:网络异常,请稍后重试或到订单列表查看");
+            }
+        }
+        return last != null ? last : R.fail("查询失败:请稍后重试");
+    }
+
+    private static boolean isNonRetryable(String msg) {
+        for (String s : NON_RETRYABLE_MSGS) {
+            if (StringUtils.contains(msg, s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void sleep(long ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("PlatformMembershipPaymentQueryService sleep interrupted");
+        }
+    }
+}

+ 40 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipAdminMutationServiceImpl.java

@@ -0,0 +1,40 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.PlatformShopMembership;
+import shop.alien.mapper.PlatformShopMembershipMapper;
+import shop.alien.store.service.PlatformShopMembershipAdminMutationService;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+public class PlatformShopMembershipAdminMutationServiceImpl
+        extends ServiceImpl<PlatformShopMembershipMapper, PlatformShopMembership>
+        implements PlatformShopMembershipAdminMutationService {
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int logicDeleteByStoreIds(List<Integer> storeIds) {
+        if (storeIds == null || storeIds.isEmpty()) {
+            throw new IllegalArgumentException("门店ID列表不能为空");
+        }
+        List<Integer> ids = storeIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (ids.isEmpty()) {
+            throw new IllegalArgumentException("门店ID列表不能为空");
+        }
+        LambdaQueryWrapper<PlatformShopMembership> w = new LambdaQueryWrapper<>();
+        w.in(PlatformShopMembership::getStoreId, ids);
+        List<PlatformShopMembership> list = list(w);
+        if (list.isEmpty()) {
+            return 0;
+        }
+        List<Integer> pkIds = list.stream().map(PlatformShopMembership::getId).collect(Collectors.toList());
+        removeByIds(pkIds);
+        return list.size();
+    }
+}

+ 57 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipAdminQueryServiceImpl.java

@@ -0,0 +1,57 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.excelVo.util.ExcelExporter;
+import shop.alien.entity.store.vo.PlatformShopMembershipAdminRowVo;
+import shop.alien.mapper.PlatformShopMembershipAdminMapper;
+import shop.alien.store.service.PlatformShopMembershipAdminQueryService;
+
+import java.net.URLEncoder;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class PlatformShopMembershipAdminQueryServiceImpl implements PlatformShopMembershipAdminQueryService {
+
+    private final PlatformShopMembershipAdminMapper platformShopMembershipAdminMapper;
+
+    @Override
+    public IPage<PlatformShopMembershipAdminRowVo> pageStores(
+            String keyword,
+            Integer membershipUiStatus,
+            String expiryStart,
+            String expiryEnd,
+            int pageNum,
+            int pageSize) {
+        Page<PlatformShopMembershipAdminRowVo> p = new Page<>(pageNum, pageSize);
+        return platformShopMembershipAdminMapper.selectStoreMembershipPage(p, keyword, membershipUiStatus, expiryStart, expiryEnd);
+    }
+
+    @Override
+    public ResponseEntity<byte[]> exportStores(
+            String keyword,
+            Integer membershipUiStatus,
+            String expiryStart,
+            String expiryEnd) {
+        try {
+            List<PlatformShopMembershipAdminRowVo> list =
+                    platformShopMembershipAdminMapper.selectStoreMembershipList(keyword, membershipUiStatus, expiryStart, expiryEnd);
+            String[] headers = {"商家名称", "联系人", "联系方式", "开通日期", "到期日期", "状态"};
+            byte[] excelData = ExcelExporter.exportToExcel(list, headers, "已购买会员商家列表");
+            HttpHeaders responseHeaders = new HttpHeaders();
+            responseHeaders.add(
+                    "Content-Disposition",
+                    "attachment; filename=\"" + URLEncoder.encode("已购买会员商家列表.xlsx", "UTF-8") + "\";charset=utf-8");
+            responseHeaders.set("Charset", "UTF-8");
+            return new ResponseEntity<>(excelData, responseHeaders, HttpStatus.OK);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 88 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderCreateDebounceServiceImpl.java

@@ -0,0 +1,88 @@
+package shop.alien.store.service.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderCreateResultVo;
+import shop.alien.mapper.PlatformShopMembershipOrderMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.PlatformShopMembershipOrderCreateDebounceService;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PlatformShopMembershipOrderCreateDebounceServiceImpl
+        implements PlatformShopMembershipOrderCreateDebounceService {
+
+    static final String REDIS_KEY_PREFIX = "platform:membership:order:create:debounce:";
+    private static final long DEBOUNCE_TTL_SECONDS = 15 * 60;
+
+    private final BaseRedisService redisService;
+    private final ObjectMapper objectMapper;
+    private final PlatformShopMembershipOrderMapper orderMapper;
+
+    static String debounceRedisKey(Integer storeId, Integer planId) {
+        return REDIS_KEY_PREFIX + storeId + ":" + planId;
+    }
+
+    @Override
+    public Optional<PlatformShopMembershipOrderCreateResultVo> getReusablePendingOrder(
+            Integer storeId, Integer planId) {
+        if (storeId == null || planId == null) {
+            return Optional.empty();
+        }
+        String key = debounceRedisKey(storeId, planId);
+        String json = redisService.getString(key);
+        if (json == null || json.isEmpty()) {
+            return Optional.empty();
+        }
+        PlatformShopMembershipOrderCreateResultVo vo;
+        try {
+            vo = objectMapper.readValue(json, PlatformShopMembershipOrderCreateResultVo.class);
+        } catch (JsonProcessingException e) {
+            log.warn("会员卡创建防抖缓存解析失败, key={}", key, e);
+            redisService.delete(key);
+            return Optional.empty();
+        }
+        if (vo == null || vo.getOrderId() == null) {
+            redisService.delete(key);
+            return Optional.empty();
+        }
+        PlatformShopMembershipOrder order = orderMapper.selectById(vo.getOrderId());
+        if (order == null
+                || !Objects.equals(order.getPayStatus(), PlatformShopMembershipOrder.PAY_STATUS_PENDING)
+                || !Objects.equals(order.getStoreId(), storeId)
+                || !Objects.equals(order.getPlanId(), planId)) {
+            redisService.delete(key);
+            return Optional.empty();
+        }
+        return Optional.of(vo);
+    }
+
+    @Override
+    public void rememberPendingOrder(Integer storeId, Integer planId, PlatformShopMembershipOrderCreateResultVo vo) {
+        if (storeId == null || planId == null || vo == null) {
+            return;
+        }
+        String key = debounceRedisKey(storeId, planId);
+        try {
+            redisService.setString(key, objectMapper.writeValueAsString(vo), DEBOUNCE_TTL_SECONDS);
+        } catch (JsonProcessingException e) {
+            log.error("会员卡创建防抖写入 Redis 失败, storeId={}, planId={}", storeId, planId, e);
+        }
+    }
+
+    @Override
+    public void clearPendingSlot(Integer storeId, Integer planId) {
+        if (storeId == null || planId == null) {
+            return;
+        }
+        redisService.delete(debounceRedisKey(storeId, planId));
+    }
+}

+ 83 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderPaymentTimeoutServiceImpl.java

@@ -0,0 +1,83 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.mapper.PlatformShopMembershipOrderMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.listener.RedisKeyExpirationHandler;
+import shop.alien.store.service.PlatformShopMembershipOrderCreateDebounceService;
+import shop.alien.store.service.PlatformShopMembershipOrderPaymentTimeoutService;
+
+import javax.annotation.PostConstruct;
+import java.util.Date;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PlatformShopMembershipOrderPaymentTimeoutServiceImpl
+        implements PlatformShopMembershipOrderPaymentTimeoutService {
+
+    /** 与律师咨询订单超时机制一致,依赖 Redis notify-keyspace-events 含 Ex */
+    public static final String REDIS_KEY_PREFIX = "platform:membership:order:payment:timeout:";
+
+    private static final long TIMEOUT_SECONDS = 15 * 60;
+
+    private final BaseRedisService redisService;
+    private final RedisKeyExpirationHandler expirationHandler;
+    private final PlatformShopMembershipOrderMapper orderMapper;
+    private final PlatformShopMembershipOrderCreateDebounceService orderCreateDebounceService;
+
+    @PostConstruct
+    public void init() {
+        expirationHandler.registerHandler(REDIS_KEY_PREFIX, this::onExpiredKey);
+    }
+
+    private void onExpiredKey(String expiredKey) {
+        String suffix = expiredKey.substring(REDIS_KEY_PREFIX.length());
+        Integer orderId;
+        try {
+            orderId = Integer.valueOf(suffix);
+        } catch (NumberFormatException e) {
+            log.warn("会员卡订单超时 key 格式无效: {}", expiredKey);
+            return;
+        }
+        try {
+            LambdaUpdateWrapper<PlatformShopMembershipOrder> u = new LambdaUpdateWrapper<>();
+            u.eq(PlatformShopMembershipOrder::getId, orderId);
+            u.eq(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_PENDING);
+            u.set(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_CLOSED);
+            u.set(PlatformShopMembershipOrder::getUpdatedTime, new Date());
+            int rows = orderMapper.update(null, u);
+            if (rows > 0) {
+                log.info("会员卡订单超时未支付已关闭, orderId={}", orderId);
+                PlatformShopMembershipOrder closed = orderMapper.selectById(orderId);
+                if (closed != null) {
+                    orderCreateDebounceService.clearPendingSlot(closed.getStoreId(), closed.getPlanId());
+                }
+            }
+        } catch (Exception e) {
+            log.error("会员卡订单超时关单失败, orderId={}", orderId, e);
+        }
+    }
+
+    @Override
+    public void scheduleClosePendingOrder(Integer orderId) {
+        if (orderId == null) {
+            return;
+        }
+        String key = REDIS_KEY_PREFIX + orderId;
+        redisService.setString(key, String.valueOf(orderId), TIMEOUT_SECONDS);
+        log.info("会员卡订单支付超时监听已设置, orderId={}, ttlSeconds={}", orderId, TIMEOUT_SECONDS);
+    }
+
+    @Override
+    public void cancelClosePendingOrder(Integer orderId) {
+        if (orderId == null) {
+            return;
+        }
+        redisService.delete(REDIS_KEY_PREFIX + orderId);
+    }
+}

+ 227 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderServiceImpl.java

@@ -0,0 +1,227 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.PlatformShopMembership;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+import shop.alien.entity.store.dto.PlatformShopMembershipOrderCreateRequest;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderCreateResultVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipOrderItemVo;
+import shop.alien.mapper.PlatformShopMembershipMapper;
+import shop.alien.mapper.PlatformShopMembershipOrderMapper;
+import shop.alien.store.platformmembership.PlatformShopMembershipDateUtil;
+import shop.alien.store.service.PlatformShopMembershipOrderCreateDebounceService;
+import shop.alien.store.service.PlatformShopMembershipOrderPaymentTimeoutService;
+import shop.alien.store.service.PlatformShopMembershipOrderService;
+import shop.alien.store.service.PlatformShopMembershipPlanService;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class PlatformShopMembershipOrderServiceImpl
+        extends ServiceImpl<PlatformShopMembershipOrderMapper, PlatformShopMembershipOrder>
+        implements PlatformShopMembershipOrderService {
+
+    private final PlatformShopMembershipPlanService planService;
+    private final PlatformShopMembershipMapper membershipMapper;
+    private final PlatformShopMembershipOrderPaymentTimeoutService orderPaymentTimeoutService;
+    private final PlatformShopMembershipOrderCreateDebounceService orderCreateDebounceService;
+
+    /**
+     * 创建待支付的会员卡订单(店铺端)。
+     * <p>
+     * <b>业务目标</b>:在方案有效的前提下,为指定门店生成一笔待支付订单,供后续拉起微信/支付宝等支付;
+     * 同一门店 + 同一方案在 15 分钟内重复调用时,不重复落库,直接返回 Redis 中缓存的创建结果。
+     * </p>
+     * <p>
+     * <b>防抖(Redis)</b>:
+     * <ul>
+     *   <li>Redis Key:{@code platform:membership:order:create:debounce:} + storeId + ":" + planId;TTL 15 分钟。</li>
+     *   <li>命中缓存时仍会查库校验:订单存在、支付状态仍为「待支付」、门店与方案与请求一致;否则删除脏缓存并走新建流程。</li>
+     *   <li>缓存由「新建成功」写入;支付成功、支付超时关单、或本方法内关闭同店其他待支付单时,会清除对应槽位(避免指向已关闭订单)。</li>
+     * </ul>
+     * </p>
+     * <p>
+     * <b>同店待支付互斥</b>:新建前会关闭该门店下所有其他「待支付」会员卡订单(避免多笔待支付并存),并清除这些订单在防抖 Redis 上的槽位。
+     * </p>
+     * <p>
+     * <b>支付超时</b>:新建后为该订单注册 Redis 支付超时监听(15 分钟未支付则关单,依赖 Redis key 过期通知配置)。
+     * </p>
+     *
+     * @param request 门店 ID、方案 ID、付款用户 ID(写入订单;防抖不按付款人区分)
+     * @return 订单 ID、订单号、应付金额等,供前端发起支付
+     * @throws IllegalArgumentException 方案不存在或已下架
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PlatformShopMembershipOrderCreateResultVo createPendingOrder(PlatformShopMembershipOrderCreateRequest request) {
+        PlatformShopMembershipPlan plan = planService.getEnabledById(request.getPlanId());
+        if (plan == null) {
+            throw new IllegalArgumentException("会员卡方案不存在或已下架");
+        }
+
+        // 15 分钟内同店同方案:若 Redis 有有效待支付快照,直接返回,不再关单、不再插入新订单
+        Optional<PlatformShopMembershipOrderCreateResultVo> cached =
+                orderCreateDebounceService.getReusablePendingOrder(request.getStoreId(), request.getPlanId());
+        if (cached.isPresent()) {
+            return cached.get();
+        }
+
+        // 无可用缓存:关闭本店其他待支付单并清防抖槽位,再创建新订单
+        closeOtherPendingOrders(request.getStoreId());
+
+        PlatformShopMembershipOrder order = new PlatformShopMembershipOrder();
+        order.setOrderSn("PM" + UniqueRandomNumGenerator.generateUniqueCode(16));
+        order.setStoreId(request.getStoreId());
+        order.setPlanId(plan.getId());
+        order.setPlanName(plan.getPlanName());
+        order.setDurationMonths(plan.getDurationMonths());
+        order.setPayAmount(plan.getPrice().setScale(2, RoundingMode.HALF_UP));
+        order.setPayStatus(PlatformShopMembershipOrder.PAY_STATUS_PENDING);
+        order.setPayerUserId(request.getPayerUserId());
+        order.setCreatedTime(new Date());
+        order.setUpdatedTime(new Date());
+        save(order);
+        // 15 分钟未支付则通过 Redis 过期事件关单(需服务端开启 key 过期通知)
+        orderPaymentTimeoutService.scheduleClosePendingOrder(order.getId());
+
+        PlatformShopMembershipOrderCreateResultVo vo = new PlatformShopMembershipOrderCreateResultVo();
+        vo.setOrderId(order.getId());
+        vo.setOrderSn(order.getOrderSn());
+        vo.setPayAmount(order.getPayAmount());
+        vo.setPlanName(order.getPlanName());
+        vo.setDurationMonths(order.getDurationMonths());
+        // 写入防抖缓存,短时间内重复创建同店同方案将命中上文分支
+        orderCreateDebounceService.rememberPendingOrder(request.getStoreId(), request.getPlanId(), vo);
+        return vo;
+    }
+
+    @Override
+    public PlatformShopMembershipOrder findPaidOrderForCurrentMembershipExpiry(Integer storeId, Date membershipExpiryDate) {
+        if (storeId == null || membershipExpiryDate == null) {
+            return null;
+        }
+        LambdaQueryWrapper<PlatformShopMembershipOrder> w = new LambdaQueryWrapper<>();
+        w.eq(PlatformShopMembershipOrder::getStoreId, storeId);
+        w.eq(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_PAID);
+        w.eq(PlatformShopMembershipOrder::getValidityEnd, membershipExpiryDate);
+        w.orderByDesc(PlatformShopMembershipOrder::getPayTime).orderByDesc(PlatformShopMembershipOrder::getId);
+        w.last("LIMIT 1");
+        PlatformShopMembershipOrder o = getBaseMapper().selectOne(w);
+        if (o != null) {
+            return o;
+        }
+        LambdaQueryWrapper<PlatformShopMembershipOrder> w2 = new LambdaQueryWrapper<>();
+        w2.eq(PlatformShopMembershipOrder::getStoreId, storeId);
+        w2.eq(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_PAID);
+        w2.orderByDesc(PlatformShopMembershipOrder::getPayTime).orderByDesc(PlatformShopMembershipOrder::getId);
+        w2.last("LIMIT 1");
+        return getBaseMapper().selectOne(w2);
+    }
+
+    private void closeOtherPendingOrders(Integer storeId) {
+        LambdaQueryWrapper<PlatformShopMembershipOrder> q = new LambdaQueryWrapper<>();
+        q.eq(PlatformShopMembershipOrder::getStoreId, storeId);
+        q.eq(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_PENDING);
+        List<PlatformShopMembershipOrder> pendings = list(q);
+        for (PlatformShopMembershipOrder o : pendings) {
+            orderCreateDebounceService.clearPendingSlot(o.getStoreId(), o.getPlanId());
+        }
+        if (pendings.isEmpty()) {
+            return;
+        }
+        LambdaUpdateWrapper<PlatformShopMembershipOrder> u = new LambdaUpdateWrapper<>();
+        u.eq(PlatformShopMembershipOrder::getStoreId, storeId);
+        u.eq(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_PENDING);
+        u.set(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_CLOSED);
+        u.set(PlatformShopMembershipOrder::getUpdatedTime, new Date());
+        update(u);
+    }
+
+    @Override
+    public IPage<PlatformShopMembershipOrderItemVo> pageHistory(Integer storeId, int pageNum, int pageSize) {
+        Page<PlatformShopMembershipOrderItemVo> p = new Page<>(pageNum, pageSize);
+        return baseMapper.selectHistoryPage(p, storeId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void applyPaySuccess(Integer membershipOrderId, Date payTime, String outTradeNo,
+                                String tradeNo, String paymentMethod) {
+        if (membershipOrderId == null || payTime == null) {
+            return;
+        }
+        PlatformShopMembershipOrder order = getById(membershipOrderId);
+        if (order == null || order.getPayStatus() == null) {
+            return;
+        }
+        if (order.getPayStatus() != PlatformShopMembershipOrder.PAY_STATUS_PENDING) {
+            return;
+        }
+
+        orderPaymentTimeoutService.cancelClosePendingOrder(membershipOrderId);
+        orderCreateDebounceService.clearPendingSlot(order.getStoreId(), order.getPlanId());
+
+        LambdaQueryWrapper<PlatformShopMembership> qw = new LambdaQueryWrapper<>();
+        qw.eq(PlatformShopMembership::getStoreId, order.getStoreId());
+        PlatformShopMembership mem = membershipMapper.selectOne(qw);
+
+        Date oldExpiry = mem != null ? mem.getExpiryDate() : null;
+        boolean stillActive = oldExpiry != null && !oldExpiry.before(payTime);
+
+        Date validityStart;
+        Date validityEnd;
+        if (stillActive) {
+            validityStart = oldExpiry;
+            validityEnd = PlatformShopMembershipDateUtil.addMonths(oldExpiry, order.getDurationMonths());
+        } else {
+            validityStart = payTime;
+            validityEnd = PlatformShopMembershipDateUtil.addMonths(payTime, order.getDurationMonths());
+        }
+
+        order.setValidityStart(validityStart);
+        order.setValidityEnd(validityEnd);
+        order.setPayStatus(PlatformShopMembershipOrder.PAY_STATUS_PAID);
+        order.setPayTime(payTime);
+        order.setPaymentMethod(paymentMethod);
+        order.setOutTradeNo(outTradeNo);
+        order.setTradeNo(tradeNo);
+        order.setUpdatedTime(payTime);
+        updateById(order);
+
+        if (mem == null) {
+            mem = new PlatformShopMembership();
+            mem.setStoreId(order.getStoreId());
+            mem.setFirstOpenTime(payTime);
+            mem.setCurrentPeriodStart(validityStart);
+            mem.setExpiryDate(validityEnd);
+            mem.setCurrentPlanId(order.getPlanId());
+            mem.setCreatedTime(payTime);
+            mem.setUpdatedTime(payTime);
+            membershipMapper.insert(mem);
+        } else {
+            if (mem.getFirstOpenTime() == null) {
+                mem.setFirstOpenTime(payTime);
+            }
+            if (!stillActive) {
+                mem.setCurrentPeriodStart(validityStart);
+            }
+            mem.setExpiryDate(validityEnd);
+            mem.setCurrentPlanId(order.getPlanId());
+            mem.setUpdatedTime(payTime);
+            membershipMapper.updateById(mem);
+        }
+    }
+}

+ 121 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPaymentOrderServiceImpl.java

@@ -0,0 +1,121 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.PlatformShopMembershipPaymentOrder;
+import shop.alien.mapper.PlatformShopMembershipOrderMapper;
+import shop.alien.mapper.PlatformShopMembershipPaymentOrderMapper;
+import shop.alien.store.service.PlatformShopMembershipOrderService;
+import shop.alien.store.service.PlatformShopMembershipPaymentOrderService;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.ThreadLocalRandom;
+
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class PlatformShopMembershipPaymentOrderServiceImpl
+        extends ServiceImpl<PlatformShopMembershipPaymentOrderMapper, PlatformShopMembershipPaymentOrder>
+        implements PlatformShopMembershipPaymentOrderService {
+
+    @Resource
+    private PlatformShopMembershipOrderMapper membershipOrderMapper;
+
+    @Resource
+    private PlatformShopMembershipOrderService platformShopMembershipOrderService;
+
+    @Override
+    public PlatformShopMembershipPaymentOrder getByOutTradeNo(String outTradeNo) {
+        if (outTradeNo == null || outTradeNo.trim().isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<PlatformShopMembershipPaymentOrder> w = new LambdaQueryWrapper<>();
+        w.eq(PlatformShopMembershipPaymentOrder::getOutTradeNo, outTradeNo.trim());
+        return getOne(w);
+    }
+
+    @Override
+    public String generatePaymentNo() {
+        String dateStr = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+        int random = ThreadLocalRandom.current().nextInt(10000);
+        return "PMP" + dateStr + String.format("%04d", random);
+    }
+
+    @Override
+    public int logicDeletePendingByMembershipOrderIdAndPayType(Integer membershipOrderId, String payType) {
+        if (membershipOrderId == null || payType == null || payType.trim().isEmpty()) {
+            return 0;
+        }
+        LambdaUpdateWrapper<PlatformShopMembershipPaymentOrder> u = new LambdaUpdateWrapper<>();
+        u.eq(PlatformShopMembershipPaymentOrder::getMembershipOrderId, membershipOrderId);
+        u.eq(PlatformShopMembershipPaymentOrder::getPayType, payType.trim());
+        u.eq(PlatformShopMembershipPaymentOrder::getPayStatus, 0);
+        u.set(PlatformShopMembershipPaymentOrder::getDeleteFlag, 1);
+        u.set(PlatformShopMembershipPaymentOrder::getUpdatedTime, new Date());
+        return baseMapper.update(null, u);
+    }
+
+    @Override
+    public PlatformShopMembershipPaymentOrder findPaidPaymentForMembershipTestRefund(
+            Integer membershipOrderId, String payType, String outTradeNo) {
+        if (membershipOrderId == null || StringUtils.isBlank(payType)) {
+            return null;
+        }
+        PlatformShopMembershipOrder ord = membershipOrderMapper.selectById(membershipOrderId);
+        if (ord == null || ord.getStoreId() == null) {
+            return null;
+        }
+        Integer storeId = ord.getStoreId();
+        String pt = payType.trim();
+        if (StringUtils.isNotBlank(outTradeNo)) {
+            PlatformShopMembershipPaymentOrder po = getByOutTradeNo(outTradeNo.trim());
+            if (po == null) {
+                return null;
+            }
+            if (!membershipOrderId.equals(po.getMembershipOrderId())
+                    || !storeId.equals(po.getStoreId())
+                    || !pt.equals(po.getPayType())) {
+                return null;
+            }
+            if (po.getPayStatus() == null || po.getPayStatus() != 1) {
+                return null;
+            }
+            return po;
+        }
+        LambdaQueryWrapper<PlatformShopMembershipPaymentOrder> w = new LambdaQueryWrapper<>();
+        w.eq(PlatformShopMembershipPaymentOrder::getMembershipOrderId, membershipOrderId);
+        w.eq(PlatformShopMembershipPaymentOrder::getStoreId, storeId);
+        w.eq(PlatformShopMembershipPaymentOrder::getPayType, pt);
+        w.eq(PlatformShopMembershipPaymentOrder::getPayStatus, 1);
+        w.orderByDesc(PlatformShopMembershipPaymentOrder::getPayTime).orderByDesc(PlatformShopMembershipPaymentOrder::getId);
+        w.last("LIMIT 1");
+        return getBaseMapper().selectOne(w);
+    }
+
+    @Override
+    public void markPaymentAndMembershipOrderRefunded(PlatformShopMembershipPaymentOrder paymentOrder) {
+        if (paymentOrder == null || paymentOrder.getId() == null) {
+            return;
+        }
+        Date now = new Date();
+        LambdaUpdateWrapper<PlatformShopMembershipPaymentOrder> uw = new LambdaUpdateWrapper<>();
+        uw.eq(PlatformShopMembershipPaymentOrder::getId, paymentOrder.getId());
+        uw.set(PlatformShopMembershipPaymentOrder::getPayStatus, PlatformShopMembershipPaymentOrder.PAY_STATUS_REFUNDED);
+        uw.set(PlatformShopMembershipPaymentOrder::getUpdatedTime, now);
+        update(uw);
+
+        if (paymentOrder.getMembershipOrderId() != null) {
+            LambdaUpdateWrapper<PlatformShopMembershipOrder> ow = new LambdaUpdateWrapper<>();
+            ow.eq(PlatformShopMembershipOrder::getId, paymentOrder.getMembershipOrderId());
+            ow.set(PlatformShopMembershipOrder::getPayStatus, PlatformShopMembershipOrder.PAY_STATUS_REFUNDED);
+            ow.set(PlatformShopMembershipOrder::getUpdatedTime, now);
+            platformShopMembershipOrderService.update(ow);
+        }
+    }
+}

+ 230 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPlanPlatformServiceImpl.java

@@ -0,0 +1,230 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+import shop.alien.mapper.PlatformShopMembershipPlanMapper;
+import shop.alien.store.service.PlatformShopMembershipPlanPlatformService;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class PlatformShopMembershipPlanPlatformServiceImpl
+        extends ServiceImpl<PlatformShopMembershipPlanMapper, PlatformShopMembershipPlan>
+        implements PlatformShopMembershipPlanPlatformService {
+
+    @Override
+    public IPage<PlatformShopMembershipPlan> pagePlans(String planName, Integer enabled, int pageNum, int pageSize) {
+        Page<PlatformShopMembershipPlan> p = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PlatformShopMembershipPlan> w = new LambdaQueryWrapper<>();
+        if (StringUtils.isNotBlank(planName)) {
+            w.like(PlatformShopMembershipPlan::getPlanName, planName.trim());
+        }
+        if (enabled != null) {
+            w.eq(PlatformShopMembershipPlan::getEnabled, enabled);
+        }
+        w.orderByDesc(PlatformShopMembershipPlan::getRecommended)
+                .orderByAsc(PlatformShopMembershipPlan::getSortOrder)
+                .orderByDesc(PlatformShopMembershipPlan::getId);
+        IPage<PlatformShopMembershipPlan> result = page(p, w);
+        for (PlatformShopMembershipPlan plan : result.getRecords()) {
+            plan.setBenefitCount(countEnabledBenefits(plan));
+        }
+        return result;
+    }
+
+    /**
+     * 统计方案已开启的权益项数量:8 个 0/1 开关为 1 则各计 1;推广位次数、月算力大于 0 各计 1。
+     */
+    static int countEnabledBenefits(PlatformShopMembershipPlan plan) {
+        if (plan == null) {
+            return 0;
+        }
+        int n = 0;
+        if (Integer.valueOf(1).equals(plan.getBenefitAdvancedDataAnalysis())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitMarketingPriority())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitFriendCouponManage())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitOrderingManage())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitFriendRelationManage())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitCommentAppealDelete())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitOperatingData())) {
+            n++;
+        }
+        if (Integer.valueOf(1).equals(plan.getBenefitOperationalActivity())) {
+            n++;
+        }
+        if (plan.getBenefitWeeklyHomepageSlots() != null && plan.getBenefitWeeklyHomepageSlots() > 0) {
+            n++;
+        }
+        if (plan.getBenefitMonthlyComputingPower() != null && plan.getBenefitMonthlyComputingPower() > 0) {
+            n++;
+        }
+        return n;
+    }
+
+    @Override
+    public boolean saveOrUpdatePlan(PlatformShopMembershipPlan plan) {
+        if (plan == null) {
+            return false;
+        }
+        if (StringUtils.isBlank(plan.getPlanName())) {
+            throw new IllegalArgumentException("会员卡名称不能为空");
+        }
+        if (plan.getDurationMonths() == null || plan.getDurationMonths() <= 0) {
+            throw new IllegalArgumentException("时长(月)必须大于0");
+        }
+        if (plan.getPrice() == null || plan.getPrice().signum() <= 0) {
+            throw new IllegalArgumentException("价格必须大于0");
+        }
+        if (plan.getSortOrder() == null) {
+            plan.setSortOrder(0);
+        }
+        if (plan.getEnabled() == null) {
+            plan.setEnabled(1);
+        }
+        if (plan.getRecommended() == null) {
+            plan.setRecommended(0);
+        }
+        applyBenefitDefaults(plan);
+        Date now = new Date();
+        if (Integer.valueOf(1).equals(plan.getRecommended())) {
+            // 全表仅允许一条推荐:新建时尚无 id,先清掉所有推荐;更新时保留当前这条
+            clearOtherPlansRecommended(plan.getId());
+        }
+        if (plan.getId() == null) {
+            plan.setCreatedTime(now);
+            plan.setUpdatedTime(now);
+            return save(plan);
+        }
+        plan.setUpdatedTime(now);
+        return updateById(plan);
+    }
+
+    @Override
+    public boolean toggleEnabled(Integer id, Integer enabled) {
+        if (id == null || enabled == null) {
+            return false;
+        }
+        PlatformShopMembershipPlan plan = getById(id);
+        if (plan == null) {
+            return false;
+        }
+        plan.setEnabled(enabled);
+        plan.setUpdatedTime(new Date());
+        return updateById(plan);
+    }
+
+    @Override
+    public boolean toggleRecommended(Integer id, Integer recommended) {
+        if (id == null || recommended == null) {
+            return false;
+        }
+        if (recommended != 0 && recommended != 1) {
+            return false;
+        }
+        PlatformShopMembershipPlan plan = getById(id);
+        if (plan == null) {
+            return false;
+        }
+        if (recommended == 1) {
+            clearOtherPlansRecommended(id);
+        }
+        plan.setRecommended(recommended);
+        plan.setUpdatedTime(new Date());
+        return updateById(plan);
+    }
+
+    /**
+     * 将「当前推荐」全部置为非推荐;若 exceptId 非空则跳过该主键(用于保留即将设为推荐的那一条)。
+     */
+    private void clearOtherPlansRecommended(Integer exceptId) {
+        LambdaUpdateWrapper<PlatformShopMembershipPlan> u = new LambdaUpdateWrapper<>();
+        u.eq(PlatformShopMembershipPlan::getRecommended, 1);
+        if (exceptId != null) {
+            u.ne(PlatformShopMembershipPlan::getId, exceptId);
+        }
+        u.set(PlatformShopMembershipPlan::getRecommended, 0);
+        u.set(PlatformShopMembershipPlan::getUpdatedTime, new Date());
+        update(u);
+    }
+
+    @Override
+    public int logicDeleteByIds(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new IllegalArgumentException("方案ID列表不能为空");
+        }
+        List<Integer> clean = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (clean.isEmpty()) {
+            throw new IllegalArgumentException("方案ID列表不能为空");
+        }
+        Collection<PlatformShopMembershipPlan> plans = listByIds(clean);
+        if (plans == null || plans.isEmpty()) {
+            return 0;
+        }
+        List<Integer> pkIds = plans.stream().map(PlatformShopMembershipPlan::getId).collect(Collectors.toList());
+        removeByIds(pkIds);
+        return pkIds.size();
+    }
+
+    /**
+     * 权益开关与数值:null 视为 0;数值类不允许为负。
+     */
+    private static void applyBenefitDefaults(PlatformShopMembershipPlan plan) {
+        if (plan.getBenefitAdvancedDataAnalysis() == null) {
+            plan.setBenefitAdvancedDataAnalysis(0);
+        }
+        if (plan.getBenefitMarketingPriority() == null) {
+            plan.setBenefitMarketingPriority(0);
+        }
+        if (plan.getBenefitFriendCouponManage() == null) {
+            plan.setBenefitFriendCouponManage(0);
+        }
+        if (plan.getBenefitOrderingManage() == null) {
+            plan.setBenefitOrderingManage(0);
+        }
+        if (plan.getBenefitFriendRelationManage() == null) {
+            plan.setBenefitFriendRelationManage(0);
+        }
+        if (plan.getBenefitCommentAppealDelete() == null) {
+            plan.setBenefitCommentAppealDelete(0);
+        }
+        if (plan.getBenefitOperatingData() == null) {
+            plan.setBenefitOperatingData(0);
+        }
+        if (plan.getBenefitOperationalActivity() == null) {
+            plan.setBenefitOperationalActivity(0);
+        }
+        if (plan.getBenefitWeeklyHomepageSlots() == null) {
+            plan.setBenefitWeeklyHomepageSlots(0);
+        }
+        if (plan.getBenefitMonthlyComputingPower() == null) {
+            plan.setBenefitMonthlyComputingPower(0);
+        }
+        if (plan.getBenefitWeeklyHomepageSlots() < 0 || plan.getBenefitMonthlyComputingPower() < 0) {
+            throw new IllegalArgumentException("推广位次数与算力不能为负数");
+        }
+    }
+}

+ 40 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPlanServiceImpl.java

@@ -0,0 +1,40 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+import shop.alien.mapper.PlatformShopMembershipPlanMapper;
+import shop.alien.store.service.PlatformShopMembershipPlanService;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class PlatformShopMembershipPlanServiceImpl
+        extends ServiceImpl<PlatformShopMembershipPlanMapper, PlatformShopMembershipPlan>
+        implements PlatformShopMembershipPlanService {
+
+    @Override
+    public List<PlatformShopMembershipPlan> listEnabledForStore() {
+        LambdaQueryWrapper<PlatformShopMembershipPlan> w = new LambdaQueryWrapper<>();
+        w.eq(PlatformShopMembershipPlan::getEnabled, 1);
+        w.orderByDesc(PlatformShopMembershipPlan::getRecommended)
+                .orderByAsc(PlatformShopMembershipPlan::getSortOrder)
+                .orderByAsc(PlatformShopMembershipPlan::getId);
+        return list(w);
+    }
+
+    @Override
+    public PlatformShopMembershipPlan getEnabledById(Integer planId) {
+        if (planId == null) {
+            return null;
+        }
+        PlatformShopMembershipPlan plan = getById(planId);
+        if (plan == null || plan.getEnabled() == null || plan.getEnabled() != 1) {
+            return null;
+        }
+        return plan;
+    }
+}

+ 264 - 0
alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipServiceImpl.java

@@ -0,0 +1,264 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.PlatformShopMembership;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.PlatformShopMembershipPlan;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.vo.PlatformShopMembershipBenefitsVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipRemainingDetailVo;
+import shop.alien.entity.store.vo.PlatformShopMembershipStatusVo;
+import shop.alien.mapper.PlatformShopMembershipMapper;
+import shop.alien.mapper.StoreContractMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.service.PlatformShopMembershipOrderService;
+import shop.alien.store.service.PlatformShopMembershipPlanService;
+import shop.alien.store.service.PlatformShopMembershipService;
+import shop.alien.store.service.TrackEventService;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+@Service
+@RequiredArgsConstructor
+public class PlatformShopMembershipServiceImpl
+        extends ServiceImpl<PlatformShopMembershipMapper, PlatformShopMembership>
+        implements PlatformShopMembershipService {
+
+    private static final ZoneId CHINA = ZoneId.of("Asia/Shanghai");
+    private static final DateTimeFormatter YMD_SLASH = DateTimeFormatter.ofPattern("yyyy/MM/dd");
+
+    private final PlatformShopMembershipOrderService orderService;
+    private final PlatformShopMembershipPlanService planService;
+    /** 使用 Mapper 而非 StoreInfoService,避免与 StoreInfoServiceImpl 循环依赖 */
+    private final StoreInfoMapper storeInfoMapper;
+    private final StoreContractMapper storeContractMapper;
+    private final TrackEventService trackEventService;
+
+    @Override
+    public PlatformShopMembership getByStoreId(Integer storeId) {
+        if (storeId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<PlatformShopMembership> w = new LambdaQueryWrapper<>();
+        w.eq(PlatformShopMembership::getStoreId, storeId);
+        return getOne(w);
+    }
+
+    @Override
+    public PlatformShopMembershipStatusVo buildStatusVo(Integer storeId) {
+        PlatformShopMembershipStatusVo vo = new PlatformShopMembershipStatusVo();
+        fillStoreFlags(vo, storeId);
+        PlatformShopMembership m = getByStoreId(storeId);
+        Date now = new Date();
+        if (m == null || m.getExpiryDate() == null) {
+            vo.setActive(false);
+            vo.setRemainingDays(0);
+            vo.setBenefits(buildZeroBenefits());
+            return vo;
+        }
+        vo.setFirstOpenTime(m.getFirstOpenTime());
+        vo.setCurrentPeriodStart(m.getCurrentPeriodStart());
+        vo.setExpiryDate(m.getExpiryDate());
+        boolean active = !m.getExpiryDate().before(now);
+        vo.setActive(active);
+        if (!active) {
+            vo.setRemainingDays(0);
+            vo.setBenefits(buildZeroBenefits());
+        } else {
+            long diffMs = m.getExpiryDate().getTime() - now.getTime();
+            int days = (int) Math.max(0, (diffMs + 86400000L - 1) / 86400000L);
+            vo.setRemainingDays(days);
+            PlatformShopMembershipBenefitsVo benefits = resolveBenefits(m, storeId, m.getExpiryDate());
+            vo.setBenefits(benefits != null ? benefits : buildZeroBenefits());
+        }
+        return vo;
+    }
+
+    @Override
+    public PlatformShopMembershipRemainingDetailVo buildRemainingDetailVo(Integer storeId) {
+        PlatformShopMembershipRemainingDetailVo vo = new PlatformShopMembershipRemainingDetailVo();
+        vo.setHasMembershipRecord(false);
+        vo.setMembershipActive(false);
+        vo.setRemainingDays(0);
+        vo.setConsecutivePurchaseTotalDays(null);
+        vo.setConsecutivePeriodStartYmd(null);
+        vo.setCurrentMembershipEndYmd(null);
+        vo.setLastMonthVisitCount(0L);
+        vo.setCurrentMonthVisitCount(0L);
+        vo.setVisitCountMoMChangePercent(null);
+        vo.setWeeklyPromotionTotalPerWeek(0);
+        vo.setWeeklyPromotionRemainingThisWeek(0);
+        if (storeId == null) {
+            return vo;
+        }
+        fillMonthlyVisitStats(vo, storeId);
+
+        PlatformShopMembership m = getByStoreId(storeId);
+        if (m == null || m.getExpiryDate() == null) {
+            return vo;
+        }
+        vo.setHasMembershipRecord(true);
+        Date now = new Date();
+        Date expiry = m.getExpiryDate();
+        vo.setCurrentMembershipEndYmd(formatYmdSlash(expiry));
+        boolean active = !expiry.before(now);
+        vo.setMembershipActive(active);
+        if (active) {
+            long diffMs = expiry.getTime() - now.getTime();
+            vo.setRemainingDays((int) Math.max(0, (diffMs + 86400000L - 1) / 86400000L));
+        } else {
+            vo.setRemainingDays(0);
+        }
+        // 连续续费周期起点:与支付成功写入规则一致;无 current_period_start 时回退首次开通时间
+        Date periodStart = m.getCurrentPeriodStart() != null ? m.getCurrentPeriodStart() : m.getFirstOpenTime();
+        vo.setConsecutivePeriodStartYmd(formatYmdSlash(periodStart));
+        if (periodStart != null && !periodStart.after(expiry)) {
+            long spanMs = expiry.getTime() - periodStart.getTime();
+            vo.setConsecutivePurchaseTotalDays((int) Math.max(0, (spanMs + 86400000L - 1) / 86400000L));
+        }
+        fillWeeklyPromotionFields(vo, m, storeId, active);
+        return vo;
+    }
+
+    private void fillWeeklyPromotionFields(PlatformShopMembershipRemainingDetailVo vo, PlatformShopMembership m,
+                                          Integer storeId, boolean active) {
+        if (!active) {
+            return;
+        }
+        Integer planId = m.getCurrentPlanId();
+        if (planId == null) {
+            PlatformShopMembershipOrder ord = orderService.findPaidOrderForCurrentMembershipExpiry(storeId, m.getExpiryDate());
+            if (ord != null) {
+                planId = ord.getPlanId();
+            }
+        }
+        if (planId != null) {
+            PlatformShopMembershipPlan plan = planService.getById(planId);
+            if (plan != null && plan.getBenefitWeeklyHomepageSlots() != null) {
+                vo.setWeeklyPromotionTotalPerWeek(plan.getBenefitWeeklyHomepageSlots());
+            }
+        }
+        int rem = m.getWeeklyPromotionRemaining() != null ? m.getWeeklyPromotionRemaining() : 0;
+        vo.setWeeklyPromotionRemainingThisWeek(rem);
+    }
+
+    /** 上月整月 vs 本月至今访问次数及环比(埋点口径) */
+    private void fillMonthlyVisitStats(PlatformShopMembershipRemainingDetailVo vo, Integer storeId) {
+        ZonedDateTime nowZ = ZonedDateTime.now(CHINA);
+        YearMonth thisYm = YearMonth.from(nowZ);
+        YearMonth lastYm = thisYm.minusMonths(1);
+
+        ZonedDateTime thisMonthStart = thisYm.atDay(1).atStartOfDay(CHINA);
+        ZonedDateTime lastMonthStart = lastYm.atDay(1).atStartOfDay(CHINA);
+        ZonedDateTime lastMonthEnd = lastYm.atEndOfMonth().atTime(23, 59, 59, 999_000_000).atZone(CHINA);
+
+        Date lastStart = Date.from(lastMonthStart.toInstant());
+        Date lastEnd = Date.from(lastMonthEnd.toInstant());
+        long lastCount = trackEventService.countStoreViewEventsBetweenInclusive(storeId, lastStart, lastEnd);
+        vo.setLastMonthVisitCount(lastCount);
+
+        Date curStart = Date.from(thisMonthStart.toInstant());
+        Date curEnd = Date.from(nowZ.toInstant());
+        long curCount = trackEventService.countStoreViewEventsBetweenInclusive(storeId, curStart, curEnd);
+        vo.setCurrentMonthVisitCount(curCount);
+
+        if (lastCount > 0) {
+            BigDecimal pct = BigDecimal.valueOf(curCount - lastCount)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(lastCount), 2, RoundingMode.HALF_UP);
+            vo.setVisitCountMoMChangePercent(pct);
+        } else {
+            vo.setVisitCountMoMChangePercent(null);
+        }
+    }
+
+    private static String formatYmdSlash(Date date) {
+        if (date == null) {
+            return null;
+        }
+        return date.toInstant().atZone(CHINA).toLocalDate().format(YMD_SLASH);
+    }
+
+    private void fillStoreFlags(PlatformShopMembershipStatusVo vo, Integer storeId) {
+        if (storeId == null) {
+            vo.setSigned(false);
+            vo.setSettled(false);
+            return;
+        }
+        StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        vo.setSigned(isSignedByContract(storeId));
+        vo.setSettled(isApproved(storeInfo == null ? null : storeInfo.getStoreApplicationStatus()));
+    }
+
+    private boolean isApproved(Integer status) {
+        return status != null && status == 1;
+    }
+
+    private boolean isSignedByContract(Integer storeId) {
+        String signingStatus = storeContractMapper.selectSigningStatus(storeId);
+        return "已签署".equals(signingStatus);
+    }
+
+    /**
+     * 优先使用会员表冗余的 {@link PlatformShopMembership#getCurrentPlanId()};旧数据为空时回退查已支付订单。
+     */
+    private PlatformShopMembershipBenefitsVo resolveBenefits(PlatformShopMembership m, Integer storeId, Date membershipExpiry) {
+        if (m != null && m.getCurrentPlanId() != null) {
+            return buildBenefitsFromPlan(m.getCurrentPlanId(), null);
+        }
+        PlatformShopMembershipOrder order = orderService.findPaidOrderForCurrentMembershipExpiry(storeId, membershipExpiry);
+        if (order == null || order.getPlanId() == null) {
+            return null;
+        }
+        return buildBenefitsFromPlan(order.getPlanId(), order.getPlanName());
+    }
+
+    private PlatformShopMembershipBenefitsVo buildBenefitsFromPlan(Integer planId, String planNameWhenPlanMissing) {
+        PlatformShopMembershipPlan plan = planService.getById(planId);
+        PlatformShopMembershipBenefitsVo b = new PlatformShopMembershipBenefitsVo();
+        b.setPlanId(planId);
+        b.setPlanName(plan != null ? plan.getPlanName()
+                : (planNameWhenPlanMissing != null ? planNameWhenPlanMissing : ""));
+        if (plan == null) {
+            return b;
+        }
+        b.setBenefitAdvancedDataAnalysis(plan.getBenefitAdvancedDataAnalysis());
+        b.setBenefitMarketingPriority(plan.getBenefitMarketingPriority());
+        b.setBenefitFriendCouponManage(plan.getBenefitFriendCouponManage());
+        b.setBenefitOrderingManage(plan.getBenefitOrderingManage());
+        b.setBenefitFriendRelationManage(plan.getBenefitFriendRelationManage());
+        b.setBenefitCommentAppealDelete(plan.getBenefitCommentAppealDelete());
+        b.setBenefitOperatingData(plan.getBenefitOperatingData());
+        b.setBenefitOperationalActivity(plan.getBenefitOperationalActivity());
+        b.setBenefitWeeklyHomepageSlots(plan.getBenefitWeeklyHomepageSlots());
+        b.setBenefitMonthlyComputingPower(plan.getBenefitMonthlyComputingPower());
+        return b;
+    }
+
+    /** 无有效会员或无法解析方案时:权益数值均为 0,方案信息为空 */
+    private PlatformShopMembershipBenefitsVo buildZeroBenefits() {
+        PlatformShopMembershipBenefitsVo empty = new PlatformShopMembershipBenefitsVo();
+        empty.setPlanId(null);
+        empty.setPlanName("");
+        empty.setBenefitAdvancedDataAnalysis(0);
+        empty.setBenefitMarketingPriority(0);
+        empty.setBenefitFriendCouponManage(0);
+        empty.setBenefitOrderingManage(0);
+        empty.setBenefitFriendRelationManage(0);
+        empty.setBenefitCommentAppealDelete(0);
+        empty.setBenefitOperatingData(0);
+        empty.setBenefitOperationalActivity(0);
+        empty.setBenefitWeeklyHomepageSlots(0);
+        empty.setBenefitMonthlyComputingPower(0);
+        return empty;
+    }
+}

+ 2 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -185,6 +185,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     private final EssentialHolidayComparisonMapper essentialHolidayComparisonMapper;
 
     private final StorePaymentConfigService storePaymentConfigService;
+    private final PlatformShopMembershipService platformShopMembershipService;
 
 
     @Value("${spring.web.resources.excel-path}")
@@ -374,6 +375,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                 .isNull(CommonRating::getAuditStatus));
         Integer ratingCountLong = commonRatingMapper.selectCount(ratingWrapper);
         storeMainInfoVo.setRatingCount(ratingCountLong != null ? ratingCountLong.intValue() : 0);
+        storeMainInfoVo.setPlatformMembershipStatus(platformShopMembershipService.buildStatusVo(id));
 
         return storeMainInfoVo;
     }

+ 17 - 0
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -1112,6 +1112,23 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             return 0L;
         }
     }
+
+    @Override
+    public Long countStoreViewEventsBetweenInclusive(Integer storeId, Date startInclusive, Date endInclusive) {
+        try {
+            if (storeId == null || storeId <= 0 || startInclusive == null || endInclusive == null) {
+                return 0L;
+            }
+            if (endInclusive.before(startInclusive)) {
+                return 0L;
+            }
+            Long n = getBaseMapper().countStoreViewCountBetweenInclusive(storeId, startInclusive, endInclusive);
+            return n != null ? n : 0L;
+        } catch (Exception e) {
+            log.error("按区间统计店铺浏览量失败: storeId={}", storeId, e);
+            return 0L;
+        }
+    }
     
     /**
      * 根据phoneId获取StoreUser

+ 23 - 0
alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/PlatformMembershipPaymentStrategy.java

@@ -0,0 +1,23 @@
+package shop.alien.store.strategy.platformMembershipPayment;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.PlatformShopMembershipPaymentOrder;
+
+import java.util.Map;
+
+/**
+ * 平台店铺会员卡支付(参照预订订单 MerchantPaymentStrategy,独立实现)
+ */
+public interface PlatformMembershipPaymentStrategy {
+
+    R<Map<String, Object>> createPrePay(Integer storeId, Integer membershipOrderId, String amountYuan, String subject, Integer userId);
+
+    R<Object> queryPayStatus(Integer storeId, String outTradeNo);
+
+    String getType();
+
+    /**
+     * 测试/联调用:对指定会员卡支付流水调用第三方退款;成功后将支付单与会员卡业务订单标为已退款(不自动回滚 platform_shop_membership 权益周期)。
+     */
+    R<String> refundForTest(PlatformShopMembershipPaymentOrder paymentOrder, String refundAmountYuan, String refundReason);
+}

+ 33 - 0
alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/PlatformMembershipPaymentStrategyFactory.java

@@ -0,0 +1,33 @@
+package shop.alien.store.strategy.platformMembershipPayment;
+
+import org.springframework.stereotype.Component;
+import shop.alien.util.common.constant.PaymentEnum;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class PlatformMembershipPaymentStrategyFactory {
+
+    private final Map<String, PlatformMembershipPaymentStrategy> strategyMap = new HashMap<>();
+
+    public PlatformMembershipPaymentStrategyFactory(List<PlatformMembershipPaymentStrategy> strategies) {
+        if (strategies != null) {
+            for (PlatformMembershipPaymentStrategy s : strategies) {
+                strategyMap.put(s.getType(), s);
+            }
+        }
+    }
+
+    public PlatformMembershipPaymentStrategy getStrategy(String type) {
+        if (type == null || type.trim().isEmpty()) {
+            type = PaymentEnum.ALIPAY.getType();
+        }
+        PlatformMembershipPaymentStrategy s = strategyMap.get(type);
+        if (s == null) {
+            throw new IllegalArgumentException("不支持的支付方式: " + type);
+        }
+        return s;
+    }
+}

+ 334 - 0
alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/impl/PlatformMembershipAlipayPaymentStrategyImpl.java

@@ -0,0 +1,334 @@
+package shop.alien.store.strategy.platformMembershipPayment.impl;
+
+import com.alibaba.fastjson.JSON;
+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.AlipayTradeAppPayModel;
+import com.alipay.api.domain.AlipayTradeRefundModel;
+import com.alipay.api.request.AlipayTradeAppPayRequest;
+import com.alipay.api.request.AlipayTradeQueryRequest;
+import com.alipay.api.request.AlipayTradeRefundRequest;
+import com.alipay.api.response.AlipayTradeAppPayResponse;
+import com.alipay.api.response.AlipayTradeQueryResponse;
+import com.alipay.api.response.AlipayTradeRefundResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.PlatformShopMembershipPaymentOrder;
+import shop.alien.store.service.PlatformShopMembershipOrderService;
+import shop.alien.store.service.PlatformShopMembershipPaymentOrderService;
+import shop.alien.store.strategy.platformMembershipPayment.PlatformMembershipPaymentStrategy;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.PaymentEnum;
+import shop.alien.util.system.OSUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 平台店铺会员卡 — 支付宝预支付/查单(支付流水写入 platform_shop_membership_payment_order)
+ * <p>支付参数与 {@link shop.alien.store.strategy.payment.impl.AlipayPaymentStrategyImpl} 一致,从 payment.aliPay.business 读取,不读 store_payment_config。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class PlatformMembershipAlipayPaymentStrategyImpl implements PlatformMembershipPaymentStrategy {
+
+    @Value("${payment.aliPay.business.appId}")
+    private String businessAppId;
+
+    @Value("${payment.aliPay.business.appPrivateKey}")
+    private String businessAppPrivateKey;
+
+    @Value("${payment.aliPay.business.win.appCertPath}")
+    private String businessWinAppCertPath;
+
+    @Value("${payment.aliPay.business.win.alipayPublicCertPath}")
+    private String businessWinAlipayPublicCertPath;
+
+    @Value("${payment.aliPay.business.win.alipayRootCertPath}")
+    private String businessWinAlipayRootCertPath;
+
+    @Value("${payment.aliPay.business.linux.appCertPath}")
+    private String businessLinuxAppCertPath;
+
+    @Value("${payment.aliPay.business.linux.alipayPublicCertPath}")
+    private String businessLinuxAlipayPublicCertPath;
+
+    @Value("${payment.aliPay.business.linux.alipayRootCertPath}")
+    private String businessLinuxAlipayRootCertPath;
+
+    @Value("${payment.aliPay.host}")
+    private String aliPayHost;
+
+    private static final String REDIS_PREPAY_KEY_PREFIX = "platform:membership:alipay:prepay:order:";
+    private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+    private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "platform:membership:alipay:prepay:debounce:order:";
+    private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
+
+    private final PlatformShopMembershipOrderService platformShopMembershipOrderService;
+    private final PlatformShopMembershipPaymentOrderService membershipPaymentOrderService;
+    private final StringRedisTemplate stringRedisTemplate;
+
+    @Override
+    public R<Map<String, Object>> createPrePay(Integer storeId, Integer membershipOrderId, String amountYuan, String subject, Integer userId) {
+        if (storeId == null || membershipOrderId == null) {
+            return R.fail("门店ID和会员卡订单ID不能为空");
+        }
+        if (StringUtils.isBlank(amountYuan)) {
+            return R.fail("支付金额不能为空");
+        }
+        BigDecimal amountYuanBD;
+        try {
+            amountYuanBD = new BigDecimal(amountYuan).setScale(2, RoundingMode.HALF_UP);
+        } catch (NumberFormatException e) {
+            return R.fail("支付金额格式不正确");
+        }
+        if (amountYuanBD.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("支付金额必须大于0");
+        }
+        if (StringUtils.isBlank(subject)) {
+            return R.fail("订单描述不能为空");
+        }
+        PlatformShopMembershipOrder order = platformShopMembershipOrderService.getById(membershipOrderId);
+        if (order == null) {
+            return R.fail("会员卡订单不存在");
+        }
+        if (!order.getStoreId().equals(storeId)) {
+            return R.fail("订单与门店不匹配");
+        }
+        if (order.getPayStatus() != null && order.getPayStatus() != PlatformShopMembershipOrder.PAY_STATUS_PENDING) {
+            return R.fail("订单不可支付");
+        }
+        BigDecimal expect = order.getPayAmount() != null ? order.getPayAmount() : BigDecimal.ZERO;
+        if (amountYuanBD.compareTo(expect.setScale(2, RoundingMode.HALF_UP)) != 0) {
+            return R.fail("支付金额与订单金额不一致");
+        }
+
+        String redisKey = REDIS_PREPAY_KEY_PREFIX + membershipOrderId;
+        String cached = stringRedisTemplate.opsForValue().get(redisKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> data = JSON.parseObject(cached, Map.class);
+                if (data != null && !data.isEmpty()) {
+                    log.info("会员卡支付宝预支付命中缓存,membershipOrderId={}", membershipOrderId);
+                    return R.data(data);
+                }
+            } catch (Exception e) {
+                log.warn("解析会员卡预支付缓存失败,将重新发起,membershipOrderId={}", membershipOrderId, e);
+            }
+        }
+
+        String debounceKey = REDIS_PREPAY_DEBOUNCE_KEY_PREFIX + membershipOrderId;
+        Boolean debounceSet = stringRedisTemplate.opsForValue().setIfAbsent(debounceKey, "1", REDIS_PREPAY_DEBOUNCE_SECONDS, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(debounceSet)) {
+            return R.fail("请勿重复提交,请稍后再试");
+        }
+
+        int deleted = membershipPaymentOrderService.logicDeletePendingByMembershipOrderIdAndPayType(
+                membershipOrderId, PaymentEnum.ALIPAY.getType());
+        if (deleted > 0) {
+            log.info("已逻辑删除该会员卡订单下支付宝支付单 {} 条,membershipOrderId={}", deleted, membershipOrderId);
+        }
+
+        String outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        Date now = new Date();
+        PlatformShopMembershipPaymentOrder paymentOrder = new PlatformShopMembershipPaymentOrder();
+        paymentOrder.setPaymentNo(membershipPaymentOrderService.generatePaymentNo());
+        paymentOrder.setMembershipOrderId(order.getId());
+        paymentOrder.setOrderSn(order.getOrderSn());
+        paymentOrder.setStoreId(storeId);
+        paymentOrder.setPayType(PaymentEnum.ALIPAY.getType());
+        paymentOrder.setOutTradeNo(outTradeNo);
+        paymentOrder.setPayAmount(amountYuanBD);
+        paymentOrder.setPayStatus(0);
+        paymentOrder.setPayerUserId(userId);
+        paymentOrder.setSubject(subject);
+        paymentOrder.setCreatedTime(now);
+        paymentOrder.setUpdatedTime(now);
+        membershipPaymentOrderService.save(paymentOrder);
+
+        try {
+            AlipayConfig alipayConfig = buildBusinessAlipayConfig();
+            AlipayClient client = new DefaultAlipayClient(alipayConfig);
+            AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
+            AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
+            model.setOutTradeNo(outTradeNo);
+            model.setTotalAmount(amountYuanBD.toPlainString());
+            model.setSubject(subject);
+            model.setTimeoutExpress("15m");
+            request.setBizModel(model);
+            AlipayTradeAppPayResponse response = client.sdkExecute(request);
+            String orderStr = response.isSuccess() ? response.getBody() : "";
+            if (!response.isSuccess()) {
+                return R.fail("预支付失败:" + response.getSubMsg());
+            }
+            Map<String, Object> data = new HashMap<>();
+            data.put("orderStr", orderStr);
+            data.put("outTradeNo", outTradeNo);
+            data.put("orderSn", order.getOrderSn());
+            data.put("orderId", order.getId());
+            data.put("paymentNo", paymentOrder.getPaymentNo());
+            stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(data), REDIS_PREPAY_EXPIRE_SECONDS, TimeUnit.SECONDS);
+            log.info("会员卡支付宝预支付成功 storeId={}, orderSn={}, outTradeNo={}", storeId, order.getOrderSn(), outTradeNo);
+            return R.data(data);
+        } catch (AlipayApiException e) {
+            log.error("会员卡支付宝预支付异常 storeId={}, membershipOrderId={}", storeId, membershipOrderId, e);
+            return R.fail("预支付失败:" + e.getErrMsg());
+        } catch (Exception e) {
+            log.error("构建支付宝配置异常 storeId={}", storeId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        PlatformShopMembershipPaymentOrder paymentOrder = membershipPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        if (!storeId.equals(paymentOrder.getStoreId())) {
+            return R.fail("支付单与门店不匹配");
+        }
+        PlatformShopMembershipOrder order = platformShopMembershipOrderService.getById(paymentOrder.getMembershipOrderId());
+        if (order == null) {
+            return R.fail("会员卡订单不存在");
+        }
+        if (order.getPayStatus() != null && order.getPayStatus() == PlatformShopMembershipOrder.PAY_STATUS_PAID) {
+            return R.success("支付成功");
+        }
+        try {
+            AlipayConfig alipayConfig = buildBusinessAlipayConfig();
+            AlipayClient client = new DefaultAlipayClient(alipayConfig);
+            AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
+            JSONObject bizContent = new JSONObject();
+            bizContent.put("out_trade_no", outTradeNo);
+            request.setBizContent(bizContent.toJSONString());
+            AlipayTradeQueryResponse response = client.certificateExecute(request);
+            if (!response.isSuccess()) {
+                return R.fail("查询失败:" + response.getSubMsg());
+            }
+            String tradeStatus = response.getTradeStatus();
+            if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
+                Date now = new Date();
+                paymentOrder.setPayStatus(1);
+                paymentOrder.setTradeNo(response.getTradeNo());
+                paymentOrder.setPayTime(now);
+                paymentOrder.setUpdatedTime(now);
+                membershipPaymentOrderService.updateById(paymentOrder);
+                platformShopMembershipOrderService.applyPaySuccess(order.getId(), now, outTradeNo, response.getTradeNo(), PaymentEnum.ALIPAY.getType());
+                return R.success("支付成功");
+            }
+            if ("TRADE_CLOSED".equals(tradeStatus)) {
+                return R.fail("交易已关闭");
+            }
+            if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
+                return R.fail("等待买家付款");
+            }
+            return R.success("订单状态:" + tradeStatus);
+        } catch (AlipayApiException e) {
+            log.error("会员卡支付宝查单异常 outTradeNo={}", outTradeNo, e);
+            return R.fail("查询异常:" + e.getErrMsg());
+        } catch (Exception e) {
+            log.error("会员卡支付宝查单异常 storeId={}", storeId, e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.ALIPAY.getType();
+    }
+
+    @Override
+    public R<String> refundForTest(PlatformShopMembershipPaymentOrder po, String refundAmountYuan, String refundReason) {
+        if (po == null || StringUtils.isBlank(po.getOutTradeNo())) {
+            return R.fail("支付单或商户订单号无效");
+        }
+        if (!PaymentEnum.ALIPAY.getType().equals(po.getPayType())) {
+            return R.fail("非支付宝会员卡支付单");
+        }
+        if (StringUtils.isBlank(refundAmountYuan)) {
+            return R.fail("退款金额不能为空");
+        }
+        BigDecimal refundBd;
+        try {
+            refundBd = new BigDecimal(refundAmountYuan).setScale(2, RoundingMode.HALF_UP);
+        } catch (NumberFormatException e) {
+            return R.fail("退款金额格式不正确");
+        }
+        if (refundBd.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("退款金额必须大于0");
+        }
+        if (po.getPayAmount() != null
+                && refundBd.compareTo(po.getPayAmount().setScale(2, RoundingMode.HALF_UP)) > 0) {
+            return R.fail("退款金额不能超过原支付金额");
+        }
+        String outTradeNo = po.getOutTradeNo();
+        try {
+            AlipayConfig alipayConfig = buildBusinessAlipayConfig();
+            AlipayClient client = new DefaultAlipayClient(alipayConfig);
+            AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
+            AlipayTradeRefundModel model = new AlipayTradeRefundModel();
+            model.setOutTradeNo(outTradeNo);
+            model.setRefundAmount(refundBd.toPlainString());
+            model.setRefundReason(StringUtils.isNotBlank(refundReason) ? refundReason : "测试退款");
+            request.setBizModel(model);
+            AlipayTradeRefundResponse response = client.certificateExecute(request);
+            if (!response.isSuccess()) {
+                return R.fail("退款失败:" + response.getSubMsg());
+            }
+            JSONObject body = JSONObject.parseObject(response.getBody());
+            JSONObject refundResp = body != null ? body.getJSONObject("alipay_trade_refund_response") : null;
+            String fundChange = refundResp != null ? refundResp.getString("fund_change") : null;
+            String tradeNo = refundResp != null ? refundResp.getString("trade_no") : null;
+            membershipPaymentOrderService.markPaymentAndMembershipOrderRefunded(po);
+            log.warn("会员卡支付宝退款成功并已回写支付单/业务订单状态 membershipOrderId={}, outTradeNo={}, tradeNo={}, fundChange={}",
+                    po.getMembershipOrderId(), outTradeNo, tradeNo, fundChange);
+            return R.data("退款成功,已更新支付单与会员卡业务订单为已退款,membershipOrderId=" + po.getMembershipOrderId()
+                    + ", trade_no=" + tradeNo + ", fund_change=" + fundChange);
+        } catch (AlipayApiException e) {
+            log.error("[测试]会员卡支付宝退款异常 outTradeNo={}", outTradeNo, e);
+            return R.fail("退款异常:" + e.getErrMsg());
+        } catch (Exception e) {
+            log.error("[测试]会员卡支付宝退款异常 membershipOrderId={}", po.getMembershipOrderId(), e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    private AlipayConfig buildBusinessAlipayConfig() {
+        AlipayConfig alipayConfig = new AlipayConfig();
+        alipayConfig.setServerUrl(aliPayHost);
+        alipayConfig.setAppId(businessAppId);
+        alipayConfig.setPrivateKey(businessAppPrivateKey);
+        alipayConfig.setFormat("json");
+        alipayConfig.setCharset("UTF-8");
+        alipayConfig.setSignType("RSA2");
+        if ("windows".equals(OSUtil.getOsName())) {
+            alipayConfig.setAppCertPath(businessWinAppCertPath);
+            alipayConfig.setAlipayPublicCertPath(businessWinAlipayPublicCertPath);
+            alipayConfig.setRootCertPath(businessWinAlipayRootCertPath);
+        } else {
+            alipayConfig.setAppCertPath(businessLinuxAppCertPath);
+            alipayConfig.setAlipayPublicCertPath(businessLinuxAlipayPublicCertPath);
+            alipayConfig.setRootCertPath(businessLinuxAlipayRootCertPath);
+        }
+        return alipayConfig;
+    }
+}

+ 440 - 0
alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/impl/PlatformMembershipWechatPaymentStrategyImpl.java

@@ -0,0 +1,440 @@
+package shop.alien.store.strategy.platformMembershipPayment.impl;
+
+import com.alibaba.fastjson.JSON;
+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.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.PlatformShopMembershipOrder;
+import shop.alien.entity.store.PlatformShopMembershipPaymentOrder;
+import shop.alien.store.service.PlatformShopMembershipOrderService;
+import shop.alien.store.service.PlatformShopMembershipPaymentOrderService;
+import shop.alien.store.strategy.payment.impl.WeChatPaymentStrategyImpl;
+import shop.alien.store.strategy.platformMembershipPayment.PlatformMembershipPaymentStrategy;
+import shop.alien.store.util.WXPayUtility;
+import shop.alien.util.common.UniqueRandomNumGenerator;
+import shop.alien.util.common.constant.PaymentEnum;
+import shop.alien.util.system.OSUtil;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 平台店铺会员卡 — 微信预支付/查单(支付流水写入 platform_shop_membership_payment_order)
+ * <p>支付参数与 {@link shop.alien.store.strategy.payment.impl.WeChatPaymentStrategyImpl} 一致,从 payment.wechatPay.business 读取,不读 store_payment_config。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class PlatformMembershipWechatPaymentStrategyImpl implements PlatformMembershipPaymentStrategy {
+
+    private static final String POSTMETHOD = "POST";
+    private static final String GETMETHOD = "GET";
+    private static final String REDIS_PREPAY_KEY_PREFIX = "platform:membership:wechat:prepay:order:";
+    private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
+    private static final String REDIS_PREPAY_DEBOUNCE_KEY_PREFIX = "platform:membership:wechat:prepay:debounce:order:";
+    private static final long REDIS_PREPAY_DEBOUNCE_SECONDS = 5;
+
+    @Value("${payment.wechatPay.host}")
+    private String wechatPayApiHost;
+
+    @Value("${payment.wechatPay.prePayPath}")
+    private String prePayPath;
+
+    @Value("${payment.wechatPay.searchOrderByOutTradeNoPath}")
+    private String searchOrderByOutTradeNoPath;
+
+    @Value("${payment.wechatPay.refundPath:/v3/refund/domestic/refunds}")
+    private String refundPath;
+
+    @Value("${payment.wechatPay.business.refundNotifyUrl:}")
+    private String businessRefundNotifyUrl;
+
+    @Value("${payment.wechatPay.business.appId}")
+    private String appId;
+
+    @Value("${payment.wechatPay.business.mchId}")
+    private String mchId;
+
+    @Value("${payment.wechatPay.business.win.privateKeyPath}")
+    private String privateWinKeyPath;
+
+    @Value("${payment.wechatPay.business.linux.privateKeyPath}")
+    private String privateLinuxKeyPath;
+
+    @Value("${payment.wechatPay.business.win.wechatPayPublicKeyFilePath}")
+    private String wechatWinPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPay.business.linux.wechatPayPublicKeyFilePath}")
+    private String wechatLinuxPayPublicKeyFilePath;
+
+    @Value("${payment.wechatPay.business.merchantSerialNumber}")
+    private String merchantSerialNumber;
+
+    @Value("${payment.wechatPay.business.wechatPayPublicKeyId}")
+    private String wechatPayPublicKeyId;
+
+    @Value("${payment.wechatPay.business.prePayNotifyUrl}")
+    private String prePayNotifyUrl;
+
+    private PrivateKey privateKey;
+    private PublicKey wechatPayPublicKey;
+
+    private final PlatformShopMembershipOrderService platformShopMembershipOrderService;
+    private final PlatformShopMembershipPaymentOrderService membershipPaymentOrderService;
+    private final StringRedisTemplate stringRedisTemplate;
+
+    @PostConstruct
+    public void loadWechatBusinessKeys() {
+        String privateKeyPath;
+        String wechatPayPublicKeyFilePath;
+        if ("windows".equals(OSUtil.getOsName())) {
+            privateKeyPath = privateWinKeyPath;
+            wechatPayPublicKeyFilePath = wechatWinPayPublicKeyFilePath;
+        } else {
+            privateKeyPath = privateLinuxKeyPath;
+            wechatPayPublicKeyFilePath = wechatLinuxPayPublicKeyFilePath;
+        }
+        this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
+        this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
+    }
+
+    @Override
+    public R<Map<String, Object>> createPrePay(Integer storeId, Integer membershipOrderId, String amountYuan, String subject, Integer userId) {
+        if (storeId == null || membershipOrderId == null) {
+            return R.fail("门店ID和会员卡订单ID不能为空");
+        }
+        if (StringUtils.isBlank(amountYuan)) {
+            return R.fail("支付金额不能为空");
+        }
+        BigDecimal amountYuanBD;
+        try {
+            amountYuanBD = new BigDecimal(amountYuan).setScale(2, RoundingMode.HALF_UP);
+        } catch (NumberFormatException e) {
+            return R.fail("支付金额格式不正确");
+        }
+        if (amountYuanBD.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("支付金额必须大于0");
+        }
+        if (StringUtils.isBlank(subject)) {
+            return R.fail("订单描述不能为空");
+        }
+        PlatformShopMembershipOrder order = platformShopMembershipOrderService.getById(membershipOrderId);
+        if (order == null) {
+            return R.fail("会员卡订单不存在");
+        }
+        if (!order.getStoreId().equals(storeId)) {
+            return R.fail("订单与门店不匹配");
+        }
+        if (order.getPayStatus() != null && order.getPayStatus() != PlatformShopMembershipOrder.PAY_STATUS_PENDING) {
+            return R.fail("订单不可支付");
+        }
+        BigDecimal expect = order.getPayAmount() != null ? order.getPayAmount() : BigDecimal.ZERO;
+        if (amountYuanBD.compareTo(expect.setScale(2, RoundingMode.HALF_UP)) != 0) {
+            return R.fail("支付金额与订单金额不一致");
+        }
+
+        String redisKey = REDIS_PREPAY_KEY_PREFIX + membershipOrderId;
+        String cached = stringRedisTemplate.opsForValue().get(redisKey);
+        if (StringUtils.isNotBlank(cached)) {
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> data = JSON.parseObject(cached, Map.class);
+                if (data != null && !data.isEmpty()) {
+                    log.info("会员卡微信预支付命中缓存,membershipOrderId={}", membershipOrderId);
+                    return R.data(data);
+                }
+            } catch (Exception e) {
+                log.warn("解析会员卡微信预支付缓存失败,将重新发起,membershipOrderId={}", membershipOrderId, e);
+            }
+        }
+
+        String debounceKey = REDIS_PREPAY_DEBOUNCE_KEY_PREFIX + membershipOrderId;
+        Boolean debounceSet = stringRedisTemplate.opsForValue().setIfAbsent(debounceKey, "1", REDIS_PREPAY_DEBOUNCE_SECONDS, TimeUnit.SECONDS);
+        if (Boolean.FALSE.equals(debounceSet)) {
+            return R.fail("请勿重复提交,请稍后再试");
+        }
+
+        int deleted = membershipPaymentOrderService.logicDeletePendingByMembershipOrderIdAndPayType(
+                membershipOrderId, PaymentEnum.WECHAT_PAY.getType());
+        if (deleted > 0) {
+            log.info("已逻辑删除该会员卡订单下微信支付单 {} 条,membershipOrderId={}", deleted, membershipOrderId);
+        }
+
+        String outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+        Date now = new Date();
+        PlatformShopMembershipPaymentOrder paymentOrder = new PlatformShopMembershipPaymentOrder();
+        paymentOrder.setPaymentNo(membershipPaymentOrderService.generatePaymentNo());
+        paymentOrder.setMembershipOrderId(order.getId());
+        paymentOrder.setOrderSn(order.getOrderSn());
+        paymentOrder.setStoreId(storeId);
+        paymentOrder.setPayType(PaymentEnum.WECHAT_PAY.getType());
+        paymentOrder.setOutTradeNo(outTradeNo);
+        paymentOrder.setPayAmount(amountYuanBD);
+        paymentOrder.setPayStatus(0);
+        paymentOrder.setPayerUserId(userId);
+        paymentOrder.setSubject(subject);
+        paymentOrder.setCreatedTime(now);
+        paymentOrder.setUpdatedTime(now);
+        membershipPaymentOrderService.save(paymentOrder);
+
+        try {
+            WeChatPaymentStrategyImpl.CommonPrepayRequest request = new WeChatPaymentStrategyImpl.CommonPrepayRequest();
+            request.appid = appId;
+            request.mchid = mchId;
+            request.description = subject;
+            request.outTradeNo = outTradeNo;
+            request.notifyUrl = StringUtils.isNotBlank(prePayNotifyUrl) ? prePayNotifyUrl : "";
+            request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
+            request.amount.total = amountYuanBD.multiply(new BigDecimal(100)).longValue();
+            request.amount.currency = "CNY";
+
+            WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = prePayOrderRun(request);
+            if (response == null || StringUtils.isBlank(response.prepayId)) {
+                return R.fail("微信预支付失败");
+            }
+
+            long timestamp = System.currentTimeMillis() / 1000;
+            String nonce = WXPayUtility.createNonce(32);
+            String message = String.format("%s\n%s\n%s\n%s\n", appId, timestamp, nonce, response.prepayId);
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initSign(privateKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            String signStr = Base64.getEncoder().encodeToString(sign.sign());
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("outTradeNo", outTradeNo);
+            data.put("orderSn", order.getOrderSn());
+            data.put("orderId", order.getId());
+            data.put("paymentNo", paymentOrder.getPaymentNo());
+            data.put("prepayId", response.prepayId);
+            data.put("appId", appId);
+            data.put("mchId", mchId);
+            data.put("sign", signStr);
+            data.put("timestamp", String.valueOf(timestamp));
+            data.put("nonce", nonce);
+            data.put("orderStr", "");
+
+            stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(data), REDIS_PREPAY_EXPIRE_SECONDS, TimeUnit.SECONDS);
+            log.info("会员卡微信预支付成功 storeId={}, orderSn={}, outTradeNo={}", storeId, order.getOrderSn(), outTradeNo);
+            return R.data(data);
+        } catch (WXPayUtility.ApiException e) {
+            log.error("会员卡微信预支付异常 storeId={}, membershipOrderId={}", storeId, membershipOrderId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("会员卡微信预支付异常 storeId={}", storeId, e);
+            return R.fail("预支付失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
+        if (storeId == null || StringUtils.isBlank(outTradeNo)) {
+            return R.fail("门店ID和商户订单号不能为空");
+        }
+        PlatformShopMembershipPaymentOrder paymentOrder = membershipPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return R.fail("支付单不存在");
+        }
+        if (!storeId.equals(paymentOrder.getStoreId())) {
+            return R.fail("支付单与门店不匹配");
+        }
+        PlatformShopMembershipOrder order = platformShopMembershipOrderService.getById(paymentOrder.getMembershipOrderId());
+        if (order == null) {
+            return R.fail("会员卡订单不存在");
+        }
+        if (order.getPayStatus() != null && order.getPayStatus() == PlatformShopMembershipOrder.PAY_STATUS_PAID) {
+            return R.success("支付成功");
+        }
+        try {
+            WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest req = new WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest();
+            req.transactionId = outTradeNo;
+            req.mchid = mchId;
+            WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = searchOrderRun(req);
+            if (response == null) {
+                return R.fail("查询失败");
+            }
+            if ("SUCCESS".equals(response.tradeState)) {
+                Date now = new Date();
+                paymentOrder.setPayStatus(1);
+                paymentOrder.setTradeNo(response.transactionId);
+                paymentOrder.setPayTime(now);
+                paymentOrder.setUpdatedTime(now);
+                membershipPaymentOrderService.updateById(paymentOrder);
+                platformShopMembershipOrderService.applyPaySuccess(order.getId(), now, outTradeNo, response.transactionId, PaymentEnum.WECHAT_PAY.getType());
+                return R.success("支付成功");
+            }
+            if ("CLOSED".equals(response.tradeState)) {
+                return R.fail("交易已关闭");
+            }
+            if ("NOTPAY".equals(response.tradeState) || "USERPAYING".equals(response.tradeState)) {
+                return R.fail("等待用户付款");
+            }
+            return R.fail("订单状态:" + (response.tradeStateDesc != null ? response.tradeStateDesc : response.tradeState));
+        } catch (WXPayUtility.ApiException e) {
+            log.error("会员卡微信查单异常 outTradeNo={}", outTradeNo, e);
+            return R.fail("查询异常:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("会员卡微信查单异常 storeId={}", storeId, e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public String getType() {
+        return PaymentEnum.WECHAT_PAY.getType();
+    }
+
+    @Override
+    public R<String> refundForTest(PlatformShopMembershipPaymentOrder po, String refundAmountYuan, String refundReason) {
+        if (po == null || StringUtils.isBlank(po.getOutTradeNo())) {
+            return R.fail("支付单或商户订单号无效");
+        }
+        if (!PaymentEnum.WECHAT_PAY.getType().equals(po.getPayType())) {
+            return R.fail("非微信会员卡支付单");
+        }
+        if (StringUtils.isBlank(refundAmountYuan)) {
+            return R.fail("退款金额不能为空");
+        }
+        BigDecimal refundYuan;
+        try {
+            refundYuan = new BigDecimal(refundAmountYuan).setScale(2, RoundingMode.HALF_UP);
+        } catch (NumberFormatException e) {
+            return R.fail("退款金额格式不正确");
+        }
+        if (refundYuan.compareTo(BigDecimal.ZERO) <= 0) {
+            return R.fail("退款金额必须大于0");
+        }
+        String outTradeNo = po.getOutTradeNo();
+        long refundFen = refundYuan.multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).longValue();
+        if (refundFen <= 0) {
+            return R.fail("退款金额(分)无效");
+        }
+        long totalFen = po.getPayAmount() != null
+                ? po.getPayAmount().multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).longValue()
+                : 0L;
+        if (totalFen > 0 && refundFen > totalFen) {
+            return R.fail("退款金额不能超过原支付金额");
+        }
+        if (totalFen <= 0) {
+            return R.fail("原支付金额异常,无法计算退款分");
+        }
+        try {
+            WeChatPaymentStrategyImpl.CreateRequest request = new WeChatPaymentStrategyImpl.CreateRequest();
+            request.outTradeNo = outTradeNo;
+            request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
+            request.reason = StringUtils.isNotBlank(refundReason) ? refundReason : "测试退款";
+            if (StringUtils.isNotBlank(businessRefundNotifyUrl)) {
+                request.notifyUrl = businessRefundNotifyUrl;
+            }
+            request.amount = new WeChatPaymentStrategyImpl.AmountReq();
+            request.amount.refund = refundFen;
+            request.amount.total = totalFen;
+            request.amount.currency = "CNY";
+            WeChatPaymentStrategyImpl.Refund response = refundRun(request);
+            String status = response.status != null ? response.status.name() : "UNKNOWN";
+            if ("SUCCESS".equals(status) || "PROCESSING".equals(status)) {
+                membershipPaymentOrderService.markPaymentAndMembershipOrderRefunded(po);
+            }
+            log.warn("会员卡微信退款接口已返回 membershipOrderId={}, outTradeNo={}, outRefundNo={}, status={}, refundId={}",
+                    po.getMembershipOrderId(), outTradeNo, response.outRefundNo, status, response.refundId);
+            return R.data(("SUCCESS".equals(status) || "PROCESSING".equals(status) ? "退款受理并已回写支付单/业务订单状态(PROCESSING 表示处理中)" : "退款返回")
+                    + ",membershipOrderId=" + po.getMembershipOrderId()
+                    + ", status=" + status
+                    + ", refund_id=" + response.refundId
+                    + ", out_refund_no=" + (response.outRefundNo != null ? response.outRefundNo : request.outRefundNo));
+        } catch (WXPayUtility.ApiException e) {
+            log.error("[测试]会员卡微信退款失败 outTradeNo={}", outTradeNo, e);
+            return R.fail("退款失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("[测试]会员卡微信退款异常 membershipOrderId={}", po.getMembershipOrderId(), e);
+            return R.fail("退款失败:" + e.getMessage());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.Refund refundRun(WeChatPaymentStrategyImpl.CreateRequest request) throws IOException {
+        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(mchId, merchantSerialNumber, privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(reqBody, jsonMediaType);
+        reqBuilder.method(POSTMETHOD, body);
+        OkHttpClient client = new OkHttpClient.Builder()
+                .connectTimeout(10, TimeUnit.SECONDS)
+                .readTimeout(30, TimeUnit.SECONDS)
+                .build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).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());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse prePayOrderRun(WeChatPaymentStrategyImpl.CommonPrepayRequest request) throws IOException {
+        String uri = prePayPath;
+        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(mchId, merchantSerialNumber, privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Content-Type", "application/json");
+        MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
+        RequestBody body = RequestBody.create(reqBody, jsonMediaType);
+        reqBuilder.method(POSTMETHOD, body);
+        OkHttpClient client = new OkHttpClient.Builder().build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).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());
+        }
+    }
+
+    private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse searchOrderRun(WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest request) throws IOException {
+        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(request.transactionId));
+        Map<String, Object> args = new HashMap<>();
+        args.put("mchid", mchId);
+        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(mchId, merchantSerialNumber, privateKey, GETMETHOD, uri, null));
+        reqBuilder.method(GETMETHOD, null);
+        OkHttpClient client = new OkHttpClient.Builder()
+                .connectTimeout(10, TimeUnit.SECONDS)
+                .readTimeout(15, TimeUnit.SECONDS)
+                .build();
+        try (Response httpResponse = client.newCall(reqBuilder.build()).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());
+        }
+    }
+}