Procházet zdrojové kódy

Merge branch 'sit' into uat-20260202

dujian před 1 týdnem
rodič
revize
0e9fca89cb
81 změnil soubory, kde provedl 3965 přidání a 918 odebrání
  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. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeMessageVo.java
  10. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeNoticeVo.java
  11. 45 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipAdminRowVo.java
  12. 47 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipBenefitsVo.java
  13. 27 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipOrderCreateResultVo.java
  14. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipOrderItemVo.java
  15. 48 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipRemainingDetailVo.java
  16. 43 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PlatformShopMembershipStatusVo.java
  17. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreMainInfoVo.java
  18. 3 2
      alien-entity/src/main/java/shop/alien/mapper/LifeMessageMapper.java
  19. 26 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipAdminMapper.java
  20. 9 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipMapper.java
  21. 17 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipOrderMapper.java
  22. 9 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipPaymentOrderMapper.java
  23. 9 0
      alien-entity/src/main/java/shop/alien/mapper/PlatformShopMembershipPlanMapper.java
  24. 17 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTrackEventMapper.java
  25. 79 0
      alien-entity/src/main/resources/mapper/PlatformShopMembershipAdminMapper.xml
  26. 28 0
      alien-entity/src/main/resources/mapper/PlatformShopMembershipOrderMapper.xml
  27. 8 0
      alien-gateway/src/main/java/shop/alien/gateway/config/BaseRedisService.java
  28. 22 11
      alien-gateway/src/main/java/shop/alien/gateway/config/JwtTokenFilter.java
  29. 16 2
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java
  30. 2 16
      alien-store-platform/src/main/java/shop/alien/storeplatform/controller/LicenseController.java
  31. 66 137
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/OperationalActivityServiceImpl.java
  32. 66 137
      alien-store-platform/src/main/java/shop/alien/storeplatform/util/AiContentModerationUtil.java
  33. 1 1
      alien-store/src/main/java/shop/alien/store/aspect/AiAuditAspect.java
  34. 10 3
      alien-store/src/main/java/shop/alien/store/controller/LifeMessageController.java
  35. 4 15
      alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java
  36. 79 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipAdminController.java
  37. 83 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipController.java
  38. 106 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipPaymentController.java
  39. 87 0
      alien-store/src/main/java/shop/alien/store/controller/PlatformShopMembershipPlanPlatformController.java
  40. 4 4
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java
  41. 2 46
      alien-store/src/main/java/shop/alien/store/controller/StoreImgController.java
  42. 4 4
      alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java
  43. 23 0
      alien-store/src/main/java/shop/alien/store/platformmembership/PlatformShopMembershipDateUtil.java
  44. 1 1
      alien-store/src/main/java/shop/alien/store/service/LifeMessageService.java
  45. 11 0
      alien-store/src/main/java/shop/alien/store/service/PlatformMembershipPaymentQueryService.java
  46. 16 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipAdminMutationService.java
  47. 23 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipAdminQueryService.java
  48. 17 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderCreateDebounceService.java
  49. 17 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderPaymentTimeoutService.java
  50. 30 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipOrderService.java
  51. 33 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPaymentOrderService.java
  52. 26 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPlanPlatformService.java
  53. 16 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipPlanService.java
  54. 18 0
      alien-store/src/main/java/shop/alien/store/service/PlatformShopMembershipService.java
  55. 5 0
      alien-store/src/main/java/shop/alien/store/service/TrackEventService.java
  56. 2 26
      alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceAuditServiceImpl.java
  57. 2 20
      alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceServiceImpl.java
  58. 15 15
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  59. 6 2
      alien-store/src/main/java/shop/alien/store/service/impl/LifeMessageServiceImpl.java
  60. 85 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformMembershipPaymentQueryServiceImpl.java
  61. 40 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipAdminMutationServiceImpl.java
  62. 57 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipAdminQueryServiceImpl.java
  63. 88 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderCreateDebounceServiceImpl.java
  64. 83 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderPaymentTimeoutServiceImpl.java
  65. 227 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipOrderServiceImpl.java
  66. 121 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPaymentOrderServiceImpl.java
  67. 230 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPlanPlatformServiceImpl.java
  68. 40 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipPlanServiceImpl.java
  69. 264 0
      alien-store/src/main/java/shop/alien/store/service/impl/PlatformShopMembershipServiceImpl.java
  70. 10 21
      alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java
  71. 2 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  72. 3 5
      alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java
  73. 7 12
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffAuditAsyncService.java
  74. 3 4
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffReviewServiceImpl.java
  75. 17 0
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  76. 23 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/PlatformMembershipPaymentStrategy.java
  77. 33 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/PlatformMembershipPaymentStrategyFactory.java
  78. 334 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/impl/PlatformMembershipAlipayPaymentStrategyImpl.java
  79. 440 0
      alien-store/src/main/java/shop/alien/store/strategy/platformMembershipPayment/impl/PlatformMembershipWechatPaymentStrategyImpl.java
  80. 74 130
      alien-store/src/main/java/shop/alien/store/util/ai/AiContentModerationUtil.java
  81. 10 302
      alien-store/src/main/java/shop/alien/store/util/ai/AiVideoModerationUtil.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());

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeMessageVo.java

@@ -15,7 +15,7 @@ import shop.alien.entity.store.LifeMessage;
 @ApiModel(value = "LifeMessageVo对象", description = "消息通知")
 public class LifeMessageVo extends LifeMessage {
 
-    @ApiModelProperty(value = "名称")
+    @ApiModelProperty(value = "展示名称:门店为店铺名(store_name),用户为生活昵称,律师为姓名")
     private String userName;
 
     @ApiModelProperty(value = "头像")

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeNoticeVo.java

@@ -13,7 +13,7 @@ import shop.alien.entity.store.LifeNotice;
 @ApiModel(value = "LifeNoticeVo对象", description = "公告通知")
 public class LifeNoticeVo extends LifeNotice {
 
-    @ApiModelProperty(value = "用户名")
+    @ApiModelProperty(value = "发送方展示名:门店通知为店铺名称(store_name),C端用户为生活昵称")
     private String userName;
 
     @ApiModelProperty(value = "用户头像")

+ 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;
+
 }

+ 3 - 2
alien-entity/src/main/java/shop/alien/mapper/LifeMessageMapper.java

@@ -23,7 +23,7 @@ public interface LifeMessageMapper extends BaseMapper<LifeMessage> {
             "from life_user " +
             "where delete_flag = 0 and user_phone != '' and user_phone in (${userPhones})  " +
             "union " +
-            "select info.id, user.nick_name userName, user.head_img userImage, concat('store_', user.phone) phoneId " +
+            "select info.id, info.store_name userName, user.head_img userImage, concat('store_', user.phone) phoneId " +
             "from store_user user " +
             "join store_info info on info.id = user.store_id " +
             "left join store_img img on img.store_id = info.id and img.img_type = '10' and img.delete_flag = 0 " +
@@ -44,7 +44,7 @@ public interface LifeMessageMapper extends BaseMapper<LifeMessage> {
             "select message.id, message.type, message.phoneId, message.content, message.created_time createdTime, message.is_read, " +
             "case "+
             "when message.flag = 'user' then user.user_name "+
-            "when message.flag = 'store' then suser.nick_name "+
+            "when message.flag = 'store' then sinfo.store_name "+
             "when message.flag = 'lawyer' then luser.name "+
             "else '' "+
             "end as userName, "+
@@ -63,6 +63,7 @@ public interface LifeMessageMapper extends BaseMapper<LifeMessage> {
             "from message_num message " +
             "left join life_user user on message.flag = 'user' and message.phone = user.user_phone and user.delete_flag = 0 " +
             "left join store_user suser on message.flag = 'store' and message.phone = suser.phone and suser.delete_flag = 0 " +
+            "left join store_info sinfo on suser.store_id = sinfo.id and sinfo.delete_flag = 0 " +
             "left join lawyer_user luser on message.flag = 'lawyer' and message.phone = luser.phone and luser.delete_flag = 0 "+
             "left join store_img img on img.store_id = suser.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "${ew.customSqlSegment}")

+ 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>

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

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

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

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

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

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

+ 2 - 16
alien-store-platform/src/main/java/shop/alien/storeplatform/controller/LicenseController.java

@@ -13,9 +13,7 @@ import shop.alien.entity.storePlatform.vo.StoreLicenseHistoryDto;
 import shop.alien.entity.storePlatform.vo.StoreLicenseHistoryVO;
 import shop.alien.mapper.WebAuditMapper;
 import shop.alien.storeplatform.service.LicenseService;
-import shop.alien.storeplatform.util.AiContentModerationUtil;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -33,9 +31,6 @@ public class LicenseController {
 
     private final WebAuditMapper webAuditMapper;
 
-    private final AiContentModerationUtil aiContentModerationUtil;
-
-
     @ApiOperation("获取营业执照图片信息")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({
@@ -164,23 +159,14 @@ public class LicenseController {
     }
 
 
-    @ApiOperation(value = "门店装修-修改娱乐经营许可证", notes = "上传前会同步进行AI图片审核,不通过则直接返回失败原因")
+    @ApiOperation(value = "门店装修-修改娱乐经营许可证", notes = "许可证图片由前端直连AI审核,服务端不再同步审图")
     @PostMapping("/uploadEntertainmentLicence")
     public R<String> uploadEntertainmentLicence(@RequestBody StoreImg storeImg) {
         log.info("StoreInfoController.uploadEntertainmentLicence?storeImg={}", storeImg);
         if (storeImg == null) {
             return R.fail("参数不能为空");
         }
-        // AI 图片审核(同步):不通过则直接返回原因给前台
-        if (storeImg.getImgUrl() != null && !storeImg.getImgUrl().trim().isEmpty()) {
-            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(
-                    null, Collections.singletonList(storeImg.getImgUrl().trim()));
-            if (!auditResult.isPassed()) {
-                String reason = auditResult.getFailureReason() != null ? auditResult.getFailureReason() : "图片审核未通过";
-                log.warn("娱乐经营许可证图片AI审核不通过, storeId={}, reason={}", storeImg.getStoreId(), reason);
-                return R.fail(reason);
-            }
-        }
+        // 许可证配图由前端直连 AI;此处不再调用 aiContentModerationUtil 审图,避免因服务端不审图而拦截上传
         int num = licenseService.uploadEntertainmentLicence(storeImg);
         if (num > 0) {
             WebAudit webAudit = new WebAudit();

+ 66 - 137
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/OperationalActivityServiceImpl.java

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RBucket;
@@ -46,8 +45,6 @@ import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
 import java.util.concurrent.TimeUnit;
 
 
@@ -180,86 +177,27 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
         if (activity.getStatus() == null) {
             activity.setStatus(1);
         }
-        Integer result =0;
-            try {
-                String accessToken = getToken();
-                if (accessToken == null || accessToken.isEmpty()) {
-                    log.error("获取AI服务access_token失败,无法生成促销图片");
-                } else {
-                    // AI登录成功
-                    // 先调用AI进行运营名称和图片的审核,同步调用
-                    String authorization = "Bearer " + accessToken;
-                    
-                    JsonNode auditParam = dto.getAuditParam();
-                    // 如果 auditParam 是字符串,先解析为 JsonNode
-                    if (auditParam != null && auditParam.isTextual()) {
-                        try {
-                            auditParam = objectMapper.readTree(auditParam.asText());
-                        } catch (Exception e) {
-                            log.error("解析 auditParam JSON 字符串失败: {}", auditParam.asText(), e);
-                            auditParam = null;
-                        }
-                    }
-                    
-                    String auditText = (auditParam != null && auditParam.has("text")) ? auditParam.get("text").asText() : "";
-                    JsonNode imagesNode = (auditParam != null) ? auditParam.get("image_urls") : null;
-                    
-                    List<String> imageUrls = (imagesNode != null && imagesNode.isArray())
-                            ? StreamSupport.stream(imagesNode.spliterator(), false)
-                                .map(JsonNode::asText)
-                                .collect(Collectors.toList())
-                            : new ArrayList<>();
-
-                    // 调用同步审核工具类
-                    AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(auditText, imageUrls);
-                    
-                    // 审核结束后,设置审核时间
-                    Date auditTime = new Date();
-                    activity.setAuditStatus(auditResult.isPassed() ? 1 : 2);
-                    activity.setAuditTime(auditTime);
-                    
-                    // 审核通过,根据活动时间自动设置状态
-                    Date currentTime = new Date();
-                    Date startTime = activity.getStartTime();
-                    Date endTime = activity.getEndTime();
-                    
-                    int status;
-                    if (currentTime.before(startTime)) {
-                        // 当前时间在活动开始时间之前,设置为未开始
-                        status = 2;
-                    } else if (currentTime.compareTo(startTime) >= 0 && currentTime.compareTo(endTime) <= 0) {
-                        // 当前时间在活动时间之间,设置为进行中
-                        status = 5;
-                    } else {
-                        // 当前时间在活动结束时间之后,设置为已结束
-                        status = 7;
-                    }
-                    activity.setStatus(status);
-
-                    // 如果审核不通过,记录原因并提前结束
-                    if (!auditResult.isPassed()) {
-                        log.warn("AI内容审核未通过: {}", auditResult.getFailureReason());
-                        failureReasonHolder.set(auditResult.getFailureReason());
-                        activity.setApprovalComments(auditResult.getFailureReason());
-                        activity.setStatus(3);
-//                        return 2; // 返回2表示审核失败
-                    }
+        Integer result = 0;
+        try {
+            String accessToken = getToken();
+            if (accessToken == null || accessToken.isEmpty()) {
+                log.error("获取AI服务 access_token 失败,无法创建活动");
+            } else {
+                AiContentModerationUtil.AuditResult auditResult = runActivityTextModeration(dto.getAuditParam());
+                applyModerationOutcomeToActivity(activity, auditResult);
 
-                    result = activityMapper.insert(activity);
-                    
-                    // AI审核后向商户发送通知
-                    if (result > 0) {
-                        try {
-                            sendActivityAuditNotice(activity, auditResult.isPassed(), auditResult.getFailureReason());
-                        } catch (Exception e) {
-                            log.error("发送活动审核通知失败,activityId={}, error={}", activity.getId(), e.getMessage(), e);
-                        }
+                result = activityMapper.insert(activity);
+                if (result > 0) {
+                    try {
+                        sendActivityAuditNotice(activity, auditResult.isPassed(), auditResult.getFailureReason());
+                    } catch (Exception e) {
+                        log.error("发送活动审核通知失败,activityId={}, error={}", activity.getId(), e.getMessage(), e);
                     }
                 }
-            } catch (Exception e) {
-                // AI调用失败,也可以添加数据
-                log.error("调用AI服务生成促销图片失败", e);
             }
+        } catch (Exception e) {
+            log.error("创建活动/审核流程异常", e);
+        }
             dto.getActivityTitleImg().setBusinessId(activity.getId());
             dto.getActivityTitleImg().setImgType(26);
             imgMapper.insert(dto.getActivityTitleImg());
@@ -312,70 +250,16 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
 
         StoreOperationalActivity activity = new StoreOperationalActivity();
         BeanUtils.copyProperties(dto, activity);
-        Integer result =0;
+        Integer result = 0;
         try {
             String accessToken = getToken();
             if (accessToken == null || accessToken.isEmpty()) {
-                log.error("获取AI服务access_token失败,无法生成促销图片");
+                log.error("获取AI服务 access_token 失败,无法更新活动");
             } else {
-                // AI登录成功
-                // 先调用AI进行运营名称和图片的审核,同步调用
                 String authorization = "Bearer " + accessToken;
 
-                JsonNode auditParam = dto.getAuditParam();
-                // 如果 auditParam 是字符串,先解析为 JsonNode
-                if (auditParam != null && auditParam.isTextual()) {
-                    try {
-                        auditParam = objectMapper.readTree(auditParam.asText());
-                    } catch (Exception e) {
-                        log.error("解析 auditParam JSON 字符串失败: {}", auditParam.asText(), e);
-                        auditParam = null;
-                    }
-                }
-
-                String auditText = (auditParam != null && auditParam.has("text")) ? auditParam.get("text").asText() : "";
-                JsonNode imagesNode = (auditParam != null) ? auditParam.get("image_urls") : null;
-
-                List<String> imageUrls = (imagesNode != null && imagesNode.isArray())
-                        ? StreamSupport.stream(imagesNode.spliterator(), false)
-                        .map(JsonNode::asText)
-                        .collect(Collectors.toList())
-                        : new ArrayList<>();
-
-                // 调用同步审核工具类
-                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(auditText, imageUrls);
-
-                // 审核结束后,设置审核时间
-                Date auditTime = new Date();
-                activity.setAuditStatus(auditResult.isPassed() ? 1 : 2);
-                activity.setAuditTime(auditTime);
-
-                // 审核通过,根据活动时间自动设置状态
-                Date currentTime = new Date();
-                Date startTime = activity.getStartTime();
-                Date endTime = activity.getEndTime();
-
-                int status;
-                if (currentTime.before(startTime)) {
-                    // 当前时间在活动开始时间之前,设置为未开始
-                    status = 2;
-                } else if (currentTime.compareTo(startTime) >= 0 && currentTime.compareTo(endTime) <= 0) {
-                    // 当前时间在活动时间之间,设置为进行中
-                    status = 5;
-                } else {
-                    // 当前时间在活动结束时间之后,设置为已结束
-                    status = 7;
-                }
-                activity.setStatus(status);
-
-                // 如果审核不通过,记录原因并提前结束
-                if (!auditResult.isPassed()) {
-                    log.warn("AI内容审核未通过: {}", auditResult.getFailureReason());
-                    failureReasonHolder.set(auditResult.getFailureReason());
-                    activity.setApprovalComments(auditResult.getFailureReason());
-                    activity.setStatus(3);
-//                        return 2; // 返回2表示审核失败
-                }
+                AiContentModerationUtil.AuditResult auditResult = runActivityTextModeration(dto.getAuditParam());
+                applyModerationOutcomeToActivity(activity, auditResult);
 
                 result = activityMapper.updateById(activity);
                 sendActivityAuditNotice(activity, auditResult.isPassed(), auditResult.getFailureReason());
@@ -687,6 +571,51 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
         return activityMapper.updateById(activity);
     }
 
+    /** auditParam 可为 JSON 对象或 JSON 字符串;其中 image_urls 仅前端使用,服务端只审 text */
+    private AiContentModerationUtil.AuditResult runActivityTextModeration(JsonNode rawAuditParam) {
+        JsonNode param = unwrapAuditParam(rawAuditParam);
+        String text = (param != null && param.has("text")) ? param.get("text").asText() : "";
+        return aiContentModerationUtil.auditContent(text, null);
+    }
+
+    private JsonNode unwrapAuditParam(JsonNode auditParam) {
+        if (auditParam == null || !auditParam.isTextual()) {
+            return auditParam;
+        }
+        try {
+            return objectMapper.readTree(auditParam.asText());
+        } catch (Exception e) {
+            log.error("解析 auditParam 失败: {}", auditParam.asText(), e);
+            return null;
+        }
+    }
+
+    /** 写入审核结果、业务时间轴状态;不通过时 status=3 并记录原因 */
+    private void applyModerationOutcomeToActivity(StoreOperationalActivity activity,
+                                                  AiContentModerationUtil.AuditResult auditResult) {
+        activity.setAuditStatus(auditResult.isPassed() ? 1 : 2);
+        activity.setAuditTime(new Date());
+
+        Date now = new Date();
+        Date start = activity.getStartTime();
+        Date end = activity.getEndTime();
+        int timelineStatus;
+        if (now.before(start)) {
+            timelineStatus = 2;
+        } else if (now.compareTo(start) >= 0 && now.compareTo(end) <= 0) {
+            timelineStatus = 5;
+        } else {
+            timelineStatus = 7;
+        }
+        activity.setStatus(timelineStatus);
+
+        if (!auditResult.isPassed()) {
+            failureReasonHolder.set(auditResult.getFailureReason());
+            activity.setApprovalComments(auditResult.getFailureReason());
+            activity.setStatus(3);
+        }
+    }
+
     /**
      * 发送活动审核通知给商户
      *

+ 66 - 137
alien-store-platform/src/main/java/shop/alien/storeplatform/util/AiContentModerationUtil.java

@@ -1,25 +1,28 @@
 package shop.alien.storeplatform.util;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.http.*;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestTemplate;
 
-import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
 /**
- * 通用图文审核工具类
- * 调用AI图文审核接口审核文本和图片内容
+ * 服务端文本内容审核(moderate-url)。无有效文本则直接通过;图/视频不参与,第二参请传 {@code null}。
  */
 @Slf4j
 @Component
@@ -27,20 +30,18 @@ import java.util.Set;
 @RequiredArgsConstructor
 public class AiContentModerationUtil {
 
+    private static final String RESULTS = "results";
+    private static final String TEXT_INPUT_MARKER = "[TEXT INPUT]";
+    private static final String GENERIC_FAILURE = "审核异常";
+
     private final RestTemplate restTemplate;
 
-    /**
-     * AI审核接口地址
-     */
     @Value("${ai.service.moderate-url}")
     private String moderateUrl;
 
-    /**
-     * 审核结果类
-     */
     public static class AuditResult {
-        private boolean passed;
-        private String failureReason;
+        private final boolean passed;
+        private final String failureReason;
 
         public AuditResult(boolean passed, String failureReason) {
             this.passed = passed;
@@ -56,157 +57,85 @@ public class AiContentModerationUtil {
         }
     }
 
-    /**
-     * 审核文本和图片内容
-     *
-     * @param text 文本内容
-     * @param imageUrls 图片URL列表
-     * @return 审核结果
-     */
     public AuditResult auditContent(String text, List<String> imageUrls) {
-        log.info("开始审核内容:text={}, imageCount={}",
-                text, imageUrls != null ? imageUrls.size() : 0);
-
+        if (imageUrls != null && !imageUrls.isEmpty()) {
+            log.debug("auditContent: 忽略 imageUrls(size={}),请统一传 null", imageUrls.size());
+        }
+        if (!StringUtils.hasText(text)) {
+            log.debug("auditContent: 无有效文本,跳过 moderate");
+            return new AuditResult(true, null);
+        }
         try {
-            // 如果没有任何内容,直接返回审核通过,避免400错误
-            if (!StringUtils.hasText(text) && (imageUrls == null || imageUrls.isEmpty())) {
-                log.info("审核内容为空,自动跳过审核");
-                return new AuditResult(true, null);
-            }
-
-            // 调用审核接口
-            return callModerateApi(text, imageUrls);
-
+            return requestModerate(text);
         } catch (Exception e) {
-            log.error("审核内容异常", e);
-            return new AuditResult(false, "审核异常");
+            log.error("auditContent 异常", e);
+            return new AuditResult(false, GENERIC_FAILURE);
         }
     }
 
-    /**
-     * 调用AI审核接口
-     *
-     * @param text 文本内容
-     * @param imageUrls 图片URL列表
-     * @return 审核结果
-     */
-    private AuditResult callModerateApi(String text, List<String> imageUrls) {
+    private AuditResult requestModerate(String text) {
         try {
-            // 构建 form-data 请求头
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
+            body.add("text", text);
 
-            // 构建 form-data 请求体
-            MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
-            if (StringUtils.hasText(text)) {
-                formData.add("text", text);
-            }
-            if (imageUrls != null) {
-                for (String url : imageUrls) {
-                    if (StringUtils.hasText(url)) {
-                        // 支持多张图片:同一个 key 重复多次
-                        formData.add("image_urls", url);
-                    }
-                }
-            }
-
-            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
+            ResponseEntity<String> response =
+                    restTemplate.postForEntity(moderateUrl, new HttpEntity<>(body, headers), String.class);
 
-            log.info("调用AI审核接口:url={}, text={}, imageCount={}",
-                    moderateUrl, text, imageUrls != null ? imageUrls.size() : 0);
-
-            // 发送请求
-            ResponseEntity<String> response = restTemplate.postForEntity(moderateUrl, requestEntity, String.class);
-
-            if (response.getStatusCode() == HttpStatus.OK) {
-                String responseBody = response.getBody();
-                log.info("AI审核接口响应:{}", responseBody);
-
-                if (StringUtils.hasText(responseBody)) {
-                    JSONObject jsonResponse = JSONObject.parseObject(responseBody);
-                    return parseAuditResult(jsonResponse);
-                } else {
-                    log.error("AI审核接口返回空响应");
-                    return new AuditResult(false, "审核异常");
-                }
-            } else {
-                log.error("AI审核接口调用失败,状态码:{}", response.getStatusCode());
-                return new AuditResult(false, "审核异常");
+            if (response.getStatusCode() != HttpStatus.OK) {
+                log.error("moderate HTTP 非 200: {}", response.getStatusCode());
+                return new AuditResult(false, GENERIC_FAILURE);
             }
-
+            String responseBody = response.getBody();
+            if (!StringUtils.hasText(responseBody)) {
+                log.error("moderate 响应体为空");
+                return new AuditResult(false, GENERIC_FAILURE);
+            }
+            log.debug("moderate 响应: {}", responseBody);
+            return parseModerateResponse(JSONObject.parseObject(responseBody));
         } catch (Exception e) {
-            log.error("调用AI审核接口异常", e);
-            return new AuditResult(false, "审核异常");
+            log.error("调用 moderate 失败", e);
+            return new AuditResult(false, GENERIC_FAILURE);
         }
     }
 
-    /**
-     * 解析审核结果
-     *
-     * @param jsonResponse API响应JSON
-     * @return 审核结果
-     */
-    private AuditResult parseAuditResult(JSONObject jsonResponse) {
+    private AuditResult parseModerateResponse(JSONObject json) {
         try {
-            // API返回格式:
-            // {
-            //     "results": [
-            //         {
-            //             "filename": "[TEXT INPUT]" 或 URL,
-            //             "flagged": true/false,
-            //             "risk_level": "high"/"safe"等,
-            //             "violation_categories": [...],
-            //             "reason": "原因描述"
-            //         }
-            //     ],
-            //     "summary": "处理摘要"
-            // }
-
-            com.alibaba.fastjson2.JSONArray results = jsonResponse.getJSONArray("results");
+            JSONArray results = json.getJSONArray(RESULTS);
             if (results == null || results.isEmpty()) {
-                log.warn("AI审核接口返回结果为空");
-                return new AuditResult(false, "审核异常");
+                log.warn("moderate 返回 results 为空");
+                return new AuditResult(false, GENERIC_FAILURE);
             }
 
-            // 检查是否有任何项目被标记为违规
-            Set<String> violationReasons = new LinkedHashSet<>();
-//            List<String> violationReasons = new ArrayList<>();
-            boolean hasViolations = false;
-
+            Set<String> reasons = new LinkedHashSet<>();
             for (int i = 0; i < results.size(); i++) {
-                JSONObject result = results.getJSONObject(i);
-                if (result != null) {
-                    Boolean flagged = result.getBoolean("flagged");
-                    String filename = result.getString("filename");
-
-                    if (flagged != null && flagged) {
-                        hasViolations = true;
-                        // 根据filename判断是文本还是图片违规
-                        if ("[TEXT INPUT]".equals(filename)) {
-                            violationReasons.add("含违规词汇");
-                        } else if (StringUtils.hasText(filename) && filename.startsWith("http")) {
-                            violationReasons.add("图片内容违规");
-                        } else {
-                            violationReasons.add("内容违规");
-                        }
-                    }
+                JSONObject item = results.getJSONObject(i);
+                if (item == null || !Boolean.TRUE.equals(item.getBoolean("flagged"))) {
+                    continue;
                 }
+                reasons.add(flaggedItemReason(item.getString("filename")));
             }
 
-            if (hasViolations) {
-                // 有违规内容,审核失败
-                String failureReason = String.join("; ", violationReasons);
-                log.warn("AI审核失败:{}", failureReason);
-                return new AuditResult(false, failureReason);
-            } else {
-                // 所有内容都安全,审核通过
-                log.info("AI审核通过:所有内容安全");
+            if (reasons.isEmpty()) {
                 return new AuditResult(true, null);
             }
-
+            String joined = String.join("; ", reasons);
+            log.warn("moderate 命中违规: {}", joined);
+            return new AuditResult(false, joined);
         } catch (Exception e) {
-            log.error("解析审核结果异常", e);
-            return new AuditResult(false, "审核异常");
+            log.error("解析 moderate 响应失败", e);
+            return new AuditResult(false, GENERIC_FAILURE);
+        }
+    }
+
+    private static String flaggedItemReason(String filename) {
+        if (TEXT_INPUT_MARKER.equals(filename)) {
+            return "含违规词汇";
+        }
+        if (StringUtils.hasText(filename) && filename.startsWith("http")) {
+            return "图片内容违规";
         }
+        return "内容违规";
     }
-}
+}

+ 1 - 1
alien-store/src/main/java/shop/alien/store/aspect/AiAuditAspect.java

@@ -79,7 +79,7 @@ public class AiAuditAspect {
     private boolean performAiAudit(String payload, List<String> imageUrls, Object[] args) {
         try {
             // token 目前仅预留,如后续需要可添加到header或payload
-            AiContentModerationUtil.AuditResult result = aiContentModerationUtil.auditContent(payload, imageUrls);
+            AiContentModerationUtil.AuditResult result = aiContentModerationUtil.auditContent(payload, null);
             if (result == null) {
                 log.warn("AI审核返回为空,视为未通过");
                 applyFailureReason(args, "审核异常");

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

@@ -27,11 +27,18 @@ public class LifeMessageController {
 
     @ApiOperation("消息列表")
     @ApiOperationSupport(order = 1)
-    @ApiImplicitParams({@ApiImplicitParam(name = "receiverId", value = "当前登录人", dataType = "String", paramType = "query"), @ApiImplicitParam(name = "friendType", value = "聊天类型  0-搜索 1-聊过 2-未聊过", dataType = "Integer", paramType = "query"), @ApiImplicitParam(name = "search", value = "搜索字段", dataType = "Integer", paramType = "query")})
+    @ApiImplicitParams(
+            {
+                    @ApiImplicitParam(name = "receiverId", value = "当前登录人", dataType = "String", paramType = "query"),
+                    @ApiImplicitParam(name = "friendType", value = "聊天类型  0-搜索 1-聊过 2-未聊过", dataType = "Integer", paramType = "query"),
+                    @ApiImplicitParam(name = "userName", value = "按展示名过滤(门店为店铺名、用户为昵称)", dataType = "String", paramType = "query"),
+                    @ApiImplicitParam(name = "search", value = "搜索关键词(昵称/店主姓名/店铺名)", dataType = "String", paramType = "query")
+            }
+                      )
     @GetMapping("/getMessageList")
-    public R<List<LifeMessageVo>> getMessageList(@RequestParam String receiverId, @RequestParam int friendType, String search) throws Exception {
+    public R<List<LifeMessageVo>> getMessageList(@RequestParam String receiverId, @RequestParam int friendType, String search, String userName) throws Exception {
         log.info("LifeMessageController.getMessageList?receiverId={}, friendType={}, search={}", receiverId, friendType, search);
-        return R.data(lifeMessageService.getMessageList(receiverId, friendType, search));
+        return R.data(lifeMessageService.getMessageList(receiverId, friendType, search, userName));
     }
 
     @ApiOperation("未读消息与通知数量")

+ 4 - 15
alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java

@@ -36,9 +36,6 @@ public class LifeUserDynamicsController {
     @Autowired
     private TextModerationUtil textModerationUtil;
 
-    @Autowired
-    private ImageModerationUtil imageModerationUtil;
-
     @ApiOperation("社区列表")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "分页页数", dataType = "String", paramType = "query", required = true),
@@ -95,7 +92,6 @@ public class LifeUserDynamicsController {
             return R.data(3);
         }*/
         try {
-
             List<String> servicesList = Lists.newArrayList();
             servicesList.add(TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService());
             servicesList.add(TextReviewServiceEnum.LLM_QUERY_MODERATION.getService());
@@ -103,21 +99,14 @@ public class LifeUserDynamicsController {
             if ("high".equals(textCheckResult.getRiskLevel())) {
                 return R.data(2);
             }
-
-            List<String> imgServicesList = Lists.newArrayList();
-            imgServicesList.add(ImageReviewServiceEnum.TONALITY_IMPROVE.getService());
-            imgServicesList.add(ImageReviewServiceEnum.AIGC_CHECK.getService());
-            ImageModerationResultVO response = imageModerationUtil.productPublishCheck(lifeUserDynamics.getImagePath(),imgServicesList);
-            if ("high".equals(response.getRiskLevel())) {
-                return R.data(3);
-            }
-            lifeUserDynamics.setUpdatedTime(new Date());
+            // 配图由前端直连AI审核,服务端只做动态正文文本审核(ImageModerationUtil已去掉)
+            lifeUserDynamics.setUpdatedTime(new java.util.Date());
             int cnt = lifeUserDynamicsService.addOrUpdateStore(lifeUserDynamics);
             if (cnt == 0) {
                 return R.data(1);
             }
-        } catch (Exception e) {
-            log.error("LifeUserDynamicsController.addOrUpdate ERROR Msg={}", e.getMessage());
+        } catch (Exception ex) {
+            log.error("LifeUserDynamicsController.addOrUpdate ERROR Msg={}", ex.getMessage());
             return R.data(1);
         }
         return R.data(0);

+ 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());
+        }
+    }
+}

+ 4 - 4
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java

@@ -156,9 +156,9 @@ public class StoreCuisineController {
                 }
             }
 
-            // 执行AI审核
+            // AI:菜品图前端审;auditContent 仅 textContent,第二参固定 null(imageUrls 仍用于是否进入审核分支)
             if (StringUtils.isNotEmpty(textContent.toString()) || imageUrls.size() > 0) {
-                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), imageUrls);
+                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), null);
                 
                 LambdaUpdateWrapper<StoreCuisine> auditUpdateWrapper = new LambdaUpdateWrapper<>();
                 auditUpdateWrapper.eq(StoreCuisine::getId, savedCuisine.getId());
@@ -283,9 +283,9 @@ public class StoreCuisineController {
                 }
             }
 
-            // 执行AI审核
+            // AI:菜品图前端审;auditContent 仅 textContent,第二参固定 null(imageUrls 仍用于是否进入审核分支)
             if (StringUtils.isNotEmpty(textContent.toString()) || imageUrls.size() > 0) {
-                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), imageUrls);
+                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), null);
 //                boolean allPassed = (auditResult != null);
                 
                 LambdaUpdateWrapper<StoreCuisine> auditUpdateWrapper = new LambdaUpdateWrapper<>();

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

@@ -16,7 +16,6 @@ import shop.alien.store.service.StoreOfficialAlbumService;
 import shop.alien.store.util.GroupConstant;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.*;
 import java.util.stream.Collectors;
@@ -40,7 +39,6 @@ public class StoreImgController {
     private final StoreImgService storeImgService;
     private final StoreInfoService storeInfoService;
     private final StoreOfficialAlbumService storeOfficialAlbumService;
-    private final AiContentModerationUtil aiContentModerationUtil;
     @Qualifier("imgAuditExecutor")
     private final ExecutorService imgAuditExecutor;
 
@@ -92,12 +90,9 @@ public class StoreImgController {
         }
         Integer imgType = storeImgInfoVo.getImgType();
         Integer storeId = storeImgInfoVo.getStoreId();
-        List<String> imageUrls = storeImgList.stream()
-                .map(StoreImg::getImgUrl)
-                .collect(Collectors.toList());
-        // 审核与环境相册查询并行,减少总耗时(imgType==4 时相册查询与审核同时进行)
+        // 图片由前端直连 AI 审核;服务端不再对门店图调用 moderate,避免保存被误拦截(仅与环境相册查询并行占位)
         CompletableFuture<AiContentModerationUtil.AuditResult> auditFuture = CompletableFuture
-                .supplyAsync(() -> auditImagesInParallel(imageUrls), imgAuditExecutor);
+                .completedFuture(new AiContentModerationUtil.AuditResult(true, null));
         CompletableFuture<List<StoreOfficialAlbum>> albumFuture = (imgType != null && imgType == 4)
                 ? CompletableFuture.supplyAsync(() -> storeOfficialAlbumService.lambdaQuery()
                         .eq(StoreOfficialAlbum::getStoreId, storeId)
@@ -346,43 +341,4 @@ public class StoreImgController {
         return R.data(storeImgService.getByCover(storeId, imgType));
     }
 
-    /**
-     * 使用共享线程池并行审核,每张图片单独审核,线程数不超过 IMG_AUDIT_MAX_PARALLEL
-     *
-     * @param imageUrls 图片URL列表
-     * @return 合并后的审核结果,任一张不通过则整体不通过
-     */
-    private AiContentModerationUtil.AuditResult auditImagesInParallel(List<String> imageUrls) {
-        if (imageUrls == null || imageUrls.isEmpty()) {
-            return new AiContentModerationUtil.AuditResult(true, null);
-        }
-        try {
-            List<CompletableFuture<AiContentModerationUtil.AuditResult>> futures = imageUrls.stream()
-                    .map(url -> CompletableFuture.supplyAsync(
-                            () -> aiContentModerationUtil.auditContent(null, Collections.singletonList(url)),
-                            imgAuditExecutor))
-                    .collect(Collectors.toList());
-            String firstFailureReason = null;
-            for (CompletableFuture<AiContentModerationUtil.AuditResult> future : futures) {
-                AiContentModerationUtil.AuditResult result = future.get(30, TimeUnit.SECONDS);
-                if (!result.isPassed() && firstFailureReason == null) {
-                    firstFailureReason = result.getFailureReason();
-                }
-            }
-            return firstFailureReason != null
-                    ? new AiContentModerationUtil.AuditResult(false, firstFailureReason)
-                    : new AiContentModerationUtil.AuditResult(true, null);
-        } catch (TimeoutException e) {
-            log.error("图片审核超时", e);
-            return new AiContentModerationUtil.AuditResult(false, "审核超时");
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            log.error("图片审核被中断", e);
-            return new AiContentModerationUtil.AuditResult(false, "审核被中断");
-        } catch (ExecutionException e) {
-            log.error("图片审核异常", e);
-            return new AiContentModerationUtil.AuditResult(false, "审核异常");
-        }
-    }
-
 }

+ 4 - 4
alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java

@@ -185,9 +185,9 @@ public class StorePriceController {
                 }
             }
 
-            // 执行AI审核
+            // AI:价目图前端审;auditContent 仅 textContent,第二参固定 null
             if (StringUtils.isNotEmpty(textContent.toString()) || imageUrls.size() > 0) {
-                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), imageUrls);
+                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), null);
 //                boolean allPassed = (auditResult != null);
                 
                 LambdaUpdateWrapper<StorePrice> auditUpdateWrapper = new LambdaUpdateWrapper<>();
@@ -314,9 +314,9 @@ public class StorePriceController {
                 }
             }
 
-            // 执行AI审核
+            // AI:价目图前端审;auditContent 仅 textContent,第二参固定 null
             if (StringUtils.isNotEmpty(textContent.toString()) || imageUrls.size() > 0) {
-                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), imageUrls);
+                AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(textContent.toString(), null);
 //                boolean allPassed = (auditResult != null);
                 
                 LambdaUpdateWrapper<StorePrice> auditUpdateWrapper = new LambdaUpdateWrapper<>();

+ 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();
+    }
+}

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

@@ -11,7 +11,7 @@ import java.util.List;
 
 public interface LifeMessageService extends IService<LifeMessage> {
 
-    List<LifeMessageVo> getMessageList(String receiverId, int friendType, String search) throws Exception;
+    List<LifeMessageVo> getMessageList(String receiverId, int friendType, String search,String userName) throws Exception;
 
     LifeMessageVo getStrangerMessageNum(String receiverId) throws Exception ;
 

+ 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);
 }

+ 2 - 26
alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceAuditServiceImpl.java

@@ -10,9 +10,6 @@ import shop.alien.mapper.BarPerformanceMapper;
 import shop.alien.store.service.BarPerformanceAuditService;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * 酒吧演出审核服务实现类
  * 负责异步执行AI内容审核
@@ -53,30 +50,9 @@ public class BarPerformanceAuditServiceImpl implements BarPerformanceAuditServic
                 textContent.append(barPerformance.getPerformanceNotice());
             }
             
-            // 组装图片URL列表
-            List<String> imageUrls = new ArrayList<>();
-            if (StringUtils.isNotEmpty(barPerformance.getPerformancePoster())) {
-                // 演出海报(可能有多张,逗号分隔)
-                String[] posterUrls = barPerformance.getPerformancePoster().split(",");
-                for (String url : posterUrls) {
-                    if (StringUtils.isNotEmpty(url.trim())) {
-                        imageUrls.add(url.trim());
-                    }
-                }
-            }
-            if (StringUtils.isNotEmpty(barPerformance.getPerformanceDetail())) {
-                // 演出详情图片(可能有多张,逗号分隔)
-                String[] detailUrls = barPerformance.getPerformanceDetail().split(",");
-                for (String url : detailUrls) {
-                    if (StringUtils.isNotEmpty(url.trim())) {
-                        imageUrls.add(url.trim());
-                    }
-                }
-            }
-            
-            // 调用AI审核接口
+            // 配图前端审;auditContent 第二参固定 null
             AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(
-                    textContent.toString().trim(), imageUrls);
+                    textContent.toString().trim(), null);
             
             // 根据AI审核结果更新审核状态和拒绝原因
             BarPerformance auditUpdate = new BarPerformance();

+ 2 - 20
alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceServiceImpl.java

@@ -202,27 +202,9 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
             throw new IllegalArgumentException("图文详情文字不能超过300个字符");
         }
 
-        // 8. 图文内容审核(调用AI内容审核接口)- 新增或更新时都必须进行
-        // - 文本:名称 + 风格 + 演出详情文字 + 演出须知(文本字段,不包含图片URL)
-        // - 图片:海报URL(如果有) + 图文详情图片URL(如果有)
+        // 8. AI:海报/详情图前端审;auditContent 仅传文案,第二参固定 null
         String moderationText = buildModerationText(barPerformance);
-        List<String> imageUrls = new ArrayList<>();
-        // 添加演出海报到图片审核列表
-        if (StringUtils.isNotEmpty(barPerformance.getPerformancePoster())) {
-            imageUrls.add(barPerformance.getPerformancePoster());
-        }
-        // 添加图文详情图片到图片审核列表(performanceDetail是图片URL列表,只做图片审核)
-        if (StringUtils.isNotEmpty(barPerformance.getPerformanceDetail())) {
-            String[] detailImages = barPerformance.getPerformanceDetail().split(",");
-            for (String imageUrl : detailImages) {
-                if (StringUtils.isNotEmpty(imageUrl.trim())) {
-                    imageUrls.add(imageUrl.trim());
-                }
-            }
-        }
-        // 调用AI内容审核接口,文本和图片分开审核
-        // 审核失败时保存数据但标记为审核拒绝状态
-        AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(moderationText, imageUrls);
+        AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(moderationText, null);
         if (auditResult == null || !auditResult.isPassed()) {
             // AI审核失败,设置审核状态为2(审核拒绝)并记录拒绝原因,但仍然保存数据
             String failureReason = (auditResult != null && StringUtils.isNotEmpty(auditResult.getFailureReason()))

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

@@ -141,12 +141,10 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             List<String> imageUrls = urlCategoryMap.get("image");
             List<String> videoUrls = urlCategoryMap.get("video");
             
-            // 3. 内容审核(基础审核,必须通过)
-            // 文本、图片、视频必须符合法律法规要求
+            // 3. 内容审核:评价正文走服务端 moderate;配图/视频由前端直连 AI(util 忽略图,视频审核 util 恒通过)
             AiContentModerationUtil.AuditResult contentAuditResult = new AiContentModerationUtil.AuditResult(true, "");
-            // 只要有内容或图片,就必须进行内容审核
             if (StringUtils.isNotEmpty(commonRating.getContent()) || !imageUrls.isEmpty()) {
-                contentAuditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), imageUrls);
+                contentAuditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), null);
             }
             
             // 内容审核不通过,直接判定为审核不通过
@@ -310,7 +308,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 }
             }
             
-            // 7. 如果有视频,进行异步视频审核
+            // 7. 视频由前端审;此处异步分支仍更新状态但 auditVideos 已不再请求后端视频接口(见 AiVideoModerationUtil)
             if (!videoUrls.isEmpty()) {
                 CompletableFuture.runAsync(() -> {
                     AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
@@ -421,17 +419,19 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             // 更新门店评价信息
             StoreInfoScoreVo storeInfoScoreVo = commonRatingMapper.getCommentCountAndScoreInfo(commonRating.getBusinessType(),businessId);
             double total = storeInfoScoreVo.getTotal();
-            double scoreAvg = (total == 0 ? 0 : storeInfoScoreVo.getScore() / total);
-            double scoreOne = (total == 0 ? 0 : storeInfoScoreVo.getScoreOne() / total);
-            double scoreTwo = (total == 0 ? 0 : storeInfoScoreVo.getScoreTwo() / total);
-            double scoreThree = (total == 0 ? 0 : storeInfoScoreVo.getScoreThree() / total);
             StoreInfo storeInfo = new StoreInfo();
-            storeInfo.setId(businessId);
-            storeInfo.setScoreAvg(new BigDecimal(scoreAvg).setScale(2, RoundingMode.HALF_UP).doubleValue());
-            storeInfo.setScoreOne(new BigDecimal(scoreOne).setScale(2, RoundingMode.HALF_UP).doubleValue());
-            storeInfo.setScoreTwo(new BigDecimal(scoreTwo).setScale(2, RoundingMode.HALF_UP).doubleValue());
-            storeInfo.setScoreThree(new BigDecimal(scoreThree).setScale(2, RoundingMode.HALF_UP).doubleValue());
-            storeInfoMapper.updateById(storeInfo);
+            if(total >= 10){
+                double scoreAvg = (total == 0 ? 0 : storeInfoScoreVo.getScore() / total);
+                double scoreOne = (total == 0 ? 0 : storeInfoScoreVo.getScoreOne() / total);
+                double scoreTwo = (total == 0 ? 0 : storeInfoScoreVo.getScoreTwo() / total);
+                double scoreThree = (total == 0 ? 0 : storeInfoScoreVo.getScoreThree() / total);
+                storeInfo.setId(businessId);
+                storeInfo.setScoreAvg(new BigDecimal(scoreAvg).setScale(2, RoundingMode.HALF_UP).doubleValue());
+                storeInfo.setScoreOne(new BigDecimal(scoreOne).setScale(2, RoundingMode.HALF_UP).doubleValue());
+                storeInfo.setScoreTwo(new BigDecimal(scoreTwo).setScale(2, RoundingMode.HALF_UP).doubleValue());
+                storeInfo.setScoreThree(new BigDecimal(scoreThree).setScale(2, RoundingMode.HALF_UP).doubleValue());
+                storeInfoMapper.updateById(storeInfo);
+            }
             StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getStoreId, storeInfo.getId()).eq(StoreUser::getDeleteFlag, 0));
 
             // 如果差评,则发送差评提醒

+ 6 - 2
alien-store/src/main/java/shop/alien/store/service/impl/LifeMessageServiceImpl.java

@@ -48,7 +48,7 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
     private final LawyerConsultationOrderMapper lawyerConsultationOrderMapper;
 
     @Override
-    public List<LifeMessageVo> getMessageList(String receiverId, int friendType, String search) throws Exception {
+    public List<LifeMessageVo> getMessageList(String receiverId, int friendType, String search ,String userName) throws Exception {
         try {
             if (receiverId == null) {
                 return new ArrayList<>();
@@ -69,7 +69,7 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
 
             // 通过搜索查询的情况下  不区分是否聊过  搜索所有消息
             if (0 == friendType && StringUtil.isNotEmpty(search)) {
-                wrapper.apply("(user.user_name like '%" + search + "%' or suser.name like '%" + search + "%')");
+                wrapper.apply("(user.user_name like '%" + search + "%' or suser.name like '%" + search + "%' or sinfo.store_name like '%" + search + "%')");
                 // 聊过
             } else if (1 == friendType) {
                 wrapper.apply("message.phoneId in ( " +
@@ -191,7 +191,11 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
                     lifeMessagePageResultList.add(messageVo);
                 }
             }
+            if (userName == null){
+                return lifeMessagePageResultList;
+            }
 
+            lifeMessagePageResultList = lifeMessagePageResultList.stream().filter(item -> item.getUserName().contains(userName)).collect(Collectors.toList());
             return lifeMessagePageResultList;
         } catch (Exception e) {
             log.error("LifeMessageServiceImpl.getMessageList Error Mgs={}", e.getMessage());

+ 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;
+    }
+}

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

@@ -321,7 +321,7 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
         wrapper.eq(StoreClockIn::getId, id);
         int updateResult = storeClockInMapper.update(null, wrapper);
         
-        // 2. 异步调用AI接口审核图片
+        // 2. 异步:配图由前端直连 AI;服务端仅审核 maybeAiContent 文字(无文字则视为通过)
         StoreClockIn storeClockIn = storeClockInMapper.selectById(id);
         if (storeClockIn == null) {
             log.warn("打卡记录不存在,id={}", id);
@@ -338,23 +338,13 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
         final Integer clockInId = id;
         final Integer userId = storeClockIn.getUserId();
         
-        List<String> imgList = new ArrayList<>();
-        if (StringUtils.isNotBlank(img)) {
-            String[] imgArray = img.split(",");
-            for (String imgUrl : imgArray) {
-                String trimmed = imgUrl.trim();
-                if (StringUtils.isNotBlank(trimmed)) {
-                    imgList.add(trimmed);
-                }
-            }
-        }
-        
-        // 异步执行AI审核任务
+        // 异步执行:仅文本走 AiContentModerationUtil(图片由前端审,不再传 imgList)
         CompletableFuture.runAsync(() -> {
             AiContentModerationUtil.AuditResult imgAuditResult = null;
             try {
+                String textForAudit = StringUtils.isNotBlank(aiContent) ? aiContent.trim() : null;
                 imgAuditResult = CompletableFuture.supplyAsync(
-                        () -> aiContentModerationUtil.auditContent(null, imgList),
+                        () -> aiContentModerationUtil.auditContent(textForAudit, null),
                         imgAuditExecutor
                 ).get();
 
@@ -363,26 +353,25 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
                 updateWrapper.eq(StoreClockIn::getId, clockInId);
                 
                 if (imgAuditResult != null && imgAuditResult.isPassed()) {
-                    // 审核通过,修改状态,打卡可显示在列表中
+                    // 配文文字审核通过(配图由前端审)
                     updateWrapper.set(StoreClockIn::getCheckFlag, 2); // 2-审核通过
                     updateWrapper.set(StoreClockIn::getReason, null); // 清除拒绝原因
                     storeClockInMapper.update(null, updateWrapper);
-                    log.info("打卡图片审核通过,打卡ID:{}", clockInId);
+                    log.info("打卡配文审核通过,打卡ID:{}", clockInId);
                 } else {
-                    // 审核拒绝,修改状态,打卡不可显示在列表中,通知用户,打卡审核不通过
                     String rejectReason = (imgAuditResult != null && StringUtils.isNotEmpty(imgAuditResult.getFailureReason()))
                             ? imgAuditResult.getFailureReason()
-                            : "图片内容不符合规范";
-                    updateWrapper.set(StoreClockIn::getCheckFlag, 3); // 2-审核完成(但审核未通过)
+                            : "文字内容不符合规范";
+                    updateWrapper.set(StoreClockIn::getCheckFlag, 3); // 3-审核拒绝
                     updateWrapper.set(StoreClockIn::getReason, rejectReason); // 记录拒绝原因
                     storeClockInMapper.update(null, updateWrapper);
-                    log.warn("打卡图片审核拒绝,打卡ID:{},原因:{}", clockInId, rejectReason);
+                    log.warn("打卡配文审核拒绝,打卡ID:{},原因:{}", clockInId, rejectReason);
                     
                     // 通知用户打卡审核不通过
                     sendAuditRejectNotification(clockInId, userId, phoneId, rejectReason);
                 }
             } catch (Exception e) {
-                log.error("图片审核接口调用异常,打卡ID:{}", clockInId, e);
+                log.error("打卡配文审核异常,打卡ID:{}", clockInId, e);
                 // 审核异常时,保持审核中状态,记录错误原因
                 LambdaUpdateWrapper<StoreClockIn> updateWrapper = new LambdaUpdateWrapper<>();
                 updateWrapper.eq(StoreClockIn::getId, clockInId);

+ 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;
     }

+ 3 - 5
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java

@@ -184,12 +184,10 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
             webSocketProcess.sendMessage("store_" + storeUser.getPhone(), JSONObject.from(websocketVo).toJSONString());
 
 
-            // TODO 审核文本和图片内容是否违规
-            // 一次遍历完成分类,避免多次流式处理
+            // 附件图/视频由前端直连 AI;服务端仅审 detailedRequirement 文本(图片 URL 在 util 内不传 moderate)
             Map<String, List<String>> urlCategoryMap = classifyUrls(dto.getAttachmentUrls());
             List<String> videoUrls = urlCategoryMap.get("video");
-            // 1.调用文本+图片审核接口 ai为同步接口
-            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(requirement.getDetailedRequirement(), urlCategoryMap.get("image"));
+            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(requirement.getDetailedRequirement(), null);
             if (!auditResult.isPassed()) {
                 requirement.setAuditStatus(2);
                 requirement.setAuditReason(auditResult.getFailureReason());
@@ -208,7 +206,7 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
                         sendAuditNotification(storeUser.getPhone(), 1, null, "text_image");
                     }
                 } else {
-                    // 异步调用视频审核接口,图片审核通过后调用(核心优化)
+                    // 有视频时走异步分支;视频不在服务端审(前端已审),auditVideos 恒通过
                     // 保存storeUser信息供异步任务使用
                     final StoreUser finalStoreUser = storeUser;
                     CompletableFuture.runAsync(() -> {

+ 7 - 12
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffAuditAsyncService.java

@@ -85,40 +85,35 @@ public class StoreStaffAuditAsyncService {
                 textContent.append(storeStaffConfig.getProficientProjects());
             }
 
-            List<String> imageUrls = new ArrayList<>();
             List<String> videoUrls = new ArrayList<>();
-
-            if (StringUtils.isNotEmpty(storeStaffConfig.getStaffImage())) {
-                imageUrls.add(storeStaffConfig.getStaffImage());
+            if (StringUtils.isNotEmpty(storeStaffConfig.getStaffImage()) && isVideoUrl(storeStaffConfig.getStaffImage())) {
+                videoUrls.add(storeStaffConfig.getStaffImage());
             }
             if (StringUtils.isNotEmpty(storeStaffConfig.getBackgroundUrl())) {
                 String[] urls = storeStaffConfig.getBackgroundUrl().split(",");
                 for (String url : urls) {
                     if (StringUtils.isNotEmpty(url.trim())) {
                         String trimmedUrl = url.trim();
-                        // 判断是视频还是图片
                         if (isVideoUrl(trimmedUrl)) {
                             videoUrls.add(trimmedUrl);
-                        } else {
-                            imageUrls.add(trimmedUrl);
                         }
                     }
                 }
             }
 
-            // 1. 审核文本和图片
+            // 1. auditContent 仅文本;配图前端审(第二参固定 null)
             AiContentModerationUtil.AuditResult textImageAuditResult = aiContentModerationUtil.auditContent(
-                    textContent.toString().trim(), imageUrls
+                    textContent.toString().trim(), null
             );
 
-            // 2. 审核视频(如果有
+            // 2. 视频不在服务端审(前端已审
             AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
             if (!videoUrls.isEmpty()) {
-                log.info("开始审核视频,视频数量:{}", videoUrls.size());
+                log.info("服务端跳过视频审核(前端已审),视频数量:{}", videoUrls.size());
                 videoAuditResult = aiVideoModerationUtil.auditVideos(videoUrls);
             }
 
-            // 3. 综合审核结果:文本图片审核和视频审核都必须通过
+            // 3. 综合结果:仅文本不通过会拦(图/视频不再导致失败)
             boolean allPassed = (textImageAuditResult != null && textImageAuditResult.isPassed()) &&
                                 (videoAuditResult == null || videoAuditResult.isPassed());
 

+ 3 - 4
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffReviewServiceImpl.java

@@ -70,13 +70,12 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
         // 创建评价
         StoreStaffReview review = buildReviewFromDto(reviewDto);
         boolean success = this.save(review);
-        // AI审核
-        // 一次遍历完成分类,避免多次流式处理
+        // AI:评价图/视频前端直连审;服务端仅审 reviewContent 文本(图列表在 util 内忽略)
         Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(reviewDto.getReviewImages());
 
         AiContentModerationUtil.AuditResult auditResult = new AiContentModerationUtil.AuditResult(true, "");
         if( StringUtils.isNotEmpty(reviewDto.getReviewContent()) || urlCategoryMap.get("image").size() > 0){
-            auditResult = aiContentModerationUtil.auditContent(reviewDto.getReviewContent(), urlCategoryMap.get("image"));
+            auditResult = aiContentModerationUtil.auditContent(reviewDto.getReviewContent(), null);
         }
         if (!auditResult.isPassed()) {
             // 审核不通过
@@ -85,10 +84,10 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
             staffReview.setAuditReason(auditResult.getFailureReason());
             this.saveOrUpdate(staffReview);
         } else{
+            // 视频审核由前端完成;此处 auditVideos 恒通过,仅保留异步更新员工分等原流程
             CompletableFuture.runAsync(() -> {
                 AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
                 try {
-                    // 调用审核接口,增加超时控制(避免接口挂死)
                     videoAuditResult = CompletableFuture.supplyAsync(
                             () -> aiVideoModerationUtil.auditVideos(urlCategoryMap.get("video")),
                             commonVideoTaskExecutor

+ 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());
+        }
+    }
+}

+ 74 - 130
alien-store/src/main/java/shop/alien/store/util/ai/AiContentModerationUtil.java

@@ -1,7 +1,7 @@
 package shop.alien.store.util.ai;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
@@ -11,38 +11,42 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestTemplate;
 
-import java.util.List;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
- * 通用图文审核工具类
- * 调用AI图文审核接口审核文本和图片内容
+ * 服务端文本内容审核(moderate-url)。
+ * <ul>
+ *   <li>无有效文本 → 直接通过,不请求接口</li>
+ *   <li>有文本 → 仅提交文本字段;图/视频由前端处理,{@code auditContent} 第二参应传 {@code null}</li>
+ * </ul>
  */
 @Slf4j
 @Component
 @RefreshScope
-@RequiredArgsConstructor
 public class AiContentModerationUtil {
 
+    private static final String RESULTS = "results";
+    private static final String TEXT_INPUT_MARKER = "[TEXT INPUT]";
+    private static final String GENERIC_FAILURE = "审核异常";
+
     private final RestTemplate restTemplate;
 
-    /**
-     * AI审核接口地址
-     */
+    public AiContentModerationUtil(RestTemplate restTemplate) {
+        this.restTemplate = restTemplate;
+    }
+
     @Value("${ai.service.moderate-url}")
     private String moderateUrl;
 
-    /**
-     * 审核结果类
-     */
     public static class AuditResult {
-        private boolean passed;
-        private String failureReason;
+        private final boolean passed;
+        private final String failureReason;
 
         public AuditResult(boolean passed, String failureReason) {
             this.passed = passed;
@@ -59,149 +63,89 @@ public class AiContentModerationUtil {
     }
 
     /**
-     * 审核文本和图片内容
-     *
-     * @param text 文本内容
-     * @param imageUrls 图片URL列表
-     * @return 审核结果
+     * @param text      待审文案;null/空白则视为通过
+     * @param imageUrls 固定 {@code null}(兼容旧签名,非 null 会被忽略并打 debug)
      */
     public AuditResult auditContent(String text, List<String> imageUrls) {
-        log.info("开始审核内容:text={}, imageCount={}",
-                text, imageUrls != null ? imageUrls.size() : 0);
-
+        if (imageUrls != null && !imageUrls.isEmpty()) {
+            log.debug("auditContent: 忽略 imageUrls(size={}),请统一传 null", imageUrls.size());
+        }
+        if (!StringUtils.hasText(text)) {
+            log.debug("auditContent: 无有效文本,跳过 moderate");
+            return new AuditResult(true, null);
+        }
         try {
-            // 调用审核接口
-            return callModerateApi(text, imageUrls);
-
+            return requestModerate(text);
         } catch (Exception e) {
-            log.error("审核内容异常", e);
-            return new AuditResult(false, "审核异常");
+            log.error("auditContent 异常", e);
+            return new AuditResult(false, GENERIC_FAILURE);
         }
     }
 
-    /**
-     * 调用AI审核接口
-     *
-     * @param text 文本内容
-     * @param imageUrls 图片URL列表
-     * @return 审核结果
-     */
-    private AuditResult callModerateApi(String text, List<String> imageUrls) {
+    /** 仅在有非空 text 时调用 */
+    private AuditResult requestModerate(String text) {
         try {
-            // 构建 form-data 请求头
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
+            body.add("text", text);
 
-            // 构建 form-data 请求体
-            MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
-            if (StringUtils.hasText(text)) {
-                formData.add("text", text);
-            }
-            if (imageUrls != null) {
-                for (String url : imageUrls) {
-                    if (StringUtils.hasText(url)) {
-                        // 支持多张图片:同一个 key 重复多次
-                        formData.add("image_urls", url);
-                    }
-                }
-            }
-
-            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
-
-            log.info("调用AI审核接口:url={}, text={}, imageCount={}",
-                    moderateUrl, text, imageUrls != null ? imageUrls.size() : 0);
-
-            // 发送请求
-            ResponseEntity<String> response = restTemplate.postForEntity(moderateUrl, requestEntity, String.class);
+            ResponseEntity<String> response =
+                    restTemplate.postForEntity(moderateUrl, new HttpEntity<>(body, headers), String.class);
 
-            if (response.getStatusCode() == HttpStatus.OK) {
-                String responseBody = response.getBody();
-                log.info("AI审核接口响应:{}", responseBody);
-
-                if (StringUtils.hasText(responseBody)) {
-                    JSONObject jsonResponse = JSONObject.parseObject(responseBody);
-                    return parseAuditResult(jsonResponse);
-                } else {
-                    log.error("AI审核接口返回空响应");
-                    return new AuditResult(false, "审核异常");
-                }
-            } else {
-                log.error("AI审核接口调用失败,状态码:{}", response.getStatusCode());
-                return new AuditResult(false, "审核异常");
+            if (response.getStatusCode() != HttpStatus.OK) {
+                log.error("moderate HTTP 非 200: {}", response.getStatusCode());
+                return new AuditResult(false, GENERIC_FAILURE);
             }
-
+            String responseBody = response.getBody();
+            if (!StringUtils.hasText(responseBody)) {
+                log.error("moderate 响应体为空");
+                return new AuditResult(false, GENERIC_FAILURE);
+            }
+            log.debug("moderate 响应: {}", responseBody);
+            return parseModerateResponse(JSONObject.parseObject(responseBody));
         } catch (Exception e) {
-            log.error("调用AI审核接口异常", e);
-            return new AuditResult(false, "审核异常");
+            log.error("调用 moderate 失败", e);
+            return new AuditResult(false, GENERIC_FAILURE);
         }
     }
 
-    /**
-     * 解析审核结果
-     *
-     * @param jsonResponse API响应JSON
-     * @return 审核结果
-     */
-    private AuditResult parseAuditResult(JSONObject jsonResponse) {
+    private AuditResult parseModerateResponse(JSONObject json) {
         try {
-            // API返回格式:
-            // {
-            //     "results": [
-            //         {
-            //             "filename": "[TEXT INPUT]" 或 URL,
-            //             "flagged": true/false,
-            //             "risk_level": "high"/"safe"等,
-            //             "violation_categories": [...],
-            //             "reason": "原因描述"
-            //         }
-            //     ],
-            //     "summary": "处理摘要"
-            // }
-
-            com.alibaba.fastjson2.JSONArray results = jsonResponse.getJSONArray("results");
+            JSONArray results = json.getJSONArray(RESULTS);
             if (results == null || results.isEmpty()) {
-                log.warn("AI审核接口返回结果为空");
-                return new AuditResult(false, "审核异常");
+                log.warn("moderate 返回 results 为空");
+                return new AuditResult(false, GENERIC_FAILURE);
             }
 
-            // 检查是否有任何项目被标记为违规
-            List<String> violationReasons = new ArrayList<>();
-            boolean hasViolations = false;
-
+            List<String> reasons = new ArrayList<>();
             for (int i = 0; i < results.size(); i++) {
-                JSONObject result = results.getJSONObject(i);
-                if (result != null) {
-                    Boolean flagged = result.getBoolean("flagged");
-                    String filename = result.getString("filename");
-
-                    if (flagged != null && flagged) {
-                        hasViolations = true;
-                        // 根据filename判断是文本还是图片违规
-                        if ("[TEXT INPUT]".equals(filename)) {
-                            violationReasons.add("含违规词汇");
-                        } else if (StringUtils.hasText(filename) && filename.startsWith("http")) {
-                            violationReasons.add("图片内容违规");
-                        } else {
-                            violationReasons.add("内容违规");
-                        }
-                    }
+                JSONObject item = results.getJSONObject(i);
+                if (item == null || !Boolean.TRUE.equals(item.getBoolean("flagged"))) {
+                    continue;
                 }
+                reasons.add(flaggedItemReason(item.getString("filename")));
             }
 
-            if (hasViolations) {
-                // 有违规内容,审核失败
-                String failureReason = String.join("; ", violationReasons);
-                log.warn("AI审核失败:{}", failureReason);
-                return new AuditResult(false, failureReason);
-            } else {
-                // 所有内容都安全,审核通过
-                log.info("AI审核通过:所有内容安全");
+            if (reasons.isEmpty()) {
                 return new AuditResult(true, null);
             }
-
+            String joined = String.join("; ", reasons);
+            log.warn("moderate 命中违规: {}", joined);
+            return new AuditResult(false, joined);
         } catch (Exception e) {
-            log.error("解析审核结果异常", e);
-            return new AuditResult(false, "审核异常");
+            log.error("解析 moderate 响应失败", e);
+            return new AuditResult(false, GENERIC_FAILURE);
+        }
+    }
+
+    private static String flaggedItemReason(String filename) {
+        if (TEXT_INPUT_MARKER.equals(filename)) {
+            return "含违规词汇";
+        }
+        if (StringUtils.hasText(filename) && filename.startsWith("http")) {
+            return "图片内容违规";
         }
+        return "内容违规";
     }
-}
+}

+ 10 - 302
alien-store/src/main/java/shop/alien/store/util/ai/AiVideoModerationUtil.java

@@ -1,72 +1,27 @@
 package shop.alien.store.util.ai;
 
 import com.alibaba.fastjson2.JSONObject;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.client.RestTemplate;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
- * 视频审核工具类
- * 调用AI视频审核接口审核视频内容
+ * 视频审核占位:视频由前端直连 AI,服务端不调用视频审核接口。
+ * <p>保留 {@link #auditVideos} 供业务异步流程调用,恒为通过,避免历史代码空指针。</p>
  */
 @Slf4j
 @Component
-@RefreshScope
-@RequiredArgsConstructor
 public class AiVideoModerationUtil {
 
-    private final RestTemplate restTemplate;
-
-    /**
-     * 视频上传接口地址
-     */
-    @Value("${ai.service.video-submit-url}")
-    private String videoSubmitUrl;
-
-    /**
-     * 视频审查接口地址
-     */
-    @Value("${ai.service.video-status-url}")
-    private String videoStatusUrl;
-
-    /**
-     * 轮询间隔(毫秒)
-     */
-    @Value("${ai.service.video-poll-interval:2000}")
-    private long pollInterval;
-
-    /**
-     * 最大轮询次数
-     */
-    @Value("${ai.service.video-max-poll-count:30}")
-    private int maxPollCount;
-
-    /**
-     * 审核结果类
-     */
     public static class VideoAuditResult {
-        private boolean passed;
-        private String failureReason;
-        private String taskId;
-        private JSONObject rawResult;
+        private final boolean passed;
+        private final String failureReason;
+        private final String taskId;
+        private final JSONObject rawResult;
 
         public VideoAuditResult(boolean passed, String failureReason) {
-            this.passed = passed;
-            this.failureReason = failureReason;
+            this(passed, failureReason, null, null);
         }
 
         public VideoAuditResult(boolean passed, String failureReason, String taskId, JSONObject rawResult) {
@@ -94,260 +49,13 @@ public class AiVideoModerationUtil {
     }
 
     /**
-     * 审核视频内容
-     *
-     * @param videoUrls 视频URL列表
-     * @return 审核结果
+     * 不进行服务端视频审核;前端已审则此处仅打日志并返回通过。
      */
     public VideoAuditResult auditVideos(List<String> videoUrls) {
         if (videoUrls == null || videoUrls.isEmpty()) {
-            log.info("视频URL列表为空,跳过审核");
             return new VideoAuditResult(true, null);
         }
-
-        log.info("开始审核视频:videoCount={}", videoUrls.size());
-
-        try {
-            List<VideoAuditResult> results = new ArrayList<>();
-            for (String videoUrl : videoUrls) {
-                if (StringUtils.hasText(videoUrl)) {
-                    VideoAuditResult result = auditSingleVideo(videoUrl);
-                    results.add(result);
-                }
-            }
-
-            // 如果任何一个视频审核失败,整体审核失败
-            for (VideoAuditResult result : results) {
-                if (!result.isPassed()) {
-                    log.warn("视频审核失败:{}", result.getFailureReason());
-                    return result;
-                }
-            }
-
-            log.info("所有视频审核通过");
-            return new VideoAuditResult(true, null);
-
-        } catch (Exception e) {
-            log.error("审核视频异常", e);
-            return new VideoAuditResult(false, "审核服务异常:" + e.getMessage());
-        }
-    }
-
-    /**
-     * 审核单个视频
-     *
-     * @param videoUrl 视频URL
-     * @return 审核结果
-     */
-    private VideoAuditResult auditSingleVideo(String videoUrl) {
-        try {
-            // 1. 提交视频审核任务
-            String taskId = submitVideo(videoUrl);
-            if (taskId == null) {
-                return new VideoAuditResult(false, "视频提交失败");
-            }
-
-            log.info("视频提交成功,taskId={}, videoUrl={}", taskId, videoUrl);
-
-            // 2. 轮询审核状态
-            return pollVideoStatus(taskId, videoUrl);
-
-        } catch (Exception e) {
-            log.error("审核视频异常,videoUrl={}", videoUrl, e);
-            return new VideoAuditResult(false, "审核服务异常:" + e.getMessage());
-        }
-    }
-
-    /**
-     * 提交视频审核任务
-     *
-     * @param videoUrl 视频URL
-     * @return 任务ID
-     */
-    private String submitVideo(String videoUrl) {
-        try {
-            // 构建 form-data 请求头
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
-
-            // 构建 form-data 请求体,兼容服务端可能的三种参数名
-            MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
-            formData.add("videoUrl", videoUrl);
-            formData.add("video_path", videoUrl);
-            formData.add("video_url", videoUrl);
-
-            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
-
-            log.info("提交视频审核任务:url={}, videoUrl={}", videoSubmitUrl, videoUrl);
-
-            // 发送请求
-            ResponseEntity<String> response = restTemplate.postForEntity(videoSubmitUrl, requestEntity, String.class);
-
-            if (response.getStatusCode() == HttpStatus.OK) {
-                String responseBody = response.getBody();
-                log.info("视频提交接口响应:{}", responseBody);
-
-                if (StringUtils.hasText(responseBody)) {
-                    JSONObject jsonResponse = JSONObject.parseObject(responseBody);
-                    String taskId = jsonResponse.getString("task_id");
-                    if (StringUtils.hasText(taskId)) {
-                        return taskId;
-                    } else {
-                        log.error("视频提交接口返回的task_id为空");
-                        return null;
-                    }
-                } else {
-                    log.error("视频提交接口返回空响应");
-                    return null;
-                }
-            } else {
-                log.error("视频提交接口调用失败,状态码:{}", response.getStatusCode());
-                return null;
-            }
-
-        } catch (org.springframework.web.client.HttpClientErrorException e) {
-            log.error("调用视频提交接口异常,status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString(), e);
-            return null;
-        } catch (Exception e) {
-            log.error("调用视频提交接口异常", e);
-            return null;
-        }
-    }
-
-    /**
-     * 轮询视频审核状态
-     *
-     * @param taskId 任务ID
-     * @param videoUrl 视频URL(用于日志)
-     * @return 审核结果
-     */
-    private VideoAuditResult pollVideoStatus(String taskId, String videoUrl) {
-        int pollCount = 0;
-        while (pollCount < maxPollCount) {
-            try {
-                // 等待一段时间再查询
-                if (pollCount > 0) {
-                    Thread.sleep(pollInterval);
-                }
-
-                String statusUrl = videoStatusUrl + "/" + taskId;
-                log.info("查询视频审核状态:url={}, taskId={}, pollCount={}", statusUrl, taskId, pollCount + 1);
-
-                ResponseEntity<String> response = restTemplate.getForEntity(statusUrl, String.class);
-
-                if (response.getStatusCode() == HttpStatus.OK) {
-                    String responseBody = response.getBody();
-                    log.info("视频审核状态接口响应:{}", responseBody);
-
-                    if (StringUtils.hasText(responseBody)) {
-                        JSONObject jsonResponse = JSONObject.parseObject(responseBody);
-                        String status = jsonResponse.getString("status");
-
-                        if ("completed".equals(status)) {
-                            // 审核完成,解析结果
-                            return parseVideoAuditResult(jsonResponse, taskId);
-                        } else if ("failed".equals(status) || "error".equals(status)) {
-                            // 审核失败
-                            String error = jsonResponse.getString("error");
-                            log.error("视频审核失败,taskId={}, error={}", taskId, error);
-                            return new VideoAuditResult(false, "视频审核失败:" + (error != null ? error : "未知错误"), taskId, jsonResponse);
-                        } else {
-                            // 审核中,继续轮询
-                            log.info("视频审核中,taskId={}, status={}, 继续等待...", taskId, status);
-                            pollCount++;
-                        }
-                    } else {
-                        log.error("视频审核状态接口返回空响应");
-                        return new VideoAuditResult(false, "审核状态接口返回空响应");
-                    }
-                } else {
-                    log.error("视频审核状态接口调用失败,状态码:{}", response.getStatusCode());
-                    return new VideoAuditResult(false, "审核状态接口调用失败,状态码:" + response.getStatusCode());
-                }
-
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                log.error("轮询视频审核状态被中断,taskId={}", taskId, e);
-                return new VideoAuditResult(false, "审核被中断");
-            } catch (Exception e) {
-                log.error("查询视频审核状态异常,taskId={}", taskId, e);
-                pollCount++;
-                // 继续重试
-            }
-        }
-
-        log.error("视频审核超时,taskId={}, maxPollCount={}", taskId, maxPollCount);
-        return new VideoAuditResult(false, "视频审核超时");
-    }
-
-    /**
-     * 解析视频审核结果
-     *
-     * @param jsonResponse API响应JSON
-     * @param taskId 任务ID
-     * @return 审核结果
-     */
-    private VideoAuditResult parseVideoAuditResult(JSONObject jsonResponse, String taskId) {
-        try {
-            JSONObject result = jsonResponse.getJSONObject("result");
-            if (result == null) {
-                log.warn("视频审核结果中result字段为空");
-                return new VideoAuditResult(false, "审核结果格式错误:result字段为空", taskId, jsonResponse);
-            }
-
-            Boolean flagged = result.getBoolean("flagged");
-            String riskLevel = result.getString("risk_level");
-            String summary = result.getString("summary");
-
-            if (flagged != null && flagged) {
-                // 有违规内容,审核失败
-                List<String> violationReasons = new ArrayList<>();
-                if (StringUtils.hasText(riskLevel)) {
-                    violationReasons.add("风险等级:" + riskLevel);
-                }
-                if (StringUtils.hasText(summary)) {
-                    violationReasons.add(summary);
-                }
-
-                // 解析违规证据
-                com.alibaba.fastjson2.JSONArray evidence = result.getJSONArray("evidence");
-                if (evidence != null && !evidence.isEmpty()) {
-                    List<String> evidenceReasons = new ArrayList<>();
-                    for (int i = 0; i < evidence.size(); i++) {
-                        JSONObject item = evidence.getJSONObject(i);
-                        if (item != null) {
-                            String reason = item.getString("reason");
-                            String timeOffset = item.getString("time_offset");
-                            if (StringUtils.hasText(reason)) {
-                                String evidenceReason = StringUtils.hasText(timeOffset)
-                                    ? String.format("时间点%s: %s", timeOffset, reason)
-                                    : reason;
-                                evidenceReasons.add(evidenceReason);
-                            }
-                        }
-                    }
-                    if (!evidenceReasons.isEmpty()) {
-                        violationReasons.add("违规证据:" + String.join("; ", evidenceReasons));
-                    }
-                }
-
-                String failureReason = String.join("; ", violationReasons);
-                if (failureReason.isEmpty()) {
-                    failureReason = "视频内容违规";
-                }
-
-                log.warn("视频审核失败:{}", failureReason);
-                return new VideoAuditResult(false, failureReason, taskId, jsonResponse);
-            } else {
-                // 无违规内容,审核通过
-                log.info("视频审核通过:taskId={}", taskId);
-                return new VideoAuditResult(true, null, taskId, jsonResponse);
-            }
-
-        } catch (Exception e) {
-            log.error("解析视频审核结果异常", e);
-            return new VideoAuditResult(false, "解析审核结果异常:" + e.getMessage(), taskId, jsonResponse);
-        }
+        log.info("跳过服务端视频审核(前端已处理),条数={}", videoUrls.size());
+        return new VideoAuditResult(true, null);
     }
 }
-