Sfoglia il codice sorgente

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin 1 mese fa
parent
commit
d912b5ada8
100 ha cambiato i file con 4038 aggiunte e 1439 eliminazioni
  1. 136 0
      fs-admin/src/main/java/com/fs/distribution/controller/DistributionController.java
  2. 30 0
      fs-admin/src/main/java/com/fs/distribution/controller/DistributionWithdrawController.java
  3. 5 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreCouponIssueScrmController.java
  4. 5 3
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreCouponScrmController.java
  5. 95 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductGroupBuyController.java
  6. 174 0
      fs-admin/src/main/java/com/fs/hisStore/task/GroupBuyExpireTask.java
  7. 0 113
      fs-admin/src/main/java/com/fs/kdniao/config/KdniaoConfig.java
  8. 0 46
      fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoEOrderController.java
  9. 78 0
      fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoUniversalEOrderController.java
  10. 0 25
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoAddService.java
  11. 0 47
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoCommodity.java
  12. 0 181
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderRequest.java
  13. 0 62
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderResponse.java
  14. 0 55
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoPerson.java
  15. 0 192
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoSimpleOrderRequest.java
  16. 0 18
      fs-admin/src/main/java/com/fs/kdniao/service/IKdniaoEOrderService.java
  17. 0 214
      fs-admin/src/main/java/com/fs/kdniao/service/impl/KdniaoEOrderServiceImpl.java
  18. 0 114
      fs-admin/src/main/java/com/fs/kdniao/util/KdniaoUtil.java
  19. 0 45
      fs-admin/src/main/java/com/fs/kdniaoNew/controller/KdniaoUniversalEOrderController.java
  20. 36 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java
  21. 2 2
      fs-company/src/main/resources/application.yml
  22. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java
  23. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  24. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java
  25. 12 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java
  26. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java
  27. 23 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogAddwxMapper.java
  28. 13 7
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCalleesMapper.java
  29. 1 0
      fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java
  30. 16 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java
  31. 25 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java
  32. 18 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java
  33. 11 10
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCalleesServiceImpl.java
  34. 4 3
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCategoryServiceImpl.java
  35. 2 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  36. 49 0
      fs-service/src/main/java/com/fs/distribution/constant/DistributionConstants.java
  37. 51 0
      fs-service/src/main/java/com/fs/distribution/domain/DistributionAccount.java
  38. 52 0
      fs-service/src/main/java/com/fs/distribution/domain/DistributionCommissionRecord.java
  39. 44 0
      fs-service/src/main/java/com/fs/distribution/domain/DistributionConfig.java
  40. 43 0
      fs-service/src/main/java/com/fs/distribution/domain/DistributionUserRelation.java
  41. 34 0
      fs-service/src/main/java/com/fs/distribution/domain/DistributionWithdrawRecord.java
  42. 25 0
      fs-service/src/main/java/com/fs/distribution/dto/BindRelationRequest.java
  43. 24 0
      fs-service/src/main/java/com/fs/distribution/dto/CreateCommissionRequest.java
  44. 25 0
      fs-service/src/main/java/com/fs/distribution/dto/QueryDistributionAccountRequest.java
  45. 18 0
      fs-service/src/main/java/com/fs/distribution/dto/QueryDistributionAccountResponse.java
  46. 16 0
      fs-service/src/main/java/com/fs/distribution/dto/WithdrawApplyRequest.java
  47. 19 0
      fs-service/src/main/java/com/fs/distribution/dto/WithdrawAuditRequest.java
  48. 61 0
      fs-service/src/main/java/com/fs/distribution/mapper/DistributionAccountMapper.java
  49. 23 0
      fs-service/src/main/java/com/fs/distribution/mapper/DistributionCommissionRecordMapper.java
  50. 10 0
      fs-service/src/main/java/com/fs/distribution/mapper/DistributionConfigMapper.java
  51. 23 0
      fs-service/src/main/java/com/fs/distribution/mapper/DistributionUserRelationMapper.java
  52. 24 0
      fs-service/src/main/java/com/fs/distribution/mapper/DistributionWithdrawRecordMapper.java
  53. 53 0
      fs-service/src/main/java/com/fs/distribution/service/DistributionService.java
  54. 12 0
      fs-service/src/main/java/com/fs/distribution/service/IDistributionAccountService.java
  55. 16 0
      fs-service/src/main/java/com/fs/distribution/service/IDistributionCommissionRecordService.java
  56. 4 0
      fs-service/src/main/java/com/fs/distribution/service/IDistributionConfigService.java
  57. 4 0
      fs-service/src/main/java/com/fs/distribution/service/IDistributionUserRelationService.java
  58. 9 0
      fs-service/src/main/java/com/fs/distribution/service/IDistributionWithdrawRecordService.java
  59. 148 0
      fs-service/src/main/java/com/fs/distribution/service/impl/DistributionAccountServiceImpl.java
  60. 231 0
      fs-service/src/main/java/com/fs/distribution/service/impl/DistributionCommissionRecordServiceImpl.java
  61. 11 0
      fs-service/src/main/java/com/fs/distribution/service/impl/DistributionConfigServiceImpl.java
  62. 579 0
      fs-service/src/main/java/com/fs/distribution/service/impl/DistributionServiceImpl.java
  63. 11 0
      fs-service/src/main/java/com/fs/distribution/service/impl/DistributionUserRelationServiceImpl.java
  64. 21 0
      fs-service/src/main/java/com/fs/distribution/service/impl/DistributionWithdrawRecordServiceImpl.java
  65. 3 1
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  66. 174 87
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  67. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCouponIssueScrm.java
  68. 19 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCouponScrm.java
  69. 7 108
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCouponUserScrm.java
  70. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  71. 16 1
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductActivity.java
  72. 82 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductGroupBuy.java
  73. 74 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductGroupBuyItem.java
  74. 36 27
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueScrmMapper.java
  75. 5 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueUserScrmMapper.java
  76. 20 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  77. 10 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductActivityMapper.java
  78. 31 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductGroupBuyItemMapper.java
  79. 109 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductGroupBuyMapper.java
  80. 10 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreCouponPublishParam.java
  81. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/LoginMpWxParam.java
  82. 6 1
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCouponIssueScrmService.java
  83. 10 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductActivityService.java
  84. 86 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductGroupBuyService.java
  85. 35 3
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  86. 94 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCouponIssueScrmServiceImpl.java
  87. 177 52
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  88. 11 11
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  89. 31 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductActivityServiceImpl.java
  90. 458 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductGroupBuyServiceImpl.java
  91. 7 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCouponIssueVO.java
  92. 75 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreGroupBuyListVO.java
  93. 75 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreGroupBuyMemberVO.java
  94. 13 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListVO.java
  95. 11 0
      fs-service/src/main/java/com/fs/hisStore/vo/ReceiveCouponDto.java
  96. 2 2
      fs-service/src/main/java/com/fs/kdniao/config/KdniaoUniversalConfig.java
  97. 1 1
      fs-service/src/main/java/com/fs/kdniao/domain/KdniaoAddServiceNew.java
  98. 1 1
      fs-service/src/main/java/com/fs/kdniao/domain/KdniaoCarrierConfig.java
  99. 1 1
      fs-service/src/main/java/com/fs/kdniao/domain/KdniaoCommodityNew.java
  100. 1 1
      fs-service/src/main/java/com/fs/kdniao/domain/KdniaoPersonNew.java

+ 136 - 0
fs-admin/src/main/java/com/fs/distribution/controller/DistributionController.java

@@ -0,0 +1,136 @@
+package com.fs.distribution.controller;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.distribution.domain.DistributionConfig;
+import com.fs.distribution.dto.*;
+import com.fs.distribution.service.DistributionService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+/**
+ * 分销控制器
+ */
+@RestController
+@RequestMapping("/distribution")
+public class DistributionController {
+
+    @Resource
+    private DistributionService distributionService;
+
+    /**
+     * 绑定分销关系
+     *
+     * 一般不建议前端单独调用这个接口,
+     * 最好是在注册成功后,后端注册逻辑内部调用 distributionService.bindRelation()
+     */
+    @PostMapping("/bind")
+    public AjaxResult bind(@RequestBody BindRelationRequest request) {
+        distributionService.bindRelation(request);
+        return AjaxResult.success();
+    }
+
+
+    /**
+     * 查询分销账户列表
+     */
+    @PostMapping("/account/list")
+    public AjaxResult accountList(@RequestBody QueryDistributionAccountRequest queryDistributionAccountRequest) {
+        return AjaxResult.success(distributionService.getAccountList(queryDistributionAccountRequest));
+
+    }
+
+    /**
+     * 我的分销账户
+     */
+    @GetMapping("/account/{userId}")
+    public AjaxResult account(@PathVariable Long userId) {
+        return AjaxResult.success(distributionService.getAccount(userId));
+    }
+
+    /**
+     * 我的分销关系
+     */
+    @GetMapping("/relation/{userId}")
+    public AjaxResult relation(@PathVariable Long userId) {
+        return AjaxResult.success(distributionService.getRelation(userId));
+    }
+
+    /**
+     * 我的佣金明细
+     */
+    @GetMapping("/commission/list/{userId}")
+    public AjaxResult commissionList(@PathVariable Long userId) {
+        return AjaxResult.success(distributionService.getCommissionList(userId));
+    }
+
+    /**
+     * 申请提现
+     */
+    @PostMapping("/withdraw/apply")
+    public AjaxResult applyWithdraw(@RequestBody WithdrawApplyRequest request) {
+        distributionService.applyWithdraw(request);
+        return AjaxResult.success("提现申请成功");
+    }
+
+    /**
+     * 我的提现记录
+     */
+    @GetMapping("/withdraw/list/{userId}")
+    public AjaxResult withdrawList(@PathVariable Long userId) {
+        return AjaxResult.success(distributionService.getWithdrawList(userId));
+    }
+
+
+
+    /**
+     * 后台审核提现
+     */
+    @PostMapping("/withdraw/audit")
+    public AjaxResult auditWithdraw(@RequestBody WithdrawAuditRequest request) {
+        distributionService.auditWithdraw(request);
+        return AjaxResult.success("审核成功");
+    }
+
+    /**
+     * 订单支付成功后生成佣金
+     *
+     * 这个接口一般不暴露给前端,
+     * 应该在订单支付回调成功后由订单服务内部调用。
+     */
+    @PostMapping("/commission/createAfterPay")
+    public AjaxResult createCommissionAfterPay(@RequestBody CreateCommissionRequest request) {
+        distributionService.createCommissionAfterPay(request);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 退款后佣金失效
+     *
+     * 这个接口一般不暴露给前端,
+     * 应该在退款成功后由订单服务内部调用。
+     */
+    @PostMapping("/commission/invalid/{orderId}")
+    public AjaxResult invalidCommission(@PathVariable Long orderId) {
+        distributionService.invalidCommissionByRefund(orderId);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 查询分销配置
+     */
+    @GetMapping("/config")
+    public AjaxResult getConfig() {
+        return AjaxResult.success(distributionService.getConfig());
+    }
+
+    /**
+     * 保存分销配置
+     */
+    @PostMapping("/config")
+    public AjaxResult saveConfig(@RequestBody DistributionConfig config) {
+        distributionService.saveConfig(config);
+        return AjaxResult.success("保存成功");
+    }
+
+}

+ 30 - 0
fs-admin/src/main/java/com/fs/distribution/controller/DistributionWithdrawController.java

@@ -0,0 +1,30 @@
+package com.fs.distribution.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.distribution.domain.DistributionWithdrawRecord;
+import com.fs.distribution.service.IDistributionWithdrawRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/withdraw")
+public class DistributionWithdrawController extends BaseController {
+
+    @Autowired
+    private IDistributionWithdrawRecordService distributionWithdrawService;
+
+    /**
+     * 查询待审核提现记录
+     */
+    @GetMapping("/audit/list")
+    public TableDataInfo auditList(DistributionWithdrawRecord query) {
+        startPage();
+        List<DistributionWithdrawRecord> list = distributionWithdrawService.selectWithdrawList(query);
+        return getDataTable(list);
+    }
+}

+ 5 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreCouponIssueScrmController.java

@@ -95,4 +95,9 @@ public class FsStoreCouponIssueScrmController extends BaseController
     {
         return toAjax(fsStoreCouponIssueService.deleteFsStoreCouponIssueByIds(ids));
     }
+
+    /**
+     *  用户领取优惠券
+     */
+
 }

+ 5 - 3
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreCouponScrmController.java

@@ -134,9 +134,11 @@ public class FsStoreCouponScrmController extends BaseController
             issue.setCouponType(coupon.getType());
             issue.setStartTime(publishParam.getStartTime());
             issue.setLimitTime(publishParam.getLimitTime());
-            issue.setTotalCount(publishParam.getTotalCount());
-            issue.setRemainCount(0);
-            issue.setIsPermanent(0);
+            issue.setIsPermanent(publishParam.getIsPermanent());
+            if (publishParam.getIsPermanent()==0){
+                issue.setTotalCount(publishParam.getTotalCount());
+                issue.setRemainCount(publishParam.getTotalCount());
+            }
             issue.setStatus(1);
             issue.setCreateTime(new Date());
             fsStoreCouponIssueService.insertFsStoreCouponIssue(issue);

+ 95 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductGroupBuyController.java

@@ -0,0 +1,95 @@
+package com.fs.hisStore.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.hisStore.service.IFsStoreProductGroupBuyService;
+import com.fs.hisStore.vo.FsStoreGroupBuyListVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 后台管理 - 限时团购管理
+ * <p>只读查询,不做 CRUD。团购活动的新增/修改沿用 fs_store_product_activity(activity_type=8)。</p>
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Api("后台-限时团购管理")
+@RestController
+@RequestMapping("/store/store/productGroupBuy")
+public class FsStoreProductGroupBuyController extends BaseController {
+
+    @Autowired
+    private IFsStoreProductGroupBuyService groupBuyService;
+
+    /**
+     * 团购列表(分页)
+     * 前端传:groupNo/productId/productName/status/beginTime/endTime 皆可选
+     */
+    @ApiOperation("团购列表")
+    @PreAuthorize("@ss.hasPermi('store:productGroupBuy:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(value = "groupNo",    required = false) String groupNo,
+                              @RequestParam(value = "productId",  required = false) Long productId,
+                              @RequestParam(value = "productName",required = false) String productName,
+                              @RequestParam(value = "status",     required = false) Integer status,
+                              @RequestParam(value = "beginTime",  required = false) String beginTime,
+                              @RequestParam(value = "endTime",    required = false) String endTime) {
+        startPage();
+        List<FsStoreGroupBuyListVO> list = groupBuyService.selectGroupBuyListForAdmin(
+                groupNo, productId, productName, status, beginTime, endTime);
+        return getDataTable(list);
+    }
+
+    /**
+     * 团购详情(含团员列表及订单状态)
+     */
+    @ApiOperation("团购详情(含团员+订单状态)")
+    @PreAuthorize("@ss.hasPermi('store:productGroupBuy:query')")
+    @GetMapping(value = "/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        FsStoreGroupBuyListVO detail = groupBuyService.selectGroupBuyDetailForAdmin(id);
+        if (detail == null) {
+            return R.error("团购不存在或已删除");
+        }
+        return R.ok().put("data", detail);
+    }
+
+    /**
+     * 按商品ID查历史拼团(商品详情页的"历史拼团"入口)
+     */
+    @ApiOperation("按商品ID查历史拼团")
+    @PreAuthorize("@ss.hasPermi('store:productGroupBuy:list')")
+    @GetMapping("/listByProduct/{productId}")
+    public TableDataInfo listByProduct(@PathVariable("productId") Long productId) {
+        startPage();
+        List<FsStoreGroupBuyListVO> list = groupBuyService.selectGroupBuyListByProduct(productId);
+        return getDataTable(list);
+    }
+
+    /**
+     * 团员列表(单独接口,用于详情页弹出/下钻查看)
+     * 也可复用 /{id} 接口,这里保留独立入口方便前端按需调用
+     */
+    @ApiOperation("团员列表")
+    @PreAuthorize("@ss.hasPermi('store:productGroupBuy:query')")
+    @GetMapping("/members/{id}")
+    public AjaxResult members(@PathVariable("id") Long id) {
+        FsStoreGroupBuyListVO detail = groupBuyService.selectGroupBuyDetailForAdmin(id);
+        if (detail == null) {
+            return AjaxResult.error("团购不存在");
+        }
+        return AjaxResult.success(detail.getMembers());
+    }
+}

+ 174 - 0
fs-admin/src/main/java/com/fs/hisStore/task/GroupBuyExpireTask.java

@@ -0,0 +1,174 @@
+package com.fs.hisStore.task;
+
+import com.fs.common.core.domain.R;
+import com.fs.hisStore.domain.FsStoreProductGroupBuy;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductGroupBuyMapper;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 团购超时兜底定时任务
+ * <p>
+ * 职责两件事:
+ * <ol>
+ *   <li><b>主路径</b>:扫“截团时间已过但还没成团”的团,把团判失败,并把团内已付款的订单自动退款</li>
+ *   <li><b>孤儿订单兜底</b>:扫“团已判失败但团内还有未退款的已付款订单”,补上退款;
+ *       对应“支付回调卡在 endTime 后到达”以及“上一轮退款失败”的极端场景</li>
+ * </ol>
+ * <p>
+ * 几个关键点:
+ * <ul>
+ *   <li>未成团的订单根本没推过 ERP,退款只走本地 + 调支付通道打钱,<b>不调 ERP 取消</b>;
+ *       service 层的 refundOrderMoney 里已经加了团购未成团放行,走到那儿自然跳过 ERP 分支</li>
+ *   <li>标团失败用 CAS(status=0 AND end_time<=now()),多实例并发跑也只会有一个任务赢得处置权,
+ *       其他实例拿到 0 影响行数直接跳过,不会重复退款</li>
+ *   <li>查团内订单复用已有 {@code selectOrderIdsByGroupBuyId},
+ *       条件是 status=1 AND extend_order_id is null——刚好就是“已付款没推 ERP”的未成团订单,
+ *       退成功后订单 status 变 -2,下一轮扫描自然漏掉,幂等</li>
+ *   <li>孤儿订单扫描用 {@code selectOrphanOrderIdsInFailedGroups},条件是 group.status=2 +
+ *       order.status=1 + refund_status=0 + extend_order_id is null,退完 status 也变 -2自然漏掉</li>
+ *   <li>未支付的团购订单由原订单超时取消任务负责,那边已经接入 releaseGroupSlot,
+ *       不用这里重复处理</li>
+ * </ul>
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Slf4j
+@Component("groupBuyExpireTask")
+public class GroupBuyExpireTask {
+
+    /** 单轮最多处理多少个过期团,防止偶尔积压时把一轮跑爆 */
+    private static final int BATCH_LIMIT = 50;
+
+    @Autowired
+    private FsStoreProductGroupBuyMapper groupBuyMapper;
+
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderMapper;
+
+    @Autowired
+    private IFsStoreOrderScrmService orderService;
+
+    /**
+     * 每分钟扫一次。触发频率跟 endTime 的精度匹配即可,不用更密。
+     * 定时注解先留着但注释掉,生产上线前人工启用,免得测试环境误触发。
+     */
+//    @Scheduled(fixedRate = 60000)
+    public void handleExpiredGroups() {
+        // 第一段:处理新过期的团——标失败 + 退款团内订单
+        processExpiredGroups();
+        // 第二段:扫一遍孤儿订单——支付回调卡在 endTime 后到达、或上轮退款失败残留下的订单兜底退款
+        processOrphanOrders();
+    }
+
+    private void processExpiredGroups() {
+        List<FsStoreProductGroupBuy> expiredList;
+        try {
+            expiredList = groupBuyMapper.selectExpiredUnformedGroups(BATCH_LIMIT);
+        } catch (Exception e) {
+            log.error("[GroupBuyExpireTask] 查询过期团失败", e);
+            return;
+        }
+        if (expiredList == null || expiredList.isEmpty()) {
+            return;
+        }
+
+        log.info("[GroupBuyExpireTask] 发现 {} 个过期未成团的团,开始处理", expiredList.size());
+
+        int handled = 0;
+        for (FsStoreProductGroupBuy group : expiredList) {
+            try {
+                if (handleOneGroup(group)) {
+                    handled++;
+                }
+            } catch (Exception e) {
+                // 单团出错不影响其他团,下一轮还会扫到,异常只打日志
+                log.error("[GroupBuyExpireTask] 团 {} 处理异常,下一轮重试", group.getId(), e);
+            }
+        }
+        log.info("[GroupBuyExpireTask] 本轮成功处理 {} 个团", handled);
+    }
+
+    /**
+     * 孤儿订单扫描:扫走“团已失败但订单还没退款”的残渣。
+     * <p>两种典型场景会走到这里:
+     * <ul>
+     *   <li>用户在 endTime-1s 下单,支付回调延迟,到达时团已被上一轮任务判失败;
+     *       finishPaidOrderInGroup 检测到 status=2 直接 return,订单留在这儿等退款</li>
+     *   <li>上一轮 processExpiredGroups 调 refundOrderMoney 时支付通道抛异常,订单还未退款成功</li>
+     * </ul>
+     */
+    private void processOrphanOrders() {
+        List<Long> orphanOrderIds;
+        try {
+            orphanOrderIds = groupBuyMapper.selectOrphanOrderIdsInFailedGroups(BATCH_LIMIT);
+        } catch (Exception e) {
+            log.error("[GroupBuyExpireTask] 查询孤儿订单失败", e);
+            return;
+        }
+        if (orphanOrderIds == null || orphanOrderIds.isEmpty()) {
+            return;
+        }
+
+        log.info("[GroupBuyExpireTask] 发现 {} 个孤儿订单(团已失败但订单未退款),开始补退", orphanOrderIds.size());
+        for (Long orderId : orphanOrderIds) {
+            try {
+                R r = orderService.refundOrderMoney(orderId);
+                if (r != null && "0".equals(String.valueOf(r.get("code")))) {
+                    log.info("[GroupBuyExpireTask] 孤儿订单 {} 补退成功", orderId);
+                } else {
+                    log.warn("[GroupBuyExpireTask] 孤儿订单 {} 补退失败:{}",
+                            orderId, r != null ? r.get("msg") : "null");
+                }
+            } catch (Exception e) {
+                log.error("[GroupBuyExpireTask] 孤儿订单 {} 补退异常,待下一轮重试", orderId, e);
+            }
+        }
+    }
+
+    /**
+     * 处理单个过期团:原子标失败 + 循环退款团内已付款订单
+     *
+     * @return true=本任务确实处理了这个团 false=被别的实例抢走/或根本没订单要退
+     */
+    private boolean handleOneGroup(FsStoreProductGroupBuy group) {
+        // CAS 标失败,抢不到就让给别人,不往下走
+        int affected = groupBuyMapper.markGroupFailed(group.getId());
+        if (affected == 0) {
+            log.info("[GroupBuyExpireTask] 团 {} 已被别的任务处理,跳过", group.getId());
+            return false;
+        }
+        log.info("[GroupBuyExpireTask] 团 {} 已标记为拼团失败(end_time={})", group.getId(), group.getEndTime());
+
+        // 拿团内所有"已付款 + 没推过 ERP"的订单,这批就是要自动退款的
+        List<Long> orderIds = fsStoreOrderMapper.selectOrderIdsByGroupBuyId(group.getId());
+        if (orderIds == null || orderIds.isEmpty()) {
+            log.info("[GroupBuyExpireTask] 团 {} 无需退款的订单,收工", group.getId());
+            return true;
+        }
+
+        for (Long orderId : orderIds) {
+            try {
+                R r = orderService.refundOrderMoney(orderId);
+                if (r != null && "0".equals(String.valueOf(r.get("code")))) {
+                    log.info("[GroupBuyExpireTask] 团 {} 订单 {} 自动退款成功", group.getId(), orderId);
+                } else {
+                    // 退款失败不中断,下一轮还会扫到这个订单重试
+                    log.warn("[GroupBuyExpireTask] 团 {} 订单 {} 自动退款失败:{}",
+                            group.getId(), orderId, r != null ? r.get("msg") : "null");
+                }
+            } catch (Exception e) {
+                log.error("[GroupBuyExpireTask] 团 {} 订单 {} 退款异常,待下一轮重试",
+                        group.getId(), orderId, e);
+            }
+        }
+        return true;
+    }
+}

+ 0 - 113
fs-admin/src/main/java/com/fs/kdniao/config/KdniaoConfig.java

@@ -1,113 +0,0 @@
-package com.fs.kdniao.config;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-/**
- * 快递鸟配置类
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "kdniao")
-public class KdniaoConfig {
-
-    /**
-     * 快递鸟用户ID
-     */
-    private String eBusinessID;
-
-    /**
-     * 快递鸟API Key
-     */
-    private String apiKey;
-
-    /**
-     * 电子面单接口地址
-     */
-    private String reqURL;
-
-    /**
-     * 默认电子面单账号配置
-     */
-    private Account account;
-
-    /**
-     * 默认发件人配置
-     */
-    private Sender sender;
-
-    /**
-     * 电子面单账号配置
-     */
-    @Data
-    public static class Account {
-
-        /**
-         * 电子面单账号
-         */
-        private String customerName;
-
-        /**
-         * 电子面单密码
-         */
-        private String customerPwd;
-
-        /**
-         * 发件网点编码
-         */
-        private String sendSite;
-
-        /**
-         * 月结号
-         */
-        private String monthCode;
-    }
-
-    /**
-     * 默认发件人配置
-     */
-    @Data
-    public static class Sender {
-
-        /**
-         * 发件公司
-         */
-        private String company;
-
-        /**
-         * 发件人姓名
-         */
-        private String name;
-
-        /**
-         * 发件人手机号
-         */
-        private String mobile;
-
-        /**
-         * 发件省
-         */
-        private String provinceName;
-
-        /**
-         * 发件市
-         */
-        private String cityName;
-
-        /**
-         * 发件区/县
-         */
-        private String expAreaName;
-
-        /**
-         * 发件详细地址
-         */
-        private String address;
-
-        /**
-         * 发件邮编
-         */
-        private String postCode;
-    }
-}

+ 0 - 46
fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoEOrderController.java

@@ -1,46 +0,0 @@
-package com.fs.kdniao.controller;
-
-import com.fs.common.annotation.Log;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.enums.BusinessType;
-import com.fs.kdniao.domain.KdniaoEOrderResponse;
-import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
-import com.fs.kdniao.service.IKdniaoEOrderService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-
-/**
- * 快递鸟电子面单控制器
- */
-@RestController
-@RequestMapping("/kdniao/eorder")
-public class KdniaoEOrderController extends BaseController {
-
-    @Autowired
-    private IKdniaoEOrderService kdniaoEOrderService;
-
-    /**
-     * 简化参数下单接口
-     * 前端只需要传常用业务参数,后端自动组装 RequestData
-     */
-    @Log(title = "快递鸟电子面单", businessType = BusinessType.INSERT)
-    @PostMapping("/submit")
-    public AjaxResult submit(@RequestBody KdniaoSimpleOrderRequest request) {
-        try {
-            KdniaoEOrderResponse response = kdniaoEOrderService.submitSimpleOrder(request);
-
-            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
-                return AjaxResult.success("下单成功", response);
-            }
-
-            if ("106".equals(response.getResultCode())) {
-                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功");
-            }
-
-            return AjaxResult.error("下单失败:" + response.getReason(), response);
-        } catch (Exception e) {
-            return AjaxResult.error("电子面单下单异常:" + e.getMessage());
-        }
-    }
-}

+ 78 - 0
fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoUniversalEOrderController.java

@@ -0,0 +1,78 @@
+package com.fs.kdniao.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.kdniao.domain.KdniaoSubmitCommand;
+import com.fs.kdniao.domain.KdniaoUniversalResponse;
+import com.fs.kdniao.domain.KdniaoWaybill;
+import com.fs.kdniao.service.IKdniaoUniversalEOrderService;
+import com.fs.kdniao.service.KdniaoWaybillService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 快递鸟统一电子面单控制器
+ */
+@RestController
+@RequestMapping("/kdniao/universal/eorder")
+public class KdniaoUniversalEOrderController extends BaseController {
+
+    @Autowired
+    private IKdniaoUniversalEOrderService kdniaoUniversalEOrderService;
+
+    @Autowired
+    private KdniaoWaybillService kdniaoWaybillService;
+
+
+    /**
+     * 统一下单
+     */
+    @Log(title = "快递鸟统一电子面单", businessType = BusinessType.INSERT)
+    @PostMapping("/submit")
+    public AjaxResult submit(@RequestBody KdniaoSubmitCommand command) {
+        try {
+            // 1. 先查本地是否已有面单,避免重复下单
+            KdniaoWaybill exist = kdniaoWaybillService.selectByOrderCode(command.getBizOrderNo());
+            if (exist != null) {
+                return AjaxResult.success("该订单已生成面单,返回已保存数据", exist);
+            }
+
+            KdniaoUniversalResponse response = kdniaoUniversalEOrderService.submit(command);
+
+            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
+                KdniaoWaybill waybill = kdniaoWaybillService.saveWaybill(command, response);
+                return AjaxResult.success("下单成功", waybill);
+            }
+
+            // 订单重复:优先查本地已保存面单
+            if ("106".equals(response.getResultCode())) {
+                KdniaoWaybill dbWaybill = kdniaoWaybillService.selectByOrderCode(command.getBizOrderNo());
+                if (dbWaybill != null) {
+                    return AjaxResult.success("该订单号已存在面单,返回本地保存数据", dbWaybill);
+                }
+                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功,但本地未保存到面单");
+            }
+
+            return AjaxResult.error("下单失败:" + response.getReason(), response);
+        } catch (Exception e) {
+            return AjaxResult.error("下单异常:" + e.getMessage());
+        }
+    }
+
+
+    /**
+     * 查看订单面单信息
+     */
+//    @PreAuthorize("@ss.hasPermi('store:storeOrder:query')")
+    @GetMapping("/waybill/{orderCode}")
+    public AjaxResult getWaybillInfo(@PathVariable Long orderCode) {
+        KdniaoWaybill waybill = kdniaoWaybillService.selectByOrderCode(String.valueOf(orderCode));
+        if (waybill == null) {
+            return AjaxResult.error("该订单暂无面单信息");
+        }
+
+        return AjaxResult.success(waybill);
+    }
+}

+ 0 - 25
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoAddService.java

@@ -1,25 +0,0 @@
-package com.fs.kdniao.domain;
-
-import lombok.Data;
-
-/**
- * 增值服务(保价、代收货款等)
- */
-@Data
-public class KdniaoAddService {
-
-    /**
-     * 服务名称(如 COD、INSURE)
-     */
-    private String Name;
-
-    /**
-     * 服务值(金额/参数)
-     */
-    private String Value;
-
-    /**
-     * 客户标识
-     */
-    private String CustomerID;
-}

+ 0 - 47
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoCommodity.java

@@ -1,47 +0,0 @@
-package com.fs.kdniao.domain;
-
-import lombok.Data;
-
-import java.math.BigDecimal;
-
-/**
- * 商品信息
- */
-@Data
-public class KdniaoCommodity {
-
-    /**
-     * 商品名称
-     */
-    private String GoodsName;
-
-    /**
-     * 商品编码
-     */
-    private String GoodsCode;
-
-    /**
-     * 商品数量
-     */
-    private Integer Goodsquantity;
-
-    /**
-     * 商品价格
-     */
-    private BigDecimal GoodsPrice;
-
-    /**
-     * 商品重量
-     */
-    private BigDecimal GoodsWeight;
-
-    /**
-     * 商品描述
-     */
-    private String GoodsDesc;
-
-    /**
-     * 商品体积
-     */
-    private BigDecimal GoodsVol;
-}

+ 0 - 181
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderRequest.java

@@ -1,181 +0,0 @@
-package com.fs.kdniao.domain;
-
-import lombok.Data;
-
-import java.math.BigDecimal;
-import java.util.List;
-
-/**
- * 快递鸟电子面单下单请求对象(RequestData)
- */
-@Data
-public class KdniaoEOrderRequest {
-
-    /**
-     * 订单编号(必须唯一)
-     * 不能重复,否则返回106(幂等控制字段)
-     */
-    private String OrderCode;
-
-    /**
-     * 快递公司编码(如 SF、YTO、ZTO、JDKY 等)
-     */
-    private String ShipperCode;
-
-    /**
-     * 电子面单账号(部分快递必填,如顺丰、圆通等)
-     */
-    private String CustomerName;
-
-    /**
-     * 电子面单密码
-     */
-    private String CustomerPwd;
-
-    /**
-     * 发件网点编码
-     */
-    private String SendSite;
-
-    /**
-     * 发件业务员
-     */
-    private String SendStaff;
-
-    /**
-     * 月结账号
-     */
-    private String MonthCode;
-
-    /**
-     * 运费支付方式
-     * 1:现付
-     * 2:到付
-     * 3:月结
-     * 4:第三方付(部分公司支持)
-     */
-    private Integer PayType;
-
-    /**
-     * 快递业务类型(不同快递公司不同)
-     */
-    private String ExpType;
-
-    /**
-     * 发件人信息(必填)
-     */
-    private KdniaoPerson Sender;
-
-    /**
-     * 收件人信息(必填)
-     */
-    private KdniaoPerson Receiver;
-
-    /**
-     * 包裹数量(>=1)
-     */
-    private Integer Quantity;
-
-    /**
-     * 总重量(kg)
-     * 京东/快运类必填
-     */
-    private BigDecimal Weight;
-
-    /**
-     * 总体积(m³)
-     * 京东/快运类必填
-     */
-    private BigDecimal Volume;
-
-    /**
-     * 运费(部分到付场景必填)
-     */
-    private BigDecimal Cost;
-
-    /**
-     * 其他费用
-     */
-    private BigDecimal OtherCost;
-
-    /**
-     * 增值服务(保价、代收货款等)
-     */
-    private List<KdniaoAddService> AddService;
-
-    /**
-     * 备注(会打印在面单上)
-     */
-    private String Remark;
-
-    /**
-     * 商品信息(至少一个 GoodsName)
-     */
-    private List<KdniaoCommodity> Commodity;
-
-    /**
-     * 是否返回电子面单模板
-     * 0:否
-     * 1:是
-     */
-    private String IsReturnPrintTemplate;
-
-    /**
-     * 面单模板尺寸(如 130)
-     */
-    private String TemplateSize;
-
-    /**
-     * 自定义打印内容
-     */
-    private String CustomArea;
-
-    /**
-     * 是否订阅轨迹推送
-     * 1:订阅(默认)
-     * 0:不订阅(避免消耗余额)
-     */
-    private String IsSubscribe;
-
-    /**
-     * 自定义回传字段
-     */
-    private String Callback;
-
-    /**
-     * 是否通知快递员上门揽件
-     * 0:通知
-     * 1:不通知
-     */
-    private Integer IsNotice;
-
-    /**
-     * 上门揽件开始时间(格式:yyyy-MM-dd HH:mm:ss)
-     */
-    private String StartDate;
-
-    /**
-     * 上门揽件结束时间
-     */
-    private String EndDate;
-
-    /**
-     * 是否要求签回单
-     * 0:否
-     * 1:是
-     */
-    private Integer IsReturnSignBill;
-
-    /**
-     * 是否发送短信通知
-     * 0:否
-     * 1:是
-     */
-    private Integer IsSendMessage;
-
-    /**
-     * 币种(顺丰港澳台必填)
-     * CNY / HKD / NTD
-     */
-    private String CurrencyCode;
-}

+ 0 - 62
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderResponse.java

@@ -1,62 +0,0 @@
-package com.fs.kdniao.domain;
-
-import lombok.Data;
-
-/**
- * 快递鸟电子面单返回对象
- */
-@Data
-public class KdniaoEOrderResponse {
-
-    /**
-     * 用户ID
-     */
-    private String EBusinessID;
-
-    /**
-     * 是否成功
-     */
-    private Boolean Success;
-
-    /**
-     * 返回编码
-     */
-    private String ResultCode;
-
-    /**
-     * 返回原因
-     */
-    private String Reason;
-
-    /**
-     * 订单信息
-     */
-    private Order Order;
-
-    /**
-     * 面单模板
-     */
-    private String PrintTemplate;
-
-    /**
-     * 运单信息
-     */
-    @Data
-    public static class Order {
-
-        /**
-         * 订单编号
-         */
-        private String OrderCode;
-
-        /**
-         * 快递公司编码
-         */
-        private String ShipperCode;
-
-        /**
-         * 运单号
-         */
-        private String LogisticCode;
-    }
-}

+ 0 - 55
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoPerson.java

@@ -1,55 +0,0 @@
-package com.fs.kdniao.domain;
-
-import lombok.Data;
-
-/**
- * 发件人 / 收件人信息
- */
-@Data
-public class KdniaoPerson {
-
-    /**
-     * 公司名称
-     */
-    private String Company;
-
-    /**
-     * 姓名
-     */
-    private String Name;
-
-    /**
-     * 电话
-     */
-    private String Tel;
-
-    /**
-     * 手机号
-     */
-    private String Mobile;
-
-    /**
-     * 省
-     */
-    private String ProvinceName;
-
-    /**
-     * 市
-     */
-    private String CityName;
-
-    /**
-     * 区/县
-     */
-    private String ExpAreaName;
-
-    /**
-     * 详细地址
-     */
-    private String Address;
-
-    /**
-     * 邮编
-     */
-    private String PostCode;
-}

+ 0 - 192
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoSimpleOrderRequest.java

@@ -1,192 +0,0 @@
-package com.fs.kdniao.domain;
-
-import lombok.Data;
-
-import java.math.BigDecimal;
-
-/**
- * 前端简化下单请求对象
- */
-@Data
-public class KdniaoSimpleOrderRequest {
-
-    /**
-     * 业务订单号
-     * 传系统自己的订单号
-     */
-    private String bizOrderNo;
-
-    /**
-     * 快递公司编码,如 SF、YTO、ZTO、JDKY、EMS
-     */
-    private String shipperCode;
-
-    /**
-     * 运费支付方式
-     * 1:现付
-     * 2:到付
-     * 3:月结
-     */
-    private Integer payType;
-
-    /**
-     * 快递业务类型
-     * 常见值:1
-     */
-    private String expType;
-
-    /**
-     * 收件人姓名
-     */
-    private String receiverName;
-
-    /**
-     * 收件人手机号
-     */
-    private String receiverMobile;
-
-    /**
-     * 收件人电话
-     */
-    private String receiverTel;
-
-    /**
-     * 收件省
-     */
-    private String receiverProvinceName;
-
-    /**
-     * 收件市
-     */
-    private String receiverCityName;
-
-    /**
-     * 收件区/县
-     */
-    private String receiverExpAreaName;
-
-    /**
-     * 收件详细地址
-     * 不要包含省市区
-     */
-    private String receiverAddress;
-
-    /**
-     * 收件邮编
-     * EMS 场景建议传
-     */
-    private String receiverPostCode;
-
-    /**
-     * 商品名称
-     * 建议传类别,如:文件、衣服、电子产品
-     */
-    private String goodsName;
-
-    /**
-     * 商品数量
-     */
-    private Integer goodsQuantity;
-
-    /**
-     * 商品价格
-     */
-    private BigDecimal goodsPrice;
-
-    /**
-     * 商品重量
-     */
-    private BigDecimal goodsWeight;
-
-    /**
-     * 商品描述
-     */
-    private String goodsDesc;
-
-    /**
-     * 包裹数量
-     */
-    private Integer quantity;
-
-    /**
-     * 总重量(kg)
-     */
-    private BigDecimal weight;
-
-    /**
-     * 总体积(m³)
-     */
-    private BigDecimal volume;
-
-    /**
-     * 运费
-     */
-    private BigDecimal cost;
-
-    /**
-     * 其他费用
-     */
-    private BigDecimal otherCost;
-
-    /**
-     * 备注
-     */
-    private String remark;
-
-    /**
-     * 是否返回面单模板
-     * 0:否
-     * 1:是
-     */
-    private String isReturnPrintTemplate;
-
-    /**
-     * 面单模板尺寸
-     */
-    private String templateSize;
-
-    /**
-     * 是否订阅轨迹推送
-     * 1:订阅
-     * 0:不订阅
-     */
-    private String isSubscribe;
-
-    /**
-     * 是否通知上门取件
-     * 0:通知
-     * 1:不通知
-     */
-    private Integer isNotice;
-
-    /**
-     * 上门取件开始时间
-     * 格式:yyyy-MM-dd HH:mm:ss
-     */
-    private String startDate;
-
-    /**
-     * 上门取件结束时间
-     */
-    private String endDate;
-
-    /**
-     * 是否要求签回单
-     * 0:否
-     * 1:是
-     */
-    private Integer isReturnSignBill;
-
-    /**
-     * 是否发送短信
-     * 0:否
-     * 1:是
-     */
-    private Integer isSendMessage;
-
-    /**
-     * 币种
-     * 特殊场景需要
-     */
-    private String currencyCode;
-}

+ 0 - 18
fs-admin/src/main/java/com/fs/kdniao/service/IKdniaoEOrderService.java

@@ -1,18 +0,0 @@
-package com.fs.kdniao.service;
-
-import com.fs.kdniao.domain.KdniaoEOrderResponse;
-import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
-
-/**
- * 快递鸟电子面单业务接口
- */
-public interface IKdniaoEOrderService {
-
-    /**
-     * 前端简化参数下单
-     *
-     * @param request 简化请求参数
-     * @return 下单结果
-     */
-    KdniaoEOrderResponse submitSimpleOrder(KdniaoSimpleOrderRequest request);
-}

+ 0 - 214
fs-admin/src/main/java/com/fs/kdniao/service/impl/KdniaoEOrderServiceImpl.java

@@ -1,214 +0,0 @@
-package com.fs.kdniao.service.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.fs.kdniao.config.KdniaoConfig;
-import com.fs.kdniao.domain.KdniaoCommodity;
-import com.fs.kdniao.domain.KdniaoEOrderRequest;
-import com.fs.kdniao.domain.KdniaoEOrderResponse;
-import com.fs.kdniao.domain.KdniaoPerson;
-import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
-import com.fs.kdniao.service.IKdniaoEOrderService;
-import com.fs.kdniao.util.KdniaoUtil;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-
-import java.net.URLEncoder;
-import java.util.Collections;
-
-/**
- * 快递鸟电子面单业务实现类
- */
-@Service
-public class KdniaoEOrderServiceImpl implements IKdniaoEOrderService {
-
-    @Autowired
-    private KdniaoConfig kdniaoConfig;
-
-    /**
-     * 前端简化参数下单
-     */
-    @Override
-    public KdniaoEOrderResponse submitSimpleOrder(KdniaoSimpleOrderRequest request) {
-        validateRequest(request);
-
-        KdniaoEOrderRequest eOrderRequest = buildEOrderRequest(request);
-
-        String requestData = KdniaoUtil.toRequestDataJson(eOrderRequest);
-        String dataSign = KdniaoUtil.getDataSign(requestData, kdniaoConfig.getApiKey());
-
-        try {
-            String formData = buildFormData(requestData, dataSign);
-            String result = KdniaoUtil.doPost(kdniaoConfig.getReqURL(), formData);
-            return JSON.parseObject(result, KdniaoEOrderResponse.class);
-        } catch (Exception e) {
-            throw new RuntimeException("电子面单下单失败:" + e.getMessage(), e);
-        }
-    }
-
-    /**
-     * 组装快递鸟标准请求对象
-     */
-    private KdniaoEOrderRequest buildEOrderRequest(KdniaoSimpleOrderRequest request) {
-        KdniaoEOrderRequest eOrderRequest = new KdniaoEOrderRequest();
-
-        // 基础字段
-        eOrderRequest.setOrderCode(buildOrderCode(request.getBizOrderNo()));
-        eOrderRequest.setShipperCode(request.getShipperCode());
-        eOrderRequest.setCustomerName(kdniaoConfig.getAccount().getCustomerName());
-        eOrderRequest.setCustomerPwd(kdniaoConfig.getAccount().getCustomerPwd());
-        eOrderRequest.setSendSite(kdniaoConfig.getAccount().getSendSite());
-        eOrderRequest.setMonthCode(kdniaoConfig.getAccount().getMonthCode());
-        eOrderRequest.setPayType(request.getPayType());
-        eOrderRequest.setExpType(request.getExpType());
-
-        // 发件人
-        eOrderRequest.setSender(buildSender());
-
-        // 收件人
-        eOrderRequest.setReceiver(buildReceiver(request));
-
-        // 商品
-        eOrderRequest.setCommodity(Collections.singletonList(buildCommodity(request)));
-
-        // 包裹信息
-        eOrderRequest.setQuantity(request.getQuantity() == null ? 1 : request.getQuantity());
-        eOrderRequest.setWeight(request.getWeight());
-        eOrderRequest.setVolume(request.getVolume());
-        eOrderRequest.setCost(request.getCost());
-        eOrderRequest.setOtherCost(request.getOtherCost());
-
-        // 打印和备注
-        eOrderRequest.setRemark(trimToNull(request.getRemark()));
-        eOrderRequest.setIsReturnPrintTemplate(StringUtils.hasText(request.getIsReturnPrintTemplate()) ? request.getIsReturnPrintTemplate() : "1");
-        eOrderRequest.setTemplateSize(StringUtils.hasText(request.getTemplateSize()) ? request.getTemplateSize() : "130");
-        eOrderRequest.setIsSubscribe(StringUtils.hasText(request.getIsSubscribe()) ? request.getIsSubscribe() : "0");
-
-        // 上门取件
-        eOrderRequest.setIsNotice(request.getIsNotice());
-        eOrderRequest.setStartDate(trimToNull(request.getStartDate()));
-        eOrderRequest.setEndDate(trimToNull(request.getEndDate()));
-
-        // 其他
-        eOrderRequest.setIsReturnSignBill(request.getIsReturnSignBill());
-        eOrderRequest.setIsSendMessage(request.getIsSendMessage());
-        eOrderRequest.setCurrencyCode(trimToNull(request.getCurrencyCode()));
-
-        return eOrderRequest;
-    }
-
-    /**
-     * 构建订单号
-     * 规则:业务订单号 + 时间戳,保证唯一
-     */
-    private String buildOrderCode(String bizOrderNo) {
-        if (StringUtils.hasText(bizOrderNo)) {
-            return bizOrderNo.trim() + "-" + System.currentTimeMillis();
-        }
-        return "KD" + System.currentTimeMillis();
-    }
-
-    /**
-     * 组装默认发件人
-     */
-    private KdniaoPerson buildSender() {
-        KdniaoPerson sender = new KdniaoPerson();
-        sender.setCompany(trimToNull(kdniaoConfig.getSender().getCompany()));
-        sender.setName(kdniaoConfig.getSender().getName());
-        sender.setMobile(kdniaoConfig.getSender().getMobile());
-        sender.setProvinceName(kdniaoConfig.getSender().getProvinceName());
-        sender.setCityName(kdniaoConfig.getSender().getCityName());
-        sender.setExpAreaName(kdniaoConfig.getSender().getExpAreaName());
-        sender.setAddress(kdniaoConfig.getSender().getAddress());
-        sender.setPostCode(trimToNull(kdniaoConfig.getSender().getPostCode()));
-        return sender;
-    }
-
-    /**
-     * 组装收件人
-     */
-    private KdniaoPerson buildReceiver(KdniaoSimpleOrderRequest request) {
-        KdniaoPerson receiver = new KdniaoPerson();
-        receiver.setName(request.getReceiverName());
-        receiver.setMobile(trimToNull(request.getReceiverMobile()));
-        receiver.setTel(trimToNull(request.getReceiverTel()));
-        receiver.setProvinceName(request.getReceiverProvinceName());
-        receiver.setCityName(request.getReceiverCityName());
-        receiver.setExpAreaName(request.getReceiverExpAreaName());
-        receiver.setAddress(request.getReceiverAddress());
-        receiver.setPostCode(trimToNull(request.getReceiverPostCode()));
-        return receiver;
-    }
-
-    /**
-     * 组装商品信息
-     */
-    private KdniaoCommodity buildCommodity(KdniaoSimpleOrderRequest request) {
-        KdniaoCommodity commodity = new KdniaoCommodity();
-        commodity.setGoodsName(request.getGoodsName());
-        commodity.setGoodsquantity(request.getGoodsQuantity() == null ? 1 : request.getGoodsQuantity());
-        commodity.setGoodsPrice(request.getGoodsPrice());
-        commodity.setGoodsWeight(request.getGoodsWeight());
-        commodity.setGoodsDesc(trimToNull(request.getGoodsDesc()));
-        return commodity;
-    }
-
-    /**
-     * 构建表单请求参数
-     */
-    private String buildFormData(String requestData, String dataSign) throws Exception {
-        StringBuilder sb = new StringBuilder();
-        sb.append("RequestData=").append(URLEncoder.encode(requestData, "UTF-8"));
-        sb.append("&EBusinessID=").append(URLEncoder.encode(kdniaoConfig.getEBusinessID(), "UTF-8"));
-        sb.append("&RequestType=").append(URLEncoder.encode("1007", "UTF-8"));
-        sb.append("&DataSign=").append(dataSign);
-        sb.append("&DataType=").append(URLEncoder.encode("2", "UTF-8"));
-        return sb.toString();
-    }
-
-    /**
-     * 前端简化参数校验
-     */
-    private void validateRequest(KdniaoSimpleOrderRequest request) {
-        if (request == null) {
-            throw new IllegalArgumentException("请求参数不能为空");
-        }
-        if (!StringUtils.hasText(request.getShipperCode())) {
-            throw new IllegalArgumentException("shipperCode不能为空");
-        }
-        if (request.getPayType() == null) {
-            throw new IllegalArgumentException("payType不能为空");
-        }
-        if (!StringUtils.hasText(request.getExpType())) {
-            throw new IllegalArgumentException("expType不能为空");
-        }
-        if (!StringUtils.hasText(request.getReceiverName())) {
-            throw new IllegalArgumentException("receiverName不能为空");
-        }
-        if (!StringUtils.hasText(request.getReceiverMobile()) && !StringUtils.hasText(request.getReceiverTel())) {
-            throw new IllegalArgumentException("receiverMobile和receiverTel至少填写一个");
-        }
-        if (!StringUtils.hasText(request.getReceiverProvinceName())) {
-            throw new IllegalArgumentException("receiverProvinceName不能为空");
-        }
-        if (!StringUtils.hasText(request.getReceiverCityName())) {
-            throw new IllegalArgumentException("receiverCityName不能为空");
-        }
-        if (!StringUtils.hasText(request.getReceiverExpAreaName())) {
-            throw new IllegalArgumentException("receiverExpAreaName不能为空");
-        }
-        if (!StringUtils.hasText(request.getReceiverAddress())) {
-            throw new IllegalArgumentException("receiverAddress不能为空");
-        }
-        if (!StringUtils.hasText(request.getGoodsName())) {
-            throw new IllegalArgumentException("goodsName不能为空");
-        }
-    }
-
-    /**
-     * 去除空白,空字符串转 null
-     */
-    private String trimToNull(String value) {
-        return StringUtils.hasText(value) ? value.trim() : null;
-    }
-}

+ 0 - 114
fs-admin/src/main/java/com/fs/kdniao/util/KdniaoUtil.java

@@ -1,114 +0,0 @@
-package com.fs.kdniao.util;
-
-import com.alibaba.fastjson.JSON;
-import com.fs.kdniao.domain.KdniaoEOrderRequest;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.security.MessageDigest;
-import java.util.Base64;
-
-/**
- * 快递鸟工具类
- */
-public class KdniaoUtil {
-
-    private KdniaoUtil() {
-    }
-
-    /**
-     * 将标准请求对象转成 RequestData JSON 字符串
-     */
-    public static String toRequestDataJson(KdniaoEOrderRequest request) {
-        return JSON.toJSONString(request);
-    }
-
-    /**
-     * 生成 DataSign
-     * 规则:Base64(MD5(RequestData + ApiKey))
-     */
-    public static String getDataSign(String requestData, String apiKey) {
-        try {
-            String md5Result = md5(requestData + apiKey);
-            String base64 = Base64.getEncoder().encodeToString(md5Result.getBytes("UTF-8"));
-            return URLEncoder.encode(base64, "UTF-8");
-        } catch (Exception e) {
-            throw new RuntimeException("生成DataSign失败", e);
-        }
-    }
-
-    /**
-     * POST 表单请求
-     */
-    public static String doPost(String reqURL, String formData) {
-        HttpURLConnection connection = null;
-        OutputStream os = null;
-        BufferedReader br = null;
-        try {
-            URL url = new URL(reqURL);
-            connection = (HttpURLConnection) url.openConnection();
-            connection.setRequestMethod("POST");
-            connection.setConnectTimeout(10000);
-            connection.setReadTimeout(20000);
-            connection.setDoOutput(true);
-            connection.setDoInput(true);
-            connection.setUseCaches(false);
-            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
-
-            os = connection.getOutputStream();
-            os.write(formData.getBytes("UTF-8"));
-            os.flush();
-
-            br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
-            StringBuilder sb = new StringBuilder();
-            String line;
-            while ((line = br.readLine()) != null) {
-                sb.append(line);
-            }
-            return sb.toString();
-        } catch (Exception e) {
-            throw new RuntimeException("调用快递鸟接口失败", e);
-        } finally {
-            try {
-                if (os != null) {
-                    os.close();
-                }
-            } catch (Exception ignored) {
-            }
-            try {
-                if (br != null) {
-                    br.close();
-                }
-            } catch (Exception ignored) {
-            }
-            if (connection != null) {
-                connection.disconnect();
-            }
-        }
-    }
-
-    /**
-     * MD5
-     */
-    private static String md5(String text) {
-        try {
-            MessageDigest md = MessageDigest.getInstance("MD5");
-            byte[] digest = md.digest(text.getBytes("UTF-8"));
-            StringBuilder sb = new StringBuilder();
-            for (byte b : digest) {
-                String hex = Integer.toHexString(b & 0xff);
-                if (hex.length() == 1) {
-                    sb.append("0");
-                }
-                sb.append(hex);
-            }
-            return sb.toString();
-        } catch (Exception e) {
-            throw new RuntimeException("MD5计算失败", e);
-        }
-    }
-}

+ 0 - 45
fs-admin/src/main/java/com/fs/kdniaoNew/controller/KdniaoUniversalEOrderController.java

@@ -1,45 +0,0 @@
-package com.fs.kdniaoNew.controller;
-
-import com.fs.common.annotation.Log;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.enums.BusinessType;
-import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
-import com.fs.kdniaoNew.domain.KdniaoUniversalResponse;
-import com.fs.kdniaoNew.service.IKdniaoUniversalEOrderService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-
-/**
- * 快递鸟统一电子面单控制器
- */
-@RestController
-@RequestMapping("/kdniao/universal/eorder")
-public class KdniaoUniversalEOrderController extends BaseController {
-
-    @Autowired
-    private IKdniaoUniversalEOrderService kdniaoUniversalEOrderService;
-
-    /**
-     * 统一下单
-     */
-    @Log(title = "快递鸟统一电子面单", businessType = BusinessType.INSERT)
-    @PostMapping("/submit")
-    public AjaxResult submit(@RequestBody KdniaoSubmitCommand command) {
-        try {
-            KdniaoUniversalResponse response = kdniaoUniversalEOrderService.submit(command);
-
-            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
-                return AjaxResult.success("下单成功", response);
-            }
-
-            if ("106".equals(response.getResultCode())) {
-                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功");
-            }
-
-            return AjaxResult.error("下单失败:" + response.getReason(), response);
-        } catch (Exception e) {
-            return AjaxResult.error("下单异常:" + e.getMessage());
-        }
-    }
-}

+ 36 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java

@@ -8,9 +8,12 @@ import java.util.Set;
 import java.util.stream.Collectors;
 
 import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddWxExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import org.springframework.beans.BeanUtils;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -127,6 +130,39 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         return toAjax(companyVoiceRoboticCallLogAddwxService.deleteCompanyVoiceRoboticCallLogAddwxByLogIds(logIds));
     }
 
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/listByClientIdAndRoboticId")
+    public TableDataInfo listByClientIdAndRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx) {
+        startPage();
+        List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listByRoboticId(companyVoiceRoboticCallLogAddwx);
+        return getDataTable(list);
+
+    }
+
+
+    /**
+     * 加微统计数据(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/groupList")
+    public TableDataInfo groupList(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogAddwx> list = companyVoiceRoboticCallLogAddwxService.selectCompanyVoiceRoboticAddwxLogGroupList(companyVoiceRoboticCallLogAddwx);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticAddwxLogCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogAddwxService.selectCompanyVoiceRoboticAddwxLogCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
+    }
+
 //    /**
 //     * 导出调用日志_加微信列表
 //     */

+ 2 - 2
fs-company/src/main/resources/application.yml

@@ -4,12 +4,12 @@ server:
 spring:
   profiles:
 #    active: druid-ylrz
-    active: dev
+#    active: dev
 #    active: druid-jnsyj-test
 #    active: druid-jnmy-test
 #    active: druid-jzzx-test
 #    active: druid-hdt
-#    active: druid-bjzm-test
+    active: druid-bjzm-test
 #    active: druid-yzt
 #    active: druid-myhk
 #    active: druid-sft

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java

@@ -72,6 +72,9 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
 
     private Integer qwWxAddWayId;
 
+    @TableField(exist = false)
+    private String successRate;
+
     public static CompanyVoiceRoboticCallLogAddwx initCallLog( String runParam, Long keyId, Long taskId,Long wxAccountId,Long companyId,int qwWxAddWayId) {
         CompanyVoiceRoboticCallLogAddwx log = new CompanyVoiceRoboticCallLogAddwx();
         log.wxClientId = keyId;

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -133,6 +133,9 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private Integer runningCount;
 
+    @TableField(exist = false)
+    private String successRate;
+
     public static CompanyVoiceRoboticCallLogCallphone initCallLog( String runParam, Long keyId, Long taskId,Long companyId) {
         CompanyVoiceRoboticCallLogCallphone log = new CompanyVoiceRoboticCallLogCallphone();
         log.callerId = keyId;

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java

@@ -92,6 +92,9 @@ public class CompanyVoiceRoboticCallLogSendmsg extends BaseEntity{
 
     private String phone;
 
+    @TableField(exist = false)
+    private String successRate;
+
 
 
     public static CompanyVoiceRoboticCallLogSendmsg initCallLog( String runParam, Long keyId, Long taskId,Long companyId,Long companyUserId,Long tempId) {

+ 12 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java

@@ -77,4 +77,16 @@ public class CompanyVoiceRoboticCallees{
 
     @TableField(exist = false)
     private List<CrmCustomerProperty> tagList ;
+
+    /**
+    * 是否添加;0否1是2加微中
+    */
+    @TableField(exist = false)
+    private Integer isAdd;
+
+    /**
+    * 客服id
+    */
+    @TableField(exist = false)
+    private Integer customer_id;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java

@@ -76,6 +76,9 @@ public interface CompanyConfigMapper
     @Select("select config_value from company_config where company_id=#{companyId} and config_key='redPacket:config' ")
     String selectRedPacketConfigByKey(Long companyId);
 
+    @Select("select config_value from company_config where company_id=#{companyId} and config_key='his.AppRedPacket' ")
+    String selectRedPacketConfigByKeyApp(Long companyId);
+
     @Select("select \n" +
             "id,\n" +
             "name,\n" +

+ 23 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogAddwxMapper.java

@@ -4,7 +4,10 @@ import java.util.List;
 import java.util.Map;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -70,4 +73,24 @@ public interface CompanyVoiceRoboticCallLogAddwxMapper extends BaseMapper<Compan
 
     Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
+    /**
+     * 查询加微记录分组列表
+     * @param companyVoiceRoboticCallLogAddwx
+     * @return
+     */
+    List<CompanyVoiceRoboticCallLogAddwx> selectCompanyVoiceRoboticCallLogAddwxGroupList (CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    /**
+     * 查询加微记录详情
+     * @param companyVoiceRoboticCallLogAddwx
+     * @return
+     */
+    List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    /**
+     * 查询加微记录统计(所有任务)
+     * @return
+     */
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticAddwxLogCount();
+
 }

+ 13 - 7
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCalleesMapper.java

@@ -5,20 +5,21 @@ import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.SendMsgByTaskVO;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
 import java.util.Set;
 
 /**
  * 任务外呼电话Mapper接口
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
 public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoiceRoboticCallees> {
     /**
      * 查询任务外呼电话
-     * 
+     *
      * @param id 任务外呼电话ID
      * @return 任务外呼电话
      */
@@ -26,15 +27,20 @@ public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoic
 
     /**
      * 查询任务外呼电话列表
-     * 
+     *
      * @param companyVoiceRoboticCallees 任务外呼电话
      * @return 任务外呼电话集合
      */
     public List<CompanyVoiceRoboticCallees> selectCompanyVoiceRoboticCalleesList(CompanyVoiceRoboticCallees companyVoiceRoboticCallees);
 
+    @Select("select cv.*,cw.is_add,cw.customer_id from company_voice_robotic_callees  cv " +
+            "left join  company_wx_client cw on cv.robotic_id = cw.robotic_id " +
+            "where cv.robotic_id = #{roboticId}")
+    public List<CompanyVoiceRoboticCallees> selectCompanyVoiceRoboticCalleesListByRoboticId(@Param("roboticId") Long id);
+
     /**
      * 新增任务外呼电话
-     * 
+     *
      * @param companyVoiceRoboticCallees 任务外呼电话
      * @return 结果
      */
@@ -42,7 +48,7 @@ public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoic
 
     /**
      * 修改任务外呼电话
-     * 
+     *
      * @param companyVoiceRoboticCallees 任务外呼电话
      * @return 结果
      */
@@ -50,7 +56,7 @@ public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoic
 
     /**
      * 删除任务外呼电话
-     * 
+     *
      * @param id 任务外呼电话ID
      * @return 结果
      */
@@ -58,7 +64,7 @@ public interface CompanyVoiceRoboticCalleesMapper extends BaseMapper<CompanyVoic
 
     /**
      * 批量删除任务外呼电话
-     * 
+     *
      * @param ids 需要删除的数据ID
      * @return 结果
      */

+ 1 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java

@@ -72,6 +72,7 @@ public interface ICompanyConfigService
     CompanyConfig selectCompanyConfigByServerKey(String key);
 
     String selectRedPacketConfigByKey(Long companyId);
+    String selectRedPacketConfigByKeyApp(Long companyId);
 
     /**
      * 获取公司可配置小程序列表

+ 16 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java

@@ -4,8 +4,11 @@ import java.util.List;
 import java.util.Map;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
 /**
  * 调用日志_加微信Service接口
@@ -75,4 +78,17 @@ public interface ICompanyVoiceRoboticCallLogAddwxService extends IService<Compan
 
     Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
+    List<CompanyVoiceRoboticCallLogAddwx> selectCompanyVoiceRoboticAddwxLogGroupList(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticAddwxLogCount();
+
+    /**
+     * 查询加微记录详情
+     * @param companyVoiceRoboticCallLogAddwx
+     * @return
+     */
+    List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+
+
 }

+ 25 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java

@@ -44,6 +44,7 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
     @Autowired
     private RedisTemplate<String, String> redisTemplate; // 注入RedisTemplate
     private static final String REDIS_KEY_PREFIX = "red_packet_config:";
+    private static final String REDIS_KEY_PREFIX_APP = "red_packet_config_app:";
     private static final long CACHE_TIMEOUT = 24 * 60 * 60;
 
     @Autowired
@@ -177,6 +178,30 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
         }
     }
 
+    @Override
+    public String selectRedPacketConfigByKeyApp(Long companyId) {
+        Asserts.notNull(companyId,"公司id不能为空!");
+        String redisKey = REDIS_KEY_PREFIX_APP + companyId;
+        String cachedConfig = redisTemplate.opsForValue().get(redisKey);
+        if (cachedConfig != null) {
+            return cachedConfig;
+        }
+        synchronized (getSynchronizationObject(companyId)) {
+            cachedConfig = redisTemplate.opsForValue().get(redisKey);
+
+            if (cachedConfig != null) {
+                return cachedConfig;
+            }
+            String configFromDb = companyConfigMapper.selectRedPacketConfigByKeyApp(companyId);
+            if (configFromDb != null) {
+                redisTemplate.opsForValue().set(redisKey, configFromDb, CACHE_TIMEOUT, TimeUnit.SECONDS);
+            } else {
+                redisTemplate.opsForValue().set(redisKey, "", 5 * 60, TimeUnit.SECONDS);
+            }
+            return configFromDb;
+        }
+    }
+
     private static final ConcurrentHashMap<Long, Object> LOCKS = new ConcurrentHashMap<>();
     private static Object getSynchronizationObject(Long companyId) {
         return LOCKS.computeIfAbsent(companyId, k -> new Object());

+ 18 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java

@@ -6,10 +6,13 @@ import java.util.Map;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.company.domain.CompanyVoiceRoboticCallLog;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
 import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
@@ -162,4 +165,19 @@ class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoic
         return baseMapper.countListAll(companyVoiceRoboticCallLogAddwx);
     }
 
+    @Override
+    public List<CompanyVoiceRoboticCallLogAddwx> selectCompanyVoiceRoboticAddwxLogGroupList(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx) {
+        return baseMapper.selectCompanyVoiceRoboticCallLogAddwxGroupList(companyVoiceRoboticCallLogAddwx);
+    }
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticAddwxLogCount() {
+        return baseMapper.selectCompanyVoiceRoboticAddwxLogCount();
+    }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx) {
+        return baseMapper.listByRoboticId(companyVoiceRoboticCallLogAddwx);
+    }
+
 }

+ 11 - 10
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCalleesServiceImpl.java

@@ -11,7 +11,7 @@ import java.util.List;
 
 /**
  * 任务外呼电话Service业务层处理
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -23,7 +23,7 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     /**
      * 查询任务外呼电话
-     * 
+     *
      * @param id 任务外呼电话ID
      * @return 任务外呼电话
      */
@@ -35,7 +35,7 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     /**
      * 查询任务外呼电话列表
-     * 
+     *
      * @param companyVoiceRoboticCallees 任务外呼电话
      * @return 任务外呼电话
      */
@@ -47,7 +47,7 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     /**
      * 新增任务外呼电话
-     * 
+     *
      * @param companyVoiceRoboticCallees 任务外呼电话
      * @return 结果
      */
@@ -59,7 +59,7 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     /**
      * 修改任务外呼电话
-     * 
+     *
      * @param companyVoiceRoboticCallees 任务外呼电话
      * @return 结果
      */
@@ -71,7 +71,7 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     /**
      * 批量删除任务外呼电话
-     * 
+     *
      * @param ids 需要删除的任务外呼电话ID
      * @return 结果
      */
@@ -83,7 +83,7 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     /**
      * 删除任务外呼电话信息
-     * 
+     *
      * @param id 任务外呼电话ID
      * @return 结果
      */
@@ -95,9 +95,10 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     @Override
     public List<CompanyVoiceRoboticCallees> selectCompanyVoiceRoboticCalleesListByRoboticId(Long id) {
-        CompanyVoiceRoboticCallees param = new CompanyVoiceRoboticCallees();
-        param.setRoboticId(id);
-        List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = selectCompanyVoiceRoboticCalleesList(param);
+//        CompanyVoiceRoboticCallees param = new CompanyVoiceRoboticCallees();
+//        param.setRoboticId(id);
+//        List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = selectCompanyVoiceRoboticCalleesList(param);
+        List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
         if(null != companyVoiceRoboticCallees && !companyVoiceRoboticCallees.isEmpty()){
             companyVoiceRoboticCallees.forEach(item -> {
                 item.setIdToString(item.getId().toString());

+ 4 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCategoryServiceImpl.java

@@ -70,9 +70,10 @@ public class FsUserCourseCategoryServiceImpl implements IFsUserCourseCategorySer
     }
 
     @Override
-    @Cacheable(
-            value = PublicCourseAppCacheNames.CATEGORY_APP_LIST,
-            key = "#param == null ? 'default' : #param.appListCacheKey()")
+//    @Cacheable(
+//            value = PublicCourseAppCacheNames.CATEGORY_APP_LIST,
+//            key = "#param == null ? 'default' : #param.appListCacheKey()")
+    @Cacheable(value = "publicCourseCategoryApp", key = "#param")
     public PageInfo<FsUserCourseCategory> selectFsUserCourseCategoryAppPage(FsUserCourseCategoryAppQueryParam param) {
         if (param == null) {
             param = new FsUserCourseCategoryAppQueryParam();

+ 2 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -5472,7 +5472,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 // 调用第三方接口(锁外操作)
                 R sendRedPacket;
                 try {
-                    sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                    sendRedPacket = paymentService.sendAppRedPacket(packetParam, config);
                 } catch (Exception e) {
                     logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
                     // 异常时回滚余额
@@ -5538,7 +5538,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
                 try{
                     // 发送红包
-                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam,config);
                     if (sendRedPacket.get("code").equals(200)) {
                         FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
                         TransferBillsResult transferBillsResult;

+ 49 - 0
fs-service/src/main/java/com/fs/distribution/constant/DistributionConstants.java

@@ -0,0 +1,49 @@
+package com.fs.distribution.constant;
+
+/**
+ * 分销相关常量
+ */
+public class DistributionConstants {
+
+    private DistributionConstants() {}
+
+    /**
+     * 分销关系状态
+     */
+    public static final Integer RELATION_NORMAL = 1;
+    public static final Integer RELATION_DISABLED = 0;
+
+    /**
+     * 分销账户状态
+     */
+    public static final Integer ACCOUNT_NORMAL = 1;
+    public static final Integer ACCOUNT_DISABLED = 0;
+
+    /**
+     * 佣金状态:0冻结中,1可提现,2已提现,3已失效
+     */
+    public static final Integer COMMISSION_FROZEN = 0;
+    public static final Integer COMMISSION_AVAILABLE = 1;
+    public static final Integer COMMISSION_WITHDRAWN = 2;
+    public static final Integer COMMISSION_INVALID = 3;
+
+    /**
+     * 提现状态:0待审核,1已打款,2已拒绝
+     */
+    public static final Integer WITHDRAW_WAIT_AUDIT = 0;
+    public static final Integer WITHDRAW_PAID = 1;
+    public static final Integer WITHDRAW_REJECT = 2;
+
+    /**
+     * 佣金层级
+     */
+    public static final Integer COMMISSION_LEVEL_ONE = 1;
+    public static final Integer COMMISSION_LEVEL_TWO = 2;
+
+    /**
+     * 绑定方式
+     */
+    public static final Integer BIND_TYPE_REGISTER = 1;
+    public static final Integer BIND_TYPE_FIRST_ORDER = 2;
+    public static final Integer BIND_TYPE_ADMIN = 3;
+}

+ 51 - 0
fs-service/src/main/java/com/fs/distribution/domain/DistributionAccount.java

@@ -0,0 +1,51 @@
+package com.fs.distribution.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 分销账户
+ */
+@Data
+public class DistributionAccount {
+
+    private Long id;
+
+    private Long userId;
+
+    /**
+     * 累计佣金
+     */
+    private BigDecimal totalCommission;
+
+    /**
+     * 冻结佣金
+     */
+    private BigDecimal frozenCommission;
+
+    /**
+     * 可提现佣金
+     */
+    private BigDecimal availableCommission;
+
+    /**
+     * 提现中佣金
+     */
+    private BigDecimal withdrawingCommission;
+
+    /**
+     * 已提现佣金
+     */
+    private BigDecimal withdrawnCommission;
+
+    /**
+     * 状态:1正常,0禁用
+     */
+    private Integer status;
+
+    private Date createTime;
+
+    private Date updateTime;
+}

+ 52 - 0
fs-service/src/main/java/com/fs/distribution/domain/DistributionCommissionRecord.java

@@ -0,0 +1,52 @@
+package com.fs.distribution.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 分销佣金记录
+ */
+@Data
+public class DistributionCommissionRecord {
+
+    private Long id;
+
+    private Long orderId;
+
+    private String orderNo;
+
+    private Long buyerUserId;
+
+    private Long distributorUserId;
+
+    /**
+     * 佣金层级:1一级,2二级
+     */
+    private Integer commissionLevel;
+
+    /**
+     * 订单分佣金额
+     */
+    private BigDecimal orderAmount;
+
+    /**
+     * 佣金比例,例如 10 表示 10%
+     */
+    private BigDecimal commissionRate;
+
+    /**
+     * 佣金金额
+     */
+    private BigDecimal commissionAmount;
+
+    /**
+     * 状态:0冻结中,1可提现,2已提现,3已失效
+     */
+    private Integer status;
+
+    private Date createTime;
+
+    private Date updateTime;
+}

+ 44 - 0
fs-service/src/main/java/com/fs/distribution/domain/DistributionConfig.java

@@ -0,0 +1,44 @@
+package com.fs.distribution.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 分销配置
+ */
+@Data
+public class DistributionConfig {
+
+    private Long id;
+
+    /**
+     * 分销开关:1开启,0关闭
+     */
+    private Integer enableStatus;
+
+    /**
+     * 一级佣金比例
+     */
+    private BigDecimal levelOneRate;
+
+    /**
+     * 二级佣金比例
+     */
+    private BigDecimal levelTwoRate;
+
+    /**
+     * 冻结天数
+     */
+    private Integer freezeDays;
+
+    /**
+     * 最低提现金额
+     */
+    private BigDecimal minWithdrawAmount;
+
+    private Date createTime;
+
+    private Date updateTime;
+}

+ 43 - 0
fs-service/src/main/java/com/fs/distribution/domain/DistributionUserRelation.java

@@ -0,0 +1,43 @@
+package com.fs.distribution.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户分销关系
+ */
+@Data
+public class DistributionUserRelation {
+
+    private Long id;
+
+    /**
+     * 当前用户ID
+     */
+    private Long userId;
+
+    /**
+     * 一级上级用户ID
+     */
+    private Long parentUserId;
+
+    /**
+     * 二级上级用户ID
+     */
+    private Long grandParentUserId;
+
+    /**
+     * 绑定方式:1注册绑定,2首次下单绑定,3后台绑定
+     */
+    private Integer bindType;
+
+    /**
+     * 状态:1正常,0无效
+     */
+    private Integer status;
+
+    private Date createTime;
+
+    private Date updateTime;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/distribution/domain/DistributionWithdrawRecord.java

@@ -0,0 +1,34 @@
+package com.fs.distribution.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 分销提现记录
+ */
+@Data
+public class DistributionWithdrawRecord {
+
+    private Long id;
+
+    private Long userId;
+
+    private String withdrawNo;
+
+    private BigDecimal withdrawAmount;
+
+    /**
+     * 状态:0待审核,1已打款,2已拒绝
+     */
+    private Integer status;
+
+    private String remark;
+
+    private Date createTime;
+
+    private Date auditTime;
+
+    private Date updateTime;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/distribution/dto/BindRelationRequest.java

@@ -0,0 +1,25 @@
+package com.fs.distribution.dto;
+
+import lombok.Data;
+
+/**
+ * 绑定分销关系请求
+ */
+@Data
+public class BindRelationRequest {
+
+    /**
+     * 当前注册用户ID
+     */
+    private Long userId;
+
+    /**
+     * 邀请人ID
+     */
+    private Long inviteUserId;
+
+    /**
+     * 绑定方式:1注册绑定,2首次下单绑定,3后台绑定
+     */
+    private Integer bindType;
+}

+ 24 - 0
fs-service/src/main/java/com/fs/distribution/dto/CreateCommissionRequest.java

@@ -0,0 +1,24 @@
+package com.fs.distribution.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 创建订单佣金请求
+ */
+@Data
+public class CreateCommissionRequest {
+
+    private Long orderId;
+
+    private String orderNo;
+
+    private Long buyerUserId;
+
+    /**
+     * 订单实际参与分佣金额
+     * 商品实付金额,不含运费
+     */
+    private BigDecimal commissionBaseAmount;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/distribution/dto/QueryDistributionAccountRequest.java

@@ -0,0 +1,25 @@
+package com.fs.distribution.dto;
+
+import lombok.Data;
+
+/**
+ * 绑定分销关系请求
+ */
+@Data
+public class QueryDistributionAccountRequest {
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 邀请人ID
+     */
+    private Long inviteUserId;
+
+    /**
+     * 账户状态
+     */
+    private Integer status;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/distribution/dto/QueryDistributionAccountResponse.java

@@ -0,0 +1,18 @@
+package com.fs.distribution.dto;
+
+import com.fs.distribution.domain.DistributionAccount;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 绑定分销关系请求
+ */
+@Data
+public class QueryDistributionAccountResponse {
+
+    /**
+     * 用户ID
+     */
+    private List<DistributionAccount> list;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/distribution/dto/WithdrawApplyRequest.java

@@ -0,0 +1,16 @@
+package com.fs.distribution.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 用户申请提现
+ */
+@Data
+public class WithdrawApplyRequest {
+
+    private Long userId;
+
+    private BigDecimal withdrawAmount;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/distribution/dto/WithdrawAuditRequest.java

@@ -0,0 +1,19 @@
+package com.fs.distribution.dto;
+
+import lombok.Data;
+
+/**
+ * 提现审核
+ */
+@Data
+public class WithdrawAuditRequest {
+
+    private Long withdrawId;
+
+    /**
+     * 1通过,2拒绝
+     */
+    private Integer status;
+
+    private String remark;
+}

+ 61 - 0
fs-service/src/main/java/com/fs/distribution/mapper/DistributionAccountMapper.java

@@ -0,0 +1,61 @@
+package com.fs.distribution.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.distribution.domain.DistributionAccount;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+
+public interface DistributionAccountMapper extends BaseMapper<DistributionAccount> {
+
+    DistributionAccount selectByUserId(@Param("userId") Long userId);
+
+    /**
+     * 加锁查询账户,提现、金额变动时使用
+     */
+    DistributionAccount selectByUserIdForUpdate(@Param("userId") Long userId);
+
+    int insertAccount(DistributionAccount account);
+
+    /**
+     * 增加冻结佣金和累计佣金
+     */
+    int addFrozenCommission(@Param("userId") Long userId,
+                            @Param("amount") BigDecimal amount);
+
+    /**
+     * 冻结转可提现
+     */
+    int frozenToAvailable(@Param("userId") Long userId,
+                          @Param("amount") BigDecimal amount);
+
+    /**
+     * 扣减冻结佣金和累计佣金
+     */
+    int deductFrozenAndTotal(@Param("userId") Long userId,
+                             @Param("amount") BigDecimal amount);
+
+    /**
+     * 扣减可提现和累计佣金
+     */
+    int deductAvailableAndTotal(@Param("userId") Long userId,
+                                @Param("amount") BigDecimal amount);
+
+    /**
+     * 申请提现:可提现转提现中
+     */
+    int availableToWithdrawing(@Param("userId") Long userId,
+                               @Param("amount") BigDecimal amount);
+
+    /**
+     * 提现审核通过:提现中转已提现
+     */
+    int withdrawingToWithdrawn(@Param("userId") Long userId,
+                               @Param("amount") BigDecimal amount);
+
+    /**
+     * 提现审核拒绝:提现中退回可提现
+     */
+    int withdrawingBackToAvailable(@Param("userId") Long userId,
+                                   @Param("amount") BigDecimal amount);
+}

+ 23 - 0
fs-service/src/main/java/com/fs/distribution/mapper/DistributionCommissionRecordMapper.java

@@ -0,0 +1,23 @@
+package com.fs.distribution.mapper;
+
+import com.fs.distribution.domain.DistributionCommissionRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+public interface DistributionCommissionRecordMapper {
+
+    int insertRecord(DistributionCommissionRecord record);
+
+    int countByOrderId(@Param("orderId") Long orderId);
+
+    List<DistributionCommissionRecord> selectByOrderId(@Param("orderId") Long orderId);
+
+    List<DistributionCommissionRecord> selectFrozenRecordsBefore(@Param("beforeTime") Date beforeTime);
+
+    int updateStatus(@Param("id") Long id,
+                     @Param("status") Integer status);
+
+    List<DistributionCommissionRecord> selectByUserId(@Param("userId") Long userId);
+}

+ 10 - 0
fs-service/src/main/java/com/fs/distribution/mapper/DistributionConfigMapper.java

@@ -0,0 +1,10 @@
+package com.fs.distribution.mapper;
+
+import com.fs.distribution.domain.DistributionConfig;
+
+public interface DistributionConfigMapper {
+
+    DistributionConfig selectConfig();
+
+    int updateConfig(DistributionConfig config);
+}

+ 23 - 0
fs-service/src/main/java/com/fs/distribution/mapper/DistributionUserRelationMapper.java

@@ -0,0 +1,23 @@
+package com.fs.distribution.mapper;
+
+import com.fs.distribution.domain.DistributionUserRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface DistributionUserRelationMapper {
+
+    DistributionUserRelation selectByUserId(@Param("userId") Long userId);
+
+    int insertRelation(DistributionUserRelation relation);
+
+    int updateRelation(DistributionUserRelation relation);
+
+    int countFirstLevelTeam(@Param("userId") Long userId);
+
+    int countSecondLevelTeam(@Param("userId") Long userId);
+
+    List<DistributionUserRelation> selectFirstLevelTeam(@Param("userId") Long userId);
+
+    List<DistributionUserRelation> selectSecondLevelTeam(@Param("userId") Long userId);
+}

+ 24 - 0
fs-service/src/main/java/com/fs/distribution/mapper/DistributionWithdrawRecordMapper.java

@@ -0,0 +1,24 @@
+package com.fs.distribution.mapper;
+
+import com.fs.distribution.domain.DistributionWithdrawRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface DistributionWithdrawRecordMapper {
+
+    int insertWithdrawRecord(DistributionWithdrawRecord record);
+
+    DistributionWithdrawRecord selectById(@Param("id") Long id);
+
+    DistributionWithdrawRecord selectByIdForUpdate(@Param("id") Long id);
+
+    int updateAuditStatus(DistributionWithdrawRecord record);
+
+    List<DistributionWithdrawRecord> selectByUserId(@Param("userId") Long userId);
+
+    /**
+     * 查询待审核的提现记录
+      */
+    List<DistributionWithdrawRecord> selectWithdrawList(DistributionWithdrawRecord query);
+}

+ 53 - 0
fs-service/src/main/java/com/fs/distribution/service/DistributionService.java

@@ -0,0 +1,53 @@
+package com.fs.distribution.service;
+
+import com.fs.distribution.domain.*;
+import com.fs.distribution.dto.*;
+
+import java.util.List;
+
+public interface DistributionService {
+
+    /**
+     * 注册成功后绑定邀请关系
+     */
+    void bindRelation(BindRelationRequest request);
+
+    /**
+     * 订单支付成功后生成佣金
+     */
+    void createCommissionAfterPay(CreateCommissionRequest request);
+
+    /**
+     * 订单完成并过售后期后,解冻佣金
+     */
+    void unlockFrozenCommission();
+
+    /**
+     * 订单退款时,让佣金失效
+     */
+    void invalidCommissionByRefund(Long orderId);
+
+    /**
+     * 用户申请提现
+     */
+    void applyWithdraw(WithdrawApplyRequest request);
+
+    /**
+     * 后台审核提现
+     */
+    void auditWithdraw(WithdrawAuditRequest request);
+
+    DistributionAccount getAccount(Long userId);
+
+    DistributionUserRelation getRelation(Long userId);
+
+    List<DistributionCommissionRecord> getCommissionList(Long userId);
+
+    List<DistributionWithdrawRecord> getWithdrawList(Long userId);
+
+    DistributionConfig getConfig();
+
+    void saveConfig(DistributionConfig config);
+
+    QueryDistributionAccountResponse getAccountList(QueryDistributionAccountRequest queryDistributionAccountRequest);
+}

+ 12 - 0
fs-service/src/main/java/com/fs/distribution/service/IDistributionAccountService.java

@@ -0,0 +1,12 @@
+package com.fs.distribution.service;
+
+
+import com.fs.distribution.dto.BindRelationRequest;
+
+public interface IDistributionAccountService {
+
+    /**
+     * 注册成功后绑定邀请关系
+     */
+    void bindRelation(BindRelationRequest request);
+}

+ 16 - 0
fs-service/src/main/java/com/fs/distribution/service/IDistributionCommissionRecordService.java

@@ -0,0 +1,16 @@
+package com.fs.distribution.service;
+
+import com.fs.distribution.dto.CreateCommissionRequest;
+
+public interface IDistributionCommissionRecordService {
+
+    /**
+     * 订单支付成功后生成佣金
+     */
+    void createCommissionAfterPay(CreateCommissionRequest request);
+
+    /**
+     * 订单完成并过售后期后,解冻佣金
+     */
+    void unlockFrozenCommission();
+}

+ 4 - 0
fs-service/src/main/java/com/fs/distribution/service/IDistributionConfigService.java

@@ -0,0 +1,4 @@
+package com.fs.distribution.service;
+
+public interface IDistributionConfigService {
+}

+ 4 - 0
fs-service/src/main/java/com/fs/distribution/service/IDistributionUserRelationService.java

@@ -0,0 +1,4 @@
+package com.fs.distribution.service;
+
+public interface IDistributionUserRelationService {
+}

+ 9 - 0
fs-service/src/main/java/com/fs/distribution/service/IDistributionWithdrawRecordService.java

@@ -0,0 +1,9 @@
+package com.fs.distribution.service;
+
+import com.fs.distribution.domain.DistributionWithdrawRecord;
+
+import java.util.List;
+
+public interface IDistributionWithdrawRecordService {
+    List<DistributionWithdrawRecord> selectWithdrawList(DistributionWithdrawRecord query);
+}

+ 148 - 0
fs-service/src/main/java/com/fs/distribution/service/impl/DistributionAccountServiceImpl.java

@@ -0,0 +1,148 @@
+
+package com.fs.distribution.service.impl;
+
+
+import com.fs.distribution.constant.DistributionConstants;
+import com.fs.distribution.domain.DistributionAccount;
+import com.fs.distribution.domain.DistributionUserRelation;
+import com.fs.distribution.dto.BindRelationRequest;
+import com.fs.distribution.mapper.DistributionAccountMapper;
+import com.fs.distribution.mapper.DistributionUserRelationMapper;
+import com.fs.distribution.service.IDistributionAccountService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+
+
+@Service
+public class DistributionAccountServiceImpl implements IDistributionAccountService {
+
+
+    @Resource
+    private DistributionUserRelationMapper relationMapper;
+
+    @Resource
+    private DistributionAccountMapper accountMapper;
+
+    /**
+     * 注册成功后绑定分销关系
+     *
+     * 核心规则:
+     * 1. 只能绑定一次
+     * 2. 不能绑定自己
+     * 3. 邀请人必须存在
+     * 4. 当前用户绑定 parentUserId
+     * 5. 当前用户的 grandParentUserId = 邀请人的 parentUserId
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void bindRelation(BindRelationRequest request) {
+        if (request == null || request.getUserId() == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+
+        Long userId = request.getUserId();
+        Long inviteUserId = request.getInviteUserId();
+
+        // 先保证当前用户有分销账户
+        createAccountIfAbsent(userId);
+
+        // 如果没有邀请人,也创建一条空关系,方便后续查询
+        if (inviteUserId == null) {
+//            createEmptyRelationIfAbsent(userId);
+            return;
+        }
+
+        // 不能绑定自己
+        if (userId.equals(inviteUserId)) {
+            throw new RuntimeException("不能绑定自己为邀请人");
+        }
+
+        // 当前用户是否已经有关系
+        DistributionUserRelation currentRelation = relationMapper.selectByUserId(userId);
+        if (currentRelation != null && currentRelation.getParentUserId() != null && currentRelation.getParentUserId() > 0) {
+            // 已绑定过上级,不允许重复绑定
+            return;
+        }
+
+        // 查询邀请人的关系
+        DistributionUserRelation inviterRelation = relationMapper.selectByUserId(inviteUserId);
+
+        // 邀请人必须有账户,没有则创建
+        createAccountIfAbsent(inviteUserId);
+
+        Long grandParentUserId = null;
+
+        if (inviterRelation != null && inviterRelation.getParentUserId() != null) {
+            grandParentUserId = inviterRelation.getParentUserId();
+        }
+
+        DistributionUserRelation relation = new DistributionUserRelation();
+        relation.setUserId(userId);
+        relation.setParentUserId(inviteUserId);
+        relation.setGrandParentUserId(grandParentUserId);
+        relation.setBindType(request.getBindType() == null
+                ? DistributionConstants.BIND_TYPE_REGISTER
+                : request.getBindType());
+        relation.setStatus(DistributionConstants.RELATION_NORMAL);
+
+        if (currentRelation == null) {
+            relationMapper.insertRelation(relation);
+        } else {
+            relationMapper.updateRelation(relation);
+        }
+    }
+
+    /**
+     * 如果账户不存在,则创建账户
+     */
+    private void createAccountIfAbsent(Long userId) {
+        DistributionAccount exist = accountMapper.selectByUserId(userId);
+        if (exist != null) {
+            return;
+        }
+
+        DistributionAccount account = new DistributionAccount();
+        account.setUserId(userId);
+        account.setStatus(DistributionConstants.ACCOUNT_NORMAL);
+
+        try {
+            accountMapper.insertAccount(account);
+        } catch (Exception e) {
+            // 并发场景下,可能两个请求同时创建,唯一索引会挡住
+            // 这里再次查询,存在就忽略,不存在再抛异常
+            DistributionAccount after = accountMapper.selectByUserId(userId);
+            if (after == null) {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * 没有邀请人的用户,也创建一条空关系
+     */
+    private void createEmptyRelationIfAbsent(Long userId) {
+        DistributionUserRelation exist = relationMapper.selectByUserId(userId);
+        if (exist != null) {
+            return;
+        }
+
+        DistributionUserRelation relation = new DistributionUserRelation();
+        relation.setUserId(userId);
+        relation.setParentUserId(0L);
+        relation.setGrandParentUserId(0L);
+        relation.setBindType(DistributionConstants.BIND_TYPE_REGISTER);
+        relation.setStatus(DistributionConstants.RELATION_NORMAL);
+
+        try {
+            relationMapper.insertRelation(relation);
+        } catch (Exception e) {
+            DistributionUserRelation after = relationMapper.selectByUserId(userId);
+            if (after == null) {
+                throw e;
+            }
+        }
+    }
+
+}

+ 231 - 0
fs-service/src/main/java/com/fs/distribution/service/impl/DistributionCommissionRecordServiceImpl.java

@@ -0,0 +1,231 @@
+package com.fs.distribution.service.impl;
+
+
+import com.fs.distribution.constant.DistributionConstants;
+import com.fs.distribution.domain.DistributionAccount;
+import com.fs.distribution.domain.DistributionCommissionRecord;
+import com.fs.distribution.domain.DistributionConfig;
+import com.fs.distribution.domain.DistributionUserRelation;
+import com.fs.distribution.dto.CreateCommissionRequest;
+import com.fs.distribution.mapper.DistributionAccountMapper;
+import com.fs.distribution.mapper.DistributionCommissionRecordMapper;
+import com.fs.distribution.mapper.DistributionConfigMapper;
+import com.fs.distribution.mapper.DistributionUserRelationMapper;
+import com.fs.distribution.service.IDistributionCommissionRecordService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+
+@Service
+public class DistributionCommissionRecordServiceImpl implements IDistributionCommissionRecordService {
+
+
+    @Resource
+    private DistributionUserRelationMapper relationMapper;
+    @Resource
+    private DistributionCommissionRecordMapper commissionRecordMapper;
+    @Resource
+    private DistributionConfigMapper configMapper;
+    @Resource
+    private DistributionAccountMapper accountMapper;
+
+    /**
+     * 订单支付成功后生成佣金
+     *
+     * 注意:
+     * 1. 必须幂等,同一个订单不能重复生成佣金
+     * 2. 佣金先进入冻结状态
+     * 3. 增加分销员 frozenCommission 和 totalCommission
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void createCommissionAfterPay(CreateCommissionRequest request) {
+        if (request == null) {
+            throw new RuntimeException("分佣参数不能为空");
+        }
+        if (request.getOrderId() == null || request.getBuyerUserId() == null) {
+            throw new RuntimeException("订单ID或购买人ID不能为空");
+        }
+        if (request.getCommissionBaseAmount() == null || request.getCommissionBaseAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        DistributionConfig config = configMapper.selectConfig();
+        if (config == null || config.getEnableStatus() == null || config.getEnableStatus() != 1) {
+            return;
+        }
+
+        // 幂等判断:同一个订单如果已经生成过佣金,则不再重复生成
+        int existCount = commissionRecordMapper.countByOrderId(request.getOrderId());
+        if (existCount > 0) {
+            return;
+        }
+
+        DistributionUserRelation buyerRelation = relationMapper.selectByUserId(request.getBuyerUserId());
+        if (buyerRelation == null || buyerRelation.getStatus() == null || buyerRelation.getStatus() != 1) {
+            return;
+        }
+
+        BigDecimal orderAmount = request.getCommissionBaseAmount();
+
+        // 一级分佣
+        if (buyerRelation.getParentUserId() != null && buyerRelation.getParentUserId() > 0) {
+            createSingleCommission(
+                    request,
+                    buyerRelation.getParentUserId(),
+                    DistributionConstants.COMMISSION_LEVEL_ONE,
+                    config.getLevelOneRate(),
+                    orderAmount
+            );
+        }
+
+        // 二级分佣
+        if (buyerRelation.getGrandParentUserId() != null && buyerRelation.getGrandParentUserId() > 0) {
+            createSingleCommission(
+                    request,
+                    buyerRelation.getGrandParentUserId(),
+                    DistributionConstants.COMMISSION_LEVEL_TWO,
+                    config.getLevelTwoRate(),
+                    orderAmount
+            );
+        }
+    }
+
+
+    /**
+     * 解冻佣金
+     *
+     * 一般用定时任务每天执行:
+     * 订单完成 + 超过售后期后,把冻结佣金转为可提现。
+     *
+     * 这里的简化版是:
+     * 按佣金记录 create_time + freezeDays 判断。
+     *
+     * 更严谨做法:
+     * 结合订单状态,只解冻已完成、未退款的订单佣金。
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unlockFrozenCommission() {
+        DistributionConfig config = configMapper.selectConfig();
+        if (config == null || config.getEnableStatus() == null || config.getEnableStatus() != 1) {
+            return;
+        }
+
+        int freezeDays = config.getFreezeDays() == null ? 7 : config.getFreezeDays();
+
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, -freezeDays);
+        Date beforeTime = calendar.getTime();
+
+        List<DistributionCommissionRecord> records = commissionRecordMapper.selectFrozenRecordsBefore(beforeTime);
+        if (records == null || records.isEmpty()) {
+            return;
+        }
+
+        for (DistributionCommissionRecord record : records) {
+            BigDecimal amount = record.getCommissionAmount();
+
+            int rows = accountMapper.frozenToAvailable(record.getDistributorUserId(), amount);
+            if (rows <= 0) {
+                throw new RuntimeException("佣金解冻失败,recordId=" + record.getId());
+            }
+
+            commissionRecordMapper.updateStatus(record.getId(), DistributionConstants.COMMISSION_AVAILABLE);
+        }
+    }
+
+
+
+    /**
+     * 创建单条佣金记录
+     */
+    private void createSingleCommission(CreateCommissionRequest request,
+                                        Long distributorUserId,
+                                        Integer commissionLevel,
+                                        BigDecimal rate,
+                                        BigDecimal orderAmount) {
+        if (distributorUserId == null || distributorUserId <= 0) {
+            return;
+        }
+        if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        // 防止自己给自己分佣
+        if (distributorUserId.equals(request.getBuyerUserId())) {
+            return;
+        }
+
+        // 确保分销账户存在
+        createAccountIfAbsent(distributorUserId);
+
+        DistributionAccount account = accountMapper.selectByUserId(distributorUserId);
+        if (account == null || account.getStatus() == null || account.getStatus() != 1) {
+            return;
+        }
+
+        BigDecimal commissionAmount = orderAmount
+                .multiply(rate)
+                .divide(new BigDecimal("100"), 2, RoundingMode.DOWN);
+
+        if (commissionAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        DistributionCommissionRecord record = new DistributionCommissionRecord();
+        record.setOrderId(request.getOrderId());
+        record.setOrderNo(request.getOrderNo());
+        record.setBuyerUserId(request.getBuyerUserId());
+        record.setDistributorUserId(distributorUserId);
+        record.setCommissionLevel(commissionLevel);
+        record.setOrderAmount(orderAmount);
+        record.setCommissionRate(rate);
+        record.setCommissionAmount(commissionAmount);
+        record.setStatus(DistributionConstants.COMMISSION_FROZEN);
+
+        commissionRecordMapper.insertRecord(record);
+
+        // 账户金额增加:冻结佣金 + 累计佣金
+        int rows = accountMapper.addFrozenCommission(distributorUserId, commissionAmount);
+        if (rows <= 0) {
+            throw new RuntimeException("增加冻结佣金失败");
+        }
+    }
+
+
+    /**
+     * 如果账户不存在,则创建账户
+     */
+    private void createAccountIfAbsent(Long userId) {
+        DistributionAccount exist = accountMapper.selectByUserId(userId);
+        if (exist != null) {
+            return;
+        }
+
+        DistributionAccount account = new DistributionAccount();
+        account.setUserId(userId);
+        account.setStatus(DistributionConstants.ACCOUNT_NORMAL);
+
+        try {
+            accountMapper.insertAccount(account);
+        } catch (Exception e) {
+            // 并发场景下,可能两个请求同时创建,唯一索引会挡住
+            // 这里再次查询,存在就忽略,不存在再抛异常
+            DistributionAccount after = accountMapper.selectByUserId(userId);
+            if (after == null) {
+                throw e;
+            }
+        }
+    }
+
+
+
+}

+ 11 - 0
fs-service/src/main/java/com/fs/distribution/service/impl/DistributionConfigServiceImpl.java

@@ -0,0 +1,11 @@
+package com.fs.distribution.service.impl;
+
+
+import com.fs.distribution.service.IDistributionConfigService;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class DistributionConfigServiceImpl implements IDistributionConfigService {
+
+}

+ 579 - 0
fs-service/src/main/java/com/fs/distribution/service/impl/DistributionServiceImpl.java

@@ -0,0 +1,579 @@
+package com.fs.distribution.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.distribution.constant.DistributionConstants;
+import com.fs.distribution.domain.*;
+import com.fs.distribution.dto.*;
+import com.fs.distribution.mapper.*;
+import com.fs.distribution.service.DistributionService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 分销核心业务实现
+ */
+@Service
+public class DistributionServiceImpl implements DistributionService {
+
+    @Resource
+    private DistributionUserRelationMapper relationMapper;
+
+    @Resource
+    private DistributionAccountMapper accountMapper;
+
+    @Resource
+    private DistributionCommissionRecordMapper commissionRecordMapper;
+
+    @Resource
+    private DistributionWithdrawRecordMapper withdrawRecordMapper;
+
+    @Resource
+    private DistributionConfigMapper configMapper;
+
+    /**
+     * 注册成功后绑定分销关系
+     *
+     * 核心规则:
+     * 1. 只能绑定一次
+     * 2. 不能绑定自己
+     * 3. 邀请人必须存在
+     * 4. 当前用户绑定 parentUserId
+     * 5. 当前用户的 grandParentUserId = 邀请人的 parentUserId
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void bindRelation(BindRelationRequest request) {
+        if (request == null || request.getUserId() == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+
+        Long userId = request.getUserId();
+        Long inviteUserId = request.getInviteUserId();
+
+        // 先保证当前用户有分销账户
+        createAccountIfAbsent(userId);
+
+        // 如果没有邀请人,也创建一条空关系,方便后续查询
+        if (inviteUserId == null) {
+//            createEmptyRelationIfAbsent(userId);
+            return;
+        }
+
+        // 不能绑定自己
+        if (userId.equals(inviteUserId)) {
+            throw new RuntimeException("不能绑定自己为邀请人");
+        }
+
+        // 当前用户是否已经有关系
+        DistributionUserRelation currentRelation = relationMapper.selectByUserId(userId);
+        if (currentRelation != null && currentRelation.getParentUserId() != null && currentRelation.getParentUserId() > 0) {
+            // 已绑定过上级,不允许重复绑定
+            return;
+        }
+
+        // 查询邀请人的关系
+        DistributionUserRelation inviterRelation = relationMapper.selectByUserId(inviteUserId);
+
+        // 邀请人必须有账户,没有则创建
+        createAccountIfAbsent(inviteUserId);
+
+        Long grandParentUserId = null;
+
+        if (inviterRelation != null && inviterRelation.getParentUserId() != null) {
+            grandParentUserId = inviterRelation.getParentUserId();
+        }
+
+        DistributionUserRelation relation = new DistributionUserRelation();
+        relation.setUserId(userId);
+        relation.setParentUserId(inviteUserId);
+        relation.setGrandParentUserId(grandParentUserId);
+        relation.setBindType(request.getBindType() == null
+                ? DistributionConstants.BIND_TYPE_REGISTER
+                : request.getBindType());
+        relation.setStatus(DistributionConstants.RELATION_NORMAL);
+
+        if (currentRelation == null) {
+            relationMapper.insertRelation(relation);
+        } else {
+            relationMapper.updateRelation(relation);
+        }
+    }
+
+    /**
+     * 订单支付成功后生成佣金
+     *
+     * 注意:
+     * 1. 必须幂等,同一个订单不能重复生成佣金
+     * 2. 佣金先进入冻结状态
+     * 3. 增加分销员 frozenCommission 和 totalCommission
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void createCommissionAfterPay(CreateCommissionRequest request) {
+        if (request == null) {
+            throw new RuntimeException("分佣参数不能为空");
+        }
+        if (request.getOrderId() == null || request.getBuyerUserId() == null) {
+            throw new RuntimeException("订单ID或购买人ID不能为空");
+        }
+        if (request.getCommissionBaseAmount() == null || request.getCommissionBaseAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        DistributionConfig config = configMapper.selectConfig();
+        if (config == null || config.getEnableStatus() == null || config.getEnableStatus() != 1) {
+            return;
+        }
+
+        // 幂等判断:同一个订单如果已经生成过佣金,则不再重复生成
+        int existCount = commissionRecordMapper.countByOrderId(request.getOrderId());
+        if (existCount > 0) {
+            return;
+        }
+
+        DistributionUserRelation buyerRelation = relationMapper.selectByUserId(request.getBuyerUserId());
+        if (buyerRelation == null || buyerRelation.getStatus() == null || buyerRelation.getStatus() != 1) {
+            return;
+        }
+
+        BigDecimal orderAmount = request.getCommissionBaseAmount();
+
+        // 一级分佣
+        if (buyerRelation.getParentUserId() != null && buyerRelation.getParentUserId() > 0) {
+            createSingleCommission(
+                    request,
+                    buyerRelation.getParentUserId(),
+                    DistributionConstants.COMMISSION_LEVEL_ONE,
+                    config.getLevelOneRate(),
+                    orderAmount
+            );
+        }
+
+        // 二级分佣
+        if (buyerRelation.getGrandParentUserId() != null && buyerRelation.getGrandParentUserId() > 0) {
+            createSingleCommission(
+                    request,
+                    buyerRelation.getGrandParentUserId(),
+                    DistributionConstants.COMMISSION_LEVEL_TWO,
+                    config.getLevelTwoRate(),
+                    orderAmount
+            );
+        }
+    }
+
+    /**
+     * 创建单条佣金记录
+     */
+    private void createSingleCommission(CreateCommissionRequest request,
+                                        Long distributorUserId,
+                                        Integer commissionLevel,
+                                        BigDecimal rate,
+                                        BigDecimal orderAmount) {
+        if (distributorUserId == null || distributorUserId <= 0) {
+            return;
+        }
+        if (rate == null || rate.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        // 防止自己给自己分佣
+        if (distributorUserId.equals(request.getBuyerUserId())) {
+            return;
+        }
+
+        // 确保分销账户存在
+        createAccountIfAbsent(distributorUserId);
+
+        DistributionAccount account = accountMapper.selectByUserId(distributorUserId);
+        if (account == null || account.getStatus() == null || account.getStatus() != 1) {
+            return;
+        }
+
+        BigDecimal commissionAmount = orderAmount
+                .multiply(rate)
+                .divide(new BigDecimal("100"), 2, RoundingMode.DOWN);
+
+        if (commissionAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+
+        DistributionCommissionRecord record = new DistributionCommissionRecord();
+        record.setOrderId(request.getOrderId());
+        record.setOrderNo(request.getOrderNo());
+        record.setBuyerUserId(request.getBuyerUserId());
+        record.setDistributorUserId(distributorUserId);
+        record.setCommissionLevel(commissionLevel);
+        record.setOrderAmount(orderAmount);
+        record.setCommissionRate(rate);
+        record.setCommissionAmount(commissionAmount);
+        record.setStatus(DistributionConstants.COMMISSION_FROZEN);
+
+        commissionRecordMapper.insertRecord(record);
+
+        // 账户金额增加:冻结佣金 + 累计佣金
+        int rows = accountMapper.addFrozenCommission(distributorUserId, commissionAmount);
+        if (rows <= 0) {
+            throw new RuntimeException("增加冻结佣金失败");
+        }
+    }
+
+    /**
+     * 解冻佣金
+     *
+     * 一般用定时任务每天执行:
+     * 订单完成 + 超过售后期后,把冻结佣金转为可提现。
+     *
+     * 这里的简化版是:
+     * 按佣金记录 create_time + freezeDays 判断。
+     *
+     * 更严谨做法:
+     * 结合订单状态,只解冻已完成、未退款的订单佣金。
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unlockFrozenCommission() {
+        DistributionConfig config = configMapper.selectConfig();
+        if (config == null || config.getEnableStatus() == null || config.getEnableStatus() != 1) {
+            return;
+        }
+
+        int freezeDays = config.getFreezeDays() == null ? 7 : config.getFreezeDays();
+
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, -freezeDays);
+        Date beforeTime = calendar.getTime();
+
+        List<DistributionCommissionRecord> records = commissionRecordMapper.selectFrozenRecordsBefore(beforeTime);
+        if (records == null || records.isEmpty()) {
+            return;
+        }
+
+        for (DistributionCommissionRecord record : records) {
+            BigDecimal amount = record.getCommissionAmount();
+
+            int rows = accountMapper.frozenToAvailable(record.getDistributorUserId(), amount);
+            if (rows <= 0) {
+                throw new RuntimeException("佣金解冻失败,recordId=" + record.getId());
+            }
+
+            commissionRecordMapper.updateStatus(record.getId(), DistributionConstants.COMMISSION_AVAILABLE);
+        }
+    }
+
+    /**
+     * 订单退款时佣金失效
+     *
+     * 规则:
+     * 1. 冻结中:扣冻结和累计
+     * 2. 可提现:扣可提现和累计
+     * 3. 已提现:目前不直接扣
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void invalidCommissionByRefund(Long orderId) {
+        if (orderId == null) {
+            throw new RuntimeException("订单ID不能为空");
+        }
+
+        List<DistributionCommissionRecord> records = commissionRecordMapper.selectByOrderId(orderId);
+        if (records == null || records.isEmpty()) {
+            return;
+        }
+
+        for (DistributionCommissionRecord record : records) {
+            if (DistributionConstants.COMMISSION_INVALID.equals(record.getStatus())) {
+                continue;
+            }
+
+            BigDecimal amount = record.getCommissionAmount();
+
+            if (DistributionConstants.COMMISSION_FROZEN.equals(record.getStatus())) {
+                int rows = accountMapper.deductFrozenAndTotal(record.getDistributorUserId(), amount);
+                if (rows <= 0) {
+                    throw new RuntimeException("扣减冻结佣金失败,recordId=" + record.getId());
+                }
+            } else if (DistributionConstants.COMMISSION_AVAILABLE.equals(record.getStatus())) {
+                int rows = accountMapper.deductAvailableAndTotal(record.getDistributorUserId(), amount);
+                if (rows <= 0) {
+                    throw new RuntimeException("扣减可提现佣金失败,recordId=" + record.getId());
+                }
+            } else if (DistributionConstants.COMMISSION_WITHDRAWN.equals(record.getStatus())) {
+                // 已提现的佣金,后续做一条负佣金记录,从未来佣金里抵扣
+            }
+
+            commissionRecordMapper.updateStatus(record.getId(), DistributionConstants.COMMISSION_INVALID);
+        }
+    }
+
+    /**
+     * 用户申请提现
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void applyWithdraw(WithdrawApplyRequest request) {
+        if (request == null || request.getUserId() == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+        if (request.getWithdrawAmount() == null
+                || request.getWithdrawAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new RuntimeException("提现金额必须大于0");
+        }
+
+        DistributionConfig config = configMapper.selectConfig();
+        if (config == null || config.getEnableStatus() == null || config.getEnableStatus() != 1) {
+            throw new RuntimeException("分销功能未开启");
+        }
+
+        BigDecimal minWithdrawAmount = config.getMinWithdrawAmount() == null
+                ? BigDecimal.ZERO
+                : config.getMinWithdrawAmount();
+
+        if (request.getWithdrawAmount().compareTo(minWithdrawAmount) < 0) {
+            throw new RuntimeException("提现金额不能低于最低提现金额:" + minWithdrawAmount);
+        }
+
+        // 加锁查询账户,防止并发提现
+        DistributionAccount account = accountMapper.selectByUserIdForUpdate(request.getUserId());
+        if (account == null) {
+            throw new RuntimeException("分销账户不存在");
+        }
+        if (account.getStatus() == null || account.getStatus() != 1) {
+            throw new RuntimeException("分销账户已禁用");
+        }
+
+        if (account.getAvailableCommission().compareTo(request.getWithdrawAmount()) < 0) {
+            throw new RuntimeException("可提现余额不足");
+        }
+
+        // 可提现转提现中
+        int rows = accountMapper.availableToWithdrawing(request.getUserId(), request.getWithdrawAmount());
+        if (rows <= 0) {
+            throw new RuntimeException("申请提现失败");
+        }
+
+        DistributionWithdrawRecord record = new DistributionWithdrawRecord();
+        record.setUserId(request.getUserId());
+        record.setWithdrawNo(generateWithdrawNo());
+        record.setWithdrawAmount(request.getWithdrawAmount());
+        record.setStatus(DistributionConstants.WITHDRAW_WAIT_AUDIT);
+        record.setRemark("用户申请提现");
+
+        withdrawRecordMapper.insertWithdrawRecord(record);
+    }
+
+    /**
+     * 后台审核提现
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void auditWithdraw(WithdrawAuditRequest request) {
+        if (request == null || request.getWithdrawId() == null) {
+            throw new RuntimeException("提现ID不能为空");
+        }
+
+        if (!DistributionConstants.WITHDRAW_PAID.equals(request.getStatus())
+                && !DistributionConstants.WITHDRAW_REJECT.equals(request.getStatus())) {
+            throw new RuntimeException("审核状态不正确");
+        }
+
+        DistributionWithdrawRecord record = withdrawRecordMapper.selectByIdForUpdate(request.getWithdrawId());
+        if (record == null) {
+            throw new RuntimeException("提现记录不存在");
+        }
+
+        if (!DistributionConstants.WITHDRAW_WAIT_AUDIT.equals(record.getStatus())) {
+            throw new RuntimeException("该提现记录已审核,不能重复处理");
+        }
+
+        if (DistributionConstants.WITHDRAW_PAID.equals(request.getStatus())) {
+            // 审核通过:提现中转已提现
+            int rows = accountMapper.withdrawingToWithdrawn(record.getUserId(), record.getWithdrawAmount());
+            if (rows <= 0) {
+                throw new RuntimeException("提现审核通过处理失败");
+            }
+        } else {
+            // 审核拒绝:提现中退回可提现
+            int rows = accountMapper.withdrawingBackToAvailable(record.getUserId(), record.getWithdrawAmount());
+            if (rows <= 0) {
+                throw new RuntimeException("提现审核拒绝处理失败");
+            }
+        }
+
+        DistributionWithdrawRecord update = new DistributionWithdrawRecord();
+        update.setId(record.getId());
+        update.setStatus(request.getStatus());
+        update.setRemark(request.getRemark());
+
+        withdrawRecordMapper.updateAuditStatus(update);
+    }
+
+    @Override
+    public DistributionAccount getAccount(Long userId) {
+        if (userId == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+        createAccountIfAbsent(userId);
+        return accountMapper.selectByUserId(userId);
+    }
+
+    @Override
+    public DistributionUserRelation getRelation(Long userId) {
+        if (userId == null) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+        return relationMapper.selectByUserId(userId);
+    }
+
+    @Override
+    public List<DistributionCommissionRecord> getCommissionList(Long userId) {
+        return commissionRecordMapper.selectByUserId(userId);
+    }
+
+    @Override
+    public List<DistributionWithdrawRecord> getWithdrawList(Long userId) {
+        return withdrawRecordMapper.selectByUserId(userId);
+    }
+
+    /**
+     * 如果账户不存在,则创建账户
+     */
+    private void createAccountIfAbsent(Long userId) {
+        DistributionAccount exist = accountMapper.selectByUserId(userId);
+        if (exist != null) {
+            return;
+        }
+
+        DistributionAccount account = new DistributionAccount();
+        account.setUserId(userId);
+        account.setStatus(DistributionConstants.ACCOUNT_NORMAL);
+
+        try {
+            accountMapper.insertAccount(account);
+        } catch (Exception e) {
+            // 并发场景下,可能两个请求同时创建,唯一索引会挡住
+            // 这里再次查询,存在就忽略,不存在再抛异常
+            DistributionAccount after = accountMapper.selectByUserId(userId);
+            if (after == null) {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * 没有邀请人的用户,也创建一条空关系
+     */
+    private void createEmptyRelationIfAbsent(Long userId) {
+        DistributionUserRelation exist = relationMapper.selectByUserId(userId);
+        if (exist != null) {
+            return;
+        }
+
+        DistributionUserRelation relation = new DistributionUserRelation();
+        relation.setUserId(userId);
+        relation.setParentUserId(0L);
+        relation.setGrandParentUserId(0L);
+        relation.setBindType(DistributionConstants.BIND_TYPE_REGISTER);
+        relation.setStatus(DistributionConstants.RELATION_NORMAL);
+
+        try {
+            relationMapper.insertRelation(relation);
+        } catch (Exception e) {
+            DistributionUserRelation after = relationMapper.selectByUserId(userId);
+            if (after == null) {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * 生成提现单号
+     */
+    private String generateWithdrawNo() {
+        return "TX" + System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
+    }
+
+
+    @Override
+    public DistributionConfig getConfig() {
+        DistributionConfig config = configMapper.selectConfig();
+
+        if (config == null) {
+            // 理论上初始化 SQL 已经插入了配置
+            // 这里兜底返回默认值
+            config = new DistributionConfig();
+            config.setEnableStatus(1);
+            config.setLevelOneRate(new BigDecimal("10.00"));
+            config.setLevelTwoRate(new BigDecimal("5.00"));
+            config.setFreezeDays(7);
+            config.setMinWithdrawAmount(new BigDecimal("10.00"));
+        }
+
+        return config;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveConfig(DistributionConfig config) {
+        if (config == null) {
+            throw new RuntimeException("配置不能为空");
+        }
+
+        if (config.getEnableStatus() == null) {
+            throw new RuntimeException("请选择分销开关");
+        }
+
+        if (config.getLevelOneRate() == null || config.getLevelOneRate().compareTo(BigDecimal.ZERO) < 0) {
+            throw new RuntimeException("一级佣金比例不能小于0");
+        }
+
+        if (config.getLevelTwoRate() == null || config.getLevelTwoRate().compareTo(BigDecimal.ZERO) < 0) {
+            throw new RuntimeException("二级佣金比例不能小于0");
+        }
+
+        if (config.getLevelOneRate().compareTo(new BigDecimal("100")) > 0
+                || config.getLevelTwoRate().compareTo(new BigDecimal("100")) > 0) {
+            throw new RuntimeException("佣金比例不能大于100%");
+        }
+
+        if (config.getFreezeDays() == null || config.getFreezeDays() < 0) {
+            throw new RuntimeException("冻结天数不能小于0");
+        }
+
+        if (config.getMinWithdrawAmount() == null || config.getMinWithdrawAmount().compareTo(BigDecimal.ZERO) < 0) {
+            throw new RuntimeException("最低提现金额不能小于0");
+        }
+
+        DistributionConfig exist = configMapper.selectConfig();
+
+        if (exist == null) {
+            // 如果你没有 insertConfig 方法,可以直接先用 SQL 初始化一条配置
+            throw new RuntimeException("分销配置不存在,请先初始化 distribution_config 表");
+        }
+
+        config.setId(exist.getId());
+
+        int rows = configMapper.updateConfig(config);
+        if (rows <= 0) {
+            throw new RuntimeException("保存分销配置失败");
+        }
+    }
+
+    @Override
+    public QueryDistributionAccountResponse getAccountList(QueryDistributionAccountRequest queryDistributionAccountRequest) {
+        QueryDistributionAccountResponse queryDistributionAccountResponse = new QueryDistributionAccountResponse();
+        List<DistributionAccount> distributionAccounts = accountMapper.selectList(new LambdaQueryWrapper<DistributionAccount>());
+        if (ObjectUtil.isNotEmpty(distributionAccounts)){
+            queryDistributionAccountResponse.setList(distributionAccounts);
+        }
+        return queryDistributionAccountResponse;
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/distribution/service/impl/DistributionUserRelationServiceImpl.java

@@ -0,0 +1,11 @@
+package com.fs.distribution.service.impl;
+
+
+import com.fs.distribution.service.IDistributionUserRelationService;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class DistributionUserRelationServiceImpl implements IDistributionUserRelationService {
+
+}

+ 21 - 0
fs-service/src/main/java/com/fs/distribution/service/impl/DistributionWithdrawRecordServiceImpl.java

@@ -0,0 +1,21 @@
+package com.fs.distribution.service.impl;
+
+import com.fs.distribution.domain.DistributionWithdrawRecord;
+import com.fs.distribution.mapper.DistributionWithdrawRecordMapper;
+import com.fs.distribution.service.IDistributionWithdrawRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class DistributionWithdrawRecordServiceImpl implements IDistributionWithdrawRecordService {
+
+    @Autowired
+    private DistributionWithdrawRecordMapper distributionWithdrawMapper;
+
+    @Override
+    public List<DistributionWithdrawRecord> selectWithdrawList(DistributionWithdrawRecord query) {
+        return distributionWithdrawMapper.selectWithdrawList(query);
+    }
+}

+ 3 - 1
fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java

@@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
+import com.fs.course.config.CourseConfig;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.PayOrderParam;
@@ -120,6 +121,7 @@ public interface IFsStorePaymentService
     String v3TransferNotifyApp(String notifyData, HttpServletRequest request);
 
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
+    String v3TransferNotifyWithCompanyIdApp(Long companyId,String notifyData, HttpServletRequest request);
 
 
 
@@ -144,5 +146,5 @@ public interface IFsStorePaymentService
 
     List<FsStorePayment> selectAllPayment();
 
-    R sendAppRedPacket(WxSendRedPacketParam packetParam);
+    R sendAppRedPacket(WxSendRedPacketParam packetParam,CourseConfig config);
 }

+ 174 - 87
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -46,6 +46,7 @@ import com.fs.company.vo.FsStorePaymentStatisticsVO;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RedPacketConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
@@ -429,6 +430,12 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
                             vipOrderService.payConfirm("", fsStorePayment.getPayCode(), fsStorePayment.getTradeNo(), "", 1,queryOrderResult.getOut_trans_id(),queryOrderResult.getParty_order_id());
                         }
 
+                }else if (queryOrderResult.getTrans_stat().equals("F") && queryOrderResult.getBank_code().equals("TRADE_CLOSED")) {
+                    FsStorePayment paymentMap = new FsStorePayment();
+                    paymentMap.setPaymentId(paymentId);
+                    paymentMap.setStatus(2);
+                    paymentMap.setPayTime(new Date());
+                    fsStorePaymentMapper.updateFsStorePayment(paymentMap);
                 }
             }
         }
@@ -1224,105 +1231,166 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
     @Override
     public String v3TransferNotify(String notifyData, HttpServletRequest request) {
         logger.info("zyp \n【收到转账回调V3】:{}",notifyData);
-        try {
-            String json = configService.selectConfigByKey("redPacket.config");
-            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
-            //创建微信订单
-            WxPayConfig payConfig = new WxPayConfig();
-            BeanUtils.copyProperties(config,payConfig);
-            WxPayService wxPayService = new WxPayServiceImpl();
-            wxPayService.setConfig(payConfig);
-            SignatureHeader signatureHeader = new SignatureHeader();
-            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
-            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
-            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
-            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("到零钱回调1:{}",result.getResult());
-            if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
-                logger.info("result:{}",r);
-                if (r.get("code").equals(200)){
-                    return WxPayNotifyResponse.success("处理成功");
-                }else {
-                    return WxPayNotifyResponse.fail("");
-                }
-            }else {
-                return WxPayNotifyResponse.fail("");
-            }
-        } catch (WxPayException e) {
-            e.printStackTrace();
-            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
-            return WxPayNotifyResponse.fail(e.getMessage());
-        }
+        String json = configService.selectConfigByKey("redPacket.config");
+        return handleTransferV3Notify(json,notifyData,request);
+//        try {
+//            String json = configService.selectConfigByKey("redPacket.config");
+//            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+//            //创建微信订单
+//            WxPayConfig payConfig = new WxPayConfig();
+//            BeanUtils.copyProperties(config,payConfig);
+//            WxPayService wxPayService = new WxPayServiceImpl();
+//            wxPayService.setConfig(payConfig);
+//            SignatureHeader signatureHeader = new SignatureHeader();
+//            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+//            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+//            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+//            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+//            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+//            logger.info("到零钱回调1:{}",result.getResult());
+//            if (result.getResult().getState().equals("SUCCESS")) {
+//                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+//                logger.info("result:{}",r);
+//                if (r.get("code").equals(200)){
+//                    return WxPayNotifyResponse.success("处理成功");
+//                }else {
+//                    return WxPayNotifyResponse.fail("");
+//                }
+//            }else {
+//                return WxPayNotifyResponse.fail("");
+//            }
+//        } catch (WxPayException e) {
+//            e.printStackTrace();
+//            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
+//            return WxPayNotifyResponse.fail(e.getMessage());
+//        }
     }
 
     @Override
     public String v3TransferNotifyApp(String notifyData, HttpServletRequest request) {
         logger.info("zyp \n【app-收到转账回调V3】:{}",notifyData);
-        try {
-            String json = configService.selectConfigByKey("his.AppRedPacket");
-            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
-            //创建微信订单
-            WxPayConfig payConfig = new WxPayConfig();
-            BeanUtils.copyProperties(config,payConfig);
-            WxPayService wxPayService = new WxPayServiceImpl();
-            wxPayService.setConfig(payConfig);
-            SignatureHeader signatureHeader = new SignatureHeader();
-            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
-            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
-            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
-            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("app-到零钱回调:{}",result.getResult());
-            if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
-                logger.info("app,result:{}",r);
-                if (r.get("code").equals(200)){
-                    return WxPayNotifyResponse.success("处理成功");
-                }else {
-                    return WxPayNotifyResponse.fail("");
-                }
-            }else {
-                return WxPayNotifyResponse.fail("");
-            }
-        } catch (WxPayException e) {
-            e.printStackTrace();
-            logger.error("zyp \n【app-转账回调异常】:{}", e.getReturnMsg());
-            return WxPayNotifyResponse.fail(e.getMessage());
-        }
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+        return handleTransferV3Notify(json,notifyData,request);
+//        try {
+//            String json = configService.selectConfigByKey("his.AppRedPacket");
+//            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+//            //创建微信订单
+//            WxPayConfig payConfig = new WxPayConfig();
+//            BeanUtils.copyProperties(config,payConfig);
+//            WxPayService wxPayService = new WxPayServiceImpl();
+//            wxPayService.setConfig(payConfig);
+//            SignatureHeader signatureHeader = new SignatureHeader();
+//            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+//            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+//            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+//            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+//            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+//            logger.info("app-到零钱回调:{}",result.getResult());
+//            if (result.getResult().getState().equals("SUCCESS")) {
+//                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+//                logger.info("app,result:{}",r);
+//                if (r.get("code").equals(200)){
+//                    return WxPayNotifyResponse.success("处理成功");
+//                }else {
+//                    return WxPayNotifyResponse.fail("");
+//                }
+//            }else {
+//                return WxPayNotifyResponse.fail("");
+//            }
+//        } catch (WxPayException e) {
+//            e.printStackTrace();
+//            logger.error("zyp \n【app-转账回调异常】:{}", e.getReturnMsg());
+//            return WxPayNotifyResponse.fail(e.getMessage());
+//        }
     }
 
     @Override
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
         logger.info("分公司回调V3::companyId:{}",companyId);
         logger.info("zyp \n【收到转账回调V3::分公司】:{}",notifyData);
+        String json = companyConfigService.selectRedPacketConfigByKey(companyId);
+        return handleTransferV3Notify(json,notifyData,request);
+//        try {
+////            String json = configService.selectConfigByKey("redPacket.config");
+//            String json = companyConfigService.selectRedPacketConfigByKey(companyId);
+//            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+//
+//            //创建微信订单
+//            WxPayConfig payConfig = new WxPayConfig();
+//            BeanUtils.copyProperties(config,payConfig);
+//            WxPayService wxPayService = new WxPayServiceImpl();
+//            wxPayService.setConfig(payConfig);
+//            SignatureHeader signatureHeader = new SignatureHeader();
+//            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+//            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+//            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+//            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+//            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+//            logger.info("到零钱回调1:{}",result.getResult());
+//            if (result.getResult().getState().equals("SUCCESS")) {
+//                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+//                logger.info("result:{}",r);
+//                if (r.get("code").equals(200)){
+//                    return WxPayNotifyResponse.success("处理成功");
+//                }else {
+//                    return WxPayNotifyResponse.fail("");
+//                }
+//            }else {
+//                return WxPayNotifyResponse.fail("");
+//            }
+//        } catch (WxPayException e) {
+//            e.printStackTrace();
+//            logger.error("zyp \n【转账回调异常】:{}", e.getReturnMsg());
+//            return WxPayNotifyResponse.fail(e.getMessage());
+//        }
+    }
+
+    @Override
+    public String v3TransferNotifyWithCompanyIdApp(Long companyId, String notifyData, HttpServletRequest request) {
+        logger.info("分公司回调V3app::app的companyId:{}",companyId);
+        logger.info("zyp \n【app收到转账回调V3::分公司】:{}",notifyData);
+
+        String json = companyConfigService.selectRedPacketConfigByKeyApp(companyId);
+
+        return handleTransferV3Notify(json,notifyData,request);
+
+    }
+
+
+    private String handleTransferV3Notify(String json, String notifyData, HttpServletRequest request) {
+        logger.info("zyp \n【收到转账回调V3】:{}", notifyData);
         try {
-//            String json = configService.selectConfigByKey("redPacket.config");
-            String json = companyConfigService.selectRedPacketConfigByKey(companyId);
+
             RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
 
             //创建微信订单
             WxPayConfig payConfig = new WxPayConfig();
-            BeanUtils.copyProperties(config,payConfig);
+            BeanUtils.copyProperties(config, payConfig);
             WxPayService wxPayService = new WxPayServiceImpl();
             wxPayService.setConfig(payConfig);
+
             SignatureHeader signatureHeader = new SignatureHeader();
             signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
             signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
             signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
             signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
-            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
-            logger.info("到零钱回调1:{}",result.getResult());
+
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData, signatureHeader);
+            logger.info("到零钱回调1:{}", result.getResult());
+
             if (result.getResult().getState().equals("SUCCESS")) {
-                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
-                logger.info("result:{}",r);
-                if (r.get("code").equals(200)){
+                R r = redPacketLogService.syncRedPacket(
+                        result.getResult().getOutBillNo(),
+                        result.getResult().getTransferBillNo()
+                );
+                logger.info("result:{}", r);
+
+                if (r.get("code").equals(200)) {
                     return WxPayNotifyResponse.success("处理成功");
-                }else {
+                } else {
                     return WxPayNotifyResponse.fail("");
                 }
-            }else {
+            } else {
                 return WxPayNotifyResponse.fail("");
             }
         } catch (WxPayException e) {
@@ -1409,12 +1477,12 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
 
         FsPayConfig payConfig=new FsPayConfig();
-        //支付宝可以不需要appid(在没有appid的情况下)【ps:小程序的支付宝没传appid 就G】
-       if (PaymentMethodEnum.ALIPAY==payOrderParam.getPaymentMethod() && StringUtils.isBlank(payOrderParam.getAppId())){
-           String json = configService.selectConfigByKey("his.pay");
-           PayConfigDTO payConfigDTO = JSONUtil.toBean(json, PayConfigDTO.class);
-           payConfig.setType(payConfigDTO.getType());
-       }else {
+//        //支付宝可以不需要appid(在没有appid的情况下)【ps:小程序的支付宝没传appid 就G】
+//       if (PaymentMethodEnum.ALIPAY==payOrderParam.getPaymentMethod() && StringUtils.isBlank(payOrderParam.getAppId())){
+//           String json = configService.selectConfigByKey("his.pay");
+//           PayConfigDTO payConfigDTO = JSONUtil.toBean(json, PayConfigDTO.class);
+//           payConfig.setType(payConfigDTO.getType());
+//       }else {
            if (StringUtils.isBlank(payOrderParam.getAppId())) {
                throw new IllegalArgumentException("appId不能为空");
            }
@@ -1433,7 +1501,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
            payConfig.setAppId(fsCoursePlaySourceConfig.getAppid());
 
            logger.debug("支付配置 his.pay: {}", payConfig);
-       }
+//       }
 
 
 
@@ -2066,19 +2134,38 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 
     @Override
     @Transactional
-    public R sendAppRedPacket(WxSendRedPacketParam param) {
+    public R sendAppRedPacket(WxSendRedPacketParam param, CourseConfig config) {
         //组合返回参数
         R result = new R();
-        String json = configService.selectConfigByKey("his.AppRedPacket");
-        AppRedPacketConfig config = JSONUtil.toBean(json, AppRedPacketConfig.class);
-        if (config.getIsNew() != null && config.getIsNew() == 1) {
-            result = sendRedPacketV3(param, config);
+
+        String json;
+        // 根据红包模式获取配置
+        switch (config.getRedPacketMode()){
+            case 1:
+                json = configService.selectConfigByKey("his.AppRedPacket");
+                break;
+            case 2:
+                 json = companyConfigService.selectRedPacketConfigByKeyApp(param.getCompanyId());
+                //如果分公司配置为空就走总后台的配置
+                if (StringUtils.isEmpty(json)){
+//                    json = configService.selectConfigByKey("his.AppRedPacket");
+                    throw new UnsupportedOperationException("销售公司红包配置为空");
+                }
+                break;
+            default:
+                throw new UnsupportedOperationException("当前红包模式不支持!");
+        }
+
+//        String json = configService.selectConfigByKey("his.AppRedPacket");
+        AppRedPacketConfig appRedConfig = JSONUtil.toBean(json, AppRedPacketConfig.class);
+        if (appRedConfig.getIsNew() != null && appRedConfig.getIsNew() == 1) {
+            result = sendRedPacketV3(param, appRedConfig);
         } else {
-            result= sendRedPacketLegacy(param, config);
+            result= sendRedPacketLegacy(param, appRedConfig);
         }
 
-        result.put("mchId", config.getMchId());
-        result.put("isNew",config.getIsNew());
+        result.put("mchId", appRedConfig.getMchId());
+        result.put("isNew",appRedConfig.getIsNew());
         logger.info("App提现返回:{}",result);
         return result;
     }

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCouponIssueScrm.java

@@ -5,6 +5,7 @@ import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 
 /**
  * 优惠券领取对象 fs_store_coupon_issue
@@ -12,6 +13,7 @@ import com.fs.common.core.domain.BaseEntity;
  * @author fs
  * @date 2022-03-15
  */
+@Data
 public class FsStoreCouponIssueScrm extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
@@ -56,6 +58,8 @@ public class FsStoreCouponIssueScrm extends BaseEntity
     @Excel(name = "1 正常 0 未开启 -1 已无效")
     private Integer status;
 
+    private Long userId;
+
     /** $column.columnComment */
     @Excel(name = "1 正常 0 未开启 -1 已无效")
     private Integer isDel;

+ 19 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCouponScrm.java

@@ -3,6 +3,7 @@ package com.fs.hisStore.domain;
 import java.math.BigDecimal;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
@@ -12,6 +13,7 @@ import org.apache.commons.lang3.builder.ToStringStyle;
  * @author fs
  * @date 2022-03-15
  */
+@Data
 public class FsStoreCouponScrm extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
@@ -62,6 +64,13 @@ public class FsStoreCouponScrm extends BaseEntity
     @Excel(name = "是否删除")
     private Integer isDel;
 
+    /** 普通券种类:0无门槛 1指定商品 2满减 */
+    @Excel(name = "普通券种类", readConverterExp = "0=无门槛,1=指定商品,2=满减")
+    private Integer couponSubType;
+
+    /** 优惠券使用说明 */
+    private String remark;
+
     public String getPackageCateIds() {
         return packageCateIds;
     }
@@ -170,6 +179,14 @@ public class FsStoreCouponScrm extends BaseEntity
         return isDel;
     }
 
+    public Integer getCouponSubType() {
+        return couponSubType;
+    }
+
+    public void setCouponSubType(Integer couponSubType) {
+        this.couponSubType = couponSubType;
+    }
+
     @Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -186,6 +203,8 @@ public class FsStoreCouponScrm extends BaseEntity
             .append("createTime", getCreateTime())
             .append("updateTime", getUpdateTime())
             .append("isDel", getIsDel())
+            .append("couponSubType", getCouponSubType())
+            .append("remark", getRemark())
             .toString();
     }
 }

+ 7 - 108
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreCouponUserScrm.java

@@ -5,6 +5,7 @@ import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 
 /**
  * 优惠券发放记录对象 fs_store_coupon_user
@@ -12,6 +13,7 @@ import com.fs.common.core.domain.BaseEntity;
  * @author fs
  * @date 2022-03-15
  */
+@Data
 public class FsStoreCouponUserScrm extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
@@ -65,114 +67,11 @@ public class FsStoreCouponUserScrm extends BaseEntity
     @Excel(name = "是否有效")
     private Integer isDel;
 
-    public void setId(Long id) 
-    {
-        this.id = id;
-    }
-
-    public Long getId() 
-    {
-        return id;
-    }
-    public void setCouponId(Long couponId) 
-    {
-        this.couponId = couponId;
-    }
-
-    public Long getCouponId() 
-    {
-        return couponId;
-    }
-    public void setUserId(Long userId) 
-    {
-        this.userId = userId;
-    }
-
-    public Long getUserId() 
-    {
-        return userId;
-    }
-    public void setCouponTitle(String couponTitle) 
-    {
-        this.couponTitle = couponTitle;
-    }
-
-    public String getCouponTitle() 
-    {
-        return couponTitle;
-    }
-    public void setCouponPrice(BigDecimal couponPrice) 
-    {
-        this.couponPrice = couponPrice;
-    }
-
-    public BigDecimal getCouponPrice() 
-    {
-        return couponPrice;
-    }
-    public void setUseMinPrice(BigDecimal useMinPrice) 
-    {
-        this.useMinPrice = useMinPrice;
-    }
-
-    public BigDecimal getUseMinPrice() 
-    {
-        return useMinPrice;
-    }
-
-    public Date getLimitTime() {
-        return limitTime;
-    }
-
-    public void setLimitTime(Date limitTime) {
-        this.limitTime = limitTime;
-    }
-
-    public void setUseTime(Date useTime)
-    {
-        this.useTime = useTime;
-    }
-
-    public Date getUseTime() 
-    {
-        return useTime;
-    }
-    public void setType(String type) 
-    {
-        this.type = type;
-    }
-
-    public String getType() 
-    {
-        return type;
-    }
-    public void setStatus(Integer status) 
-    {
-        this.status = status;
-    }
-
-    public Integer getStatus() 
-    {
-        return status;
-    }
-    public void setIsFail(Integer isFail) 
-    {
-        this.isFail = isFail;
-    }
-
-    public Integer getIsFail() 
-    {
-        return isFail;
-    }
-    public void setIsDel(Integer isDel) 
-    {
-        this.isDel = isDel;
-    }
-
-    public Integer getIsDel() 
-    {
-        return isDel;
-    }
+    /**
+     * 优惠券到期时间
+     * @param id
+     */
+    private Date expireTime;
 
 
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java

@@ -396,6 +396,9 @@ public class FsStoreOrderScrm extends BaseEntity
     //关联id根据订单类型+associatedId唯一数据
     private Long associatedId;
 
+    /** 团购ID(fs_store_product_group_buy.id,限时团购订单用于关联拼团) */
+    private Long groupBuyId;
+
     //是否同步库存 0-否 1-是
     private Integer isSyncInventory;
 

+ 16 - 1
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductActivity.java

@@ -33,7 +33,7 @@ public class FsStoreProductActivity implements Serializable
     @Excel(name = "活动类型", readConverterExp = "6=秒杀,7=限时折扣")
     private Integer activityType;
 
-    /** 规格ID(fs_store_product_attr_value.id) */
+    /** 规格ID(fs_store_product_attr_value_scrm.id) */
     @Excel(name = "规格ID")
     private Long specId;
 
@@ -53,6 +53,21 @@ public class FsStoreProductActivity implements Serializable
     @Excel(name = "折扣价")
     private BigDecimal discountPrice;
 
+    /**
+     * 团购人数(仅 activity_type=8 限时团购有效)
+     * <p>范围:2 ≤ groupNum ≤ 5,默认 5。
+     * 当同一团中“参团并已付款”的人数达到该值时,将自动成团并触发发货(推送 ERP)。</p>
+     */
+    @Excel(name = "团购人数")
+    private Integer groupNum;
+
+    /**
+     * 团购价(仅 activity_type=8 限时团购有效,必填)
+     * <p>成团后用户结算的实际价格,必须大于 0 且小于原价(originalPrice)。</p>
+     */
+    @Excel(name = "团购价")
+    private BigDecimal groupPrice;
+
     /** 开始时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")

+ 82 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductGroupBuy.java

@@ -0,0 +1,82 @@
+package com.fs.hisStore.domain;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+/**
+ * 商品限时团购主表对象 fs_store_product_group_buy
+ * 对应 activity_type=8(限时团购)的团记录,一个团一条
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Data
+public class FsStoreProductGroupBuy implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 团号(唯一,便于分享/查询) */
+    @Excel(name = "团号")
+    private String groupNo;
+
+    /** 活动ID(fs_store_product_activity.id) */
+    private Long activityId;
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 满团人数(冗余 fs_store_product_activity.group_num) */
+    @Excel(name = "满团人数")
+    private Integer groupNum;
+
+    /** 当前已参团人数(下单预占后+1) */
+    @Excel(name = "已参团人数")
+    private Integer joinNum;
+
+    /** 当前已付款人数(支付回调成功后+1),是成团判定的唯一依据 */
+    @Excel(name = "已付款人数")
+    private Integer paidNum;
+
+    /** 团状态:0=进行中 1=拼团完成 2=拼团失败(超时未满员) */
+    @Excel(name = "团状态", readConverterExp = "0=进行中,1=拼团完成,2=拼团失败")
+    private Integer status;
+
+    /** 开团时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 截团时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 拼团完成时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date completeTime;
+
+    /** 删除标志:0=正常 1=删除 */
+    private Integer delFlag;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+}

+ 74 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductGroupBuyItem.java

@@ -0,0 +1,74 @@
+package com.fs.hisStore.domain;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+/**
+ * 商品限时团购参与详情对象 fs_store_product_group_buy_item
+ * 每位成功参团用户一条记录,uk_group_user(group_id,user_id) 保证不重复
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Data
+public class FsStoreProductGroupBuyItem implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 团购主表ID */
+    private Long groupId;
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 商品规格ID */
+    private Long specId;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 用户昵称 */
+    @Excel(name = "用户昵称")
+    private String nickName;
+
+    /** 用户头像 */
+    private String avatar;
+
+    /** 支付状态:0=未支付 1=已支付 2=已退款 */
+    private Integer payStatus;
+
+    /** 支付时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    /** 参团时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date joinTime;
+
+    /** 删除标志:0=正常 1=删除 */
+    private Integer delFlag;
+
+    /** 创建者 */
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新者 */
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 36 - 27
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueScrmMapper.java

@@ -108,31 +108,40 @@ public interface FsStoreCouponIssueScrmMapper
             "where i.id=#{id}  order by c.coupon_price desc " +
             "</script>"})
     FsStoreCouponIssueVO selectFsStoreCouponIssueVOById(Long id);
-    @Select({"<script> " +
-            "select i.*,c.coupon_price,c.use_min_price,c.coupon_time  from fs_store_coupon_issue_scrm i left join fs_store_coupon_scrm c on c.coupon_id=i.coupon_id  " +
-            "where i.limit_time &gt; now() " +
-            "<if test = 'maps.isDel != null     '> " +
-            "and i.is_del =#{maps.isDel} " +
-            "</if>" +
-            "<if test = 'maps.status != null     '> " +
-            "and i.status =#{maps.status} " +
-            "</if>" +
-            "<if test = 'maps.couponType != null     '> " +
-            "and i.coupon_type =#{maps.couponType} " +
-            "</if>" +
-            "<if test = 'maps.couponPrice != null     '> " +
-            "and c.coupon_price =#{maps.couponPrice} " +
-            "</if>" +
-            "<if test = 'maps.couponName != null and  maps.couponName !=\"\"     '> " +
-            "and i.coupon_name like CONCAT('%',#{maps.couponName},'%') " +
-            "</if>" +
-            "<if test = 'maps.beginTime != null and maps.beginTime != \"\" '> " +
-            "and date_format(i.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
-            "</if>" +
-            "<if test = 'maps.endTime != null and maps.endTime != \"\" '> " +
-            "and date_format(i.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
-            "</if>" +
-            " order by c.coupon_price "+
-            "</script>"})
-    List<FsStoreCouponIssueVO> selectFsStoreCouponIssueListVO(@Param("maps") FsStoreCouponIssueScrm fsStoreCouponIssue);
+//    @Select({"<script> " +
+//            "select i.*,c.coupon_price,c.use_min_price,c.coupon_time,c.product_ids  from fs_store_coupon_issue_scrm i left join fs_store_coupon_scrm c on c.coupon_id=i.coupon_id  " +
+//            "where i.limit_time &gt; now() " +
+//            "<if test = 'maps.isDel != null     '> " +
+//            "and i.is_del =#{maps.isDel} " +
+//            "</if>" +
+//            "<if test = 'maps.status != null     '> " +
+//            "and i.status =#{maps.status} " +
+//            "</if>" +
+//            "<if test = 'maps.couponType != null     '> " +
+//            "and i.coupon_type =#{maps.couponType} " +
+//            "</if>" +
+//            "<if test = 'maps.couponPrice != null     '> " +
+//            "and c.coupon_price =#{maps.couponPrice} " +
+//            "</if>" +
+//            "<if test = 'maps.couponName != null and  maps.couponName !=\"\"     '> " +
+//            "and i.coupon_name like CONCAT('%',#{maps.couponName},'%') " +
+//            "</if>" +
+//            "<if test = 'maps.beginTime != null and maps.beginTime != \"\" '> " +
+//            "and date_format(i.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
+//            "</if>" +
+//            "<if test = 'maps.endTime != null and maps.endTime != \"\" '> " +
+//            "and date_format(i.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
+//            "</if>" +
+//            " order by c.coupon_price "+
+//            "</script>"})
+//    List<FsStoreCouponIssueVO> selectFsStoreCouponIssueListVO(@Param("maps") FsStoreCouponIssueScrm fsStoreCouponIssue);
+
+    List<FsStoreCouponIssueVO> selectFsStoreCouponIssueListVO(
+            @Param("maps") FsStoreCouponIssueScrm fsStoreCouponIssue
+    );
+
+    /**
+     * 扣减优惠券库存,防止超发
+     */
+    int deductRemainCount(@Param("issueId") Long issueId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueUserScrmMapper.java

@@ -62,4 +62,9 @@ public interface FsStoreCouponIssueUserScrmMapper
     public int deleteFsStoreCouponIssueUserByIds(Long[] ids);
     @Select("select ifnull(count(1),0) from fs_store_coupon_issue_user_scrm where user_id=#{userId} and issue_id=#{id} ")
     int checkReceive(@Param("userId") String userId,@Param("id") Long id);
+
+    /**
+     *  校验用户是否领取过当前优惠券
+     */
+    int countUserReceived(@Param("userId") Long userId, @Param("issueId") Long issueId);
 }

+ 20 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -1101,15 +1101,33 @@ public interface FsStoreOrderScrmMapper
 
     List<FsStoreOrderScrm> selectFsStoreOrderStatisticsByUserId(@Param("ids") List<Long> ids);
 
-    @Select("select id from fs_store_order_scrm WHERE `status`= 1  and  extend_order_id is null ")
+    // 未推ERP订单列表,排除【团购订单(order_type=8)但对应的团尚未成团】,避免未成团的团购订单被提前推送到ERP
+    @Select("select id from fs_store_order_scrm "
+            + "where `status` = 1 and extend_order_id is null "
+            + "and (order_type is null or order_type <> 8 "
+            + "     or exists (select 1 from fs_store_product_group_buy g "
+            + "                where g.id = fs_store_order_scrm.group_buy_id "
+            + "                  and g.status = 1 and g.del_flag = 0))")
     List<Long> selectFsStoreOrderNoCreateOms();
 
     @Select("select * from fs_store_order_scrm where status = 1 and extend_order_id is not null and extend_order_id != '' and delivery_id is null order by update_time")
     List<FsStoreOrderScrm> selectUpdateExpress();
 
-    @Select("select fso.id from fs_store_order_scrm fso inner join fs_store_order_audit_scrm fsoa on fsoa.order_id = fso.id where fso.`status`= 1 and fso.extend_order_id is null and fsoa.audit_status = 4")
+    // 未推ERP且已审核订单列表,排除【团购订单(order_type=8)但对应的团尚未成团】,避免未成团的团购订单被提前推送到ERP
+    @Select("select fso.id from fs_store_order_scrm fso "
+            + "inner join fs_store_order_audit_scrm fsoa on fsoa.order_id = fso.id "
+            + "where fso.`status` = 1 and fso.extend_order_id is null and fsoa.audit_status = 4 "
+            + "and (fso.order_type is null or fso.order_type <> 8 "
+            + "     or exists (select 1 from fs_store_product_group_buy g "
+            + "                where g.id = fso.group_buy_id "
+            + "                  and g.status = 1 and g.del_flag = 0))")
     List<Long> selectFsStoreOrderNoCreateOmsAndReviewed();
 
+    // 按 group_buy_id 查询所有已支付未推ERP的订单ID(成团后主动补推 ERP 用)
+    @Select("select id from fs_store_order_scrm "
+            + "where group_buy_id = #{groupBuyId} and `status` = 1 and extend_order_id is null")
+    List<Long> selectOrderIdsByGroupBuyId(@Param("groupBuyId") Long groupBuyId);
+
     List<ReportScrm> selectOrderByCustomerIds(@Param("map") ReportParam param);
 
     int selectFsStoreOrderCountByParam(FsStoreOrderStatisticsParam param);

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductActivityMapper.java

@@ -122,6 +122,16 @@ public interface FsStoreProductActivityMapper {
     List<FsStoreProductActivity> selectActivitySpecsByProductIdAndType(
             @Param("productId") Long productId,
             @Param("activityType") Integer activityType);
+
+    /**
+     * 查询团购活动列表(1小时内即将开抢+未过期,activity_type=8)
+     */
+    List<FsStoreProductActivity> selectUpcomingGroupBuyActivityList();
+
+    /**
+     * 按商品ID查当前进行中的团购活动
+     */
+    List<FsStoreProductActivity> selectGroupBuyActivityByProductId(@Param("productId") Long productId);
     /**
      * 按商品ID和规格ID查询活动记录
      * @param productId 商品id

+ 31 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductGroupBuyItemMapper.java

@@ -0,0 +1,31 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.domain.FsStoreProductGroupBuyItem;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 商品限时团购参与详情Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+public interface FsStoreProductGroupBuyItemMapper {
+
+    /**
+     * 新增参与详情
+     * 依靠唯一键 uk_group_user(group_id,user_id) 做回调幂等:重复插入会抛 DuplicateKeyException
+     */
+    int insertFsStoreProductGroupBuyItem(FsStoreProductGroupBuyItem item);
+
+    /**
+     * 按团ID查询所有参与详情
+     */
+    List<FsStoreProductGroupBuyItem> selectByGroupId(@Param("groupId") Long groupId);
+
+    /**
+     * 判断指定用户是否已在该团
+     */
+    Integer existsInGroup(@Param("groupId") Long groupId, @Param("userId") Long userId);
+}

+ 109 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductGroupBuyMapper.java

@@ -0,0 +1,109 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.domain.FsStoreProductGroupBuy;
+import com.fs.hisStore.vo.FsStoreGroupBuyListVO;
+import com.fs.hisStore.vo.FsStoreGroupBuyMemberVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 商品限时团购主表Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+public interface FsStoreProductGroupBuyMapper {
+
+    /**
+     * 根据主键查询
+     */
+    FsStoreProductGroupBuy selectFsStoreProductGroupBuyById(@Param("id") Long id);
+
+    /**
+     * 新增团
+     */
+    int insertFsStoreProductGroupBuy(FsStoreProductGroupBuy groupBuy);
+
+    /**
+     * 更新团
+     */
+    int updateFsStoreProductGroupBuy(FsStoreProductGroupBuy groupBuy);
+
+    /**
+     * 查询一个"可加入"的团(同活动、进行中、未满员、未过期、且用户未加入过)
+     * 按"差的人少、创建早"排序,优先凑满老团
+     */
+    FsStoreProductGroupBuy selectJoinableGroup(@Param("activityId") Long activityId,
+                                               @Param("userId") Long userId);
+
+    /**
+     * 原子抢名额:join_num+1,仅在进行中、未过期、未满员时生效
+     * 成团标记改由 {@link #tryMarkPaidAndComplete} 在支付回调时按实际付款人数判定,
+     * 这里不再检测满员。
+     * 返回影响行数:1=抢到,0=失败(满员/已结束/已关闭)
+     */
+    int tryJoinGroup(@Param("groupId") Long groupId);
+
+    /**
+     * 支付回调推进付款数 + 满员时原子置成团:
+     * paid_num+1,按新 paid_num 判定是否满员 -> 满员时同步置 status=1 + complete_time
+     * 返回行数:1=正常推进(0=团已非进行中跳过)
+     */
+    int tryMarkPaidAndComplete(@Param("groupId") Long groupId);
+
+    /**
+     * 释放名额:超时未支付取消订单时调用,join_num-1
+     * 仅对 status=0 且 join_num>0 的团生效;已成团的不回滚
+     * 返回影响行数:1=释放成功,0=无需释放
+     */
+    int releaseJoin(@Param("groupId") Long groupId);
+
+    /**
+     * 已付款订单退款时释放:同时退 join_num 和 paid_num,避免 paid_num 虚高影响后续成团判定
+     * 仅对 status=0 且 join_num>0 且 paid_num>0 的团生效;已成团的不动
+     */
+    int releasePaidAndJoin(@Param("groupId") Long groupId);
+
+    /**
+     * 扫过期未成团的团:status=0 且 end_time<=now()
+     * 带 limit 保护,避免一次把所有过期团全捞出来堵住任务
+     */
+    List<FsStoreProductGroupBuy> selectExpiredUnformedGroups(@Param("limit") int limit);
+
+    /**
+     * 原子将团标记为“拼团失败”:仅在 status=0 且 end_time<=now() 时置 2
+     * 返回行数:1=标记成功,可以接着处理团内订单;0=已被别的任务抢先处理过,跳过
+     */
+    int markGroupFailed(@Param("groupId") Long groupId);
+
+    /**
+     * 扫孤儿订单:团已判失败(status=2),但团内还有已支付且未退款的订单
+     * <p>这种情况是“支付回调卡在 endTime 之后到达”或“先前任务轮退款失败”造成的,
+     * 是定时任务的兜底切入点。</p>
+     * <p>条件:order.order_type=8 + order.group_buy_id != null + order.status=1
+     * + order.refund_status=0 + order.extend_order_id is null + group.status=2</p>
+     */
+    List<Long> selectOrphanOrderIdsInFailedGroups(@Param("limit") int limit);
+
+    // ==================== 后台管理:列表 / 详情 ====================
+
+    /**
+     * 后台团购列表查询(联 product / activity,拿商品名/图/活动价)。
+     * 所有参数均为可选,null / 空串视为不过滤。当传 id 时只返回这一条,用于详情页头部。
+     */
+    List<FsStoreGroupBuyListVO> selectGroupBuyListForAdmin(
+            @Param("id") Long id,
+            @Param("groupNo") String groupNo,
+            @Param("productId") Long productId,
+            @Param("productName") String productName,
+            @Param("status") Integer status,
+            @Param("beginTime") String beginTime,
+            @Param("endTime") String endTime);
+
+    /**
+     * 查团内成员详情:JOIN item + user + order,把团员与订单状态打平在一起返回。
+     * 用于后台“查看团购详情”。
+     */
+    List<FsStoreGroupBuyMemberVO> selectGroupBuyMembers(@Param("groupId") Long groupId);
+}

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreCouponPublishParam.java

@@ -22,5 +22,15 @@ public class FsStoreCouponPublishParam implements Serializable
 
     private String ids;
 
+    /**
+     * 是否无限张数(0.否 1.是)
+     */
+    private Integer isPermanent;
+
+    /**
+     * 优惠券类型(1.用户领取的券,2.平台发放的券)
+     */
+    private Integer couponType;
+
 
 }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/param/LoginMpWxParam.java

@@ -27,4 +27,6 @@ public class LoginMpWxParam implements Serializable {
     private String appId;
     // 广告链路id
     private String traceId;
+    //邀请人id
+    private Long inviteUserId;
 }

+ 6 - 1
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreCouponIssueScrmService.java

@@ -18,7 +18,7 @@ import com.fs.hisStore.vo.FsStoreCouponIssueVO;
 public interface IFsStoreCouponIssueScrmService
 {
     /**
-     * 查询优惠券领取
+     * 查询优惠券详情
      *
      * @param id 优惠券领取ID
      * @return 优惠券领取
@@ -76,4 +76,9 @@ public interface IFsStoreCouponIssueScrmService
     List<FsStoreCouponIssueVO> selectFsStoreCouponIssueListVO(FsStoreCouponIssueScrm fsStoreCouponIssue);
 
     R sendFsUserCoupon(FsUserCouponScrmSendParam param);
+
+    /**
+     * 用户领取优惠券
+     */
+    R receiveCoupon(Long issueId, Long userId);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductActivityService.java

@@ -96,4 +96,14 @@ public interface IFsStoreProductActivityService {
      * 按商品ID和活动类型查询所有参与活动的规格(用于详情页返回规格数组)
      */
     List<FsStoreProductActivity> selectActivitySpecsByProductIdAndType(Long productId, Integer activityType);
+
+    /**
+     * 查询团购活动列表(1小时内即将开抢+未过期,activity_type=8)
+     */
+    List<FsStoreProductActivity> selectUpcomingGroupBuyActivityList();
+
+    /**
+     * 按商品ID查当前进行中的团购活动
+     */
+    List<FsStoreProductActivity> selectGroupBuyActivityByProductId(Long productId);
 }

+ 86 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductGroupBuyService.java

@@ -0,0 +1,86 @@
+package com.fs.hisStore.service;
+
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.vo.FsStoreGroupBuyListVO;
+
+import java.util.List;
+
+/**
+ * 限时团购业务Service
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+public interface IFsStoreProductGroupBuyService {
+
+    /**
+     * 下单阶段预占团购名额。
+     * 策略:
+     *  1. 找一个"未满员、未过期、当前用户未加入"的团,原子抢一个名额
+     *  2. 都抢不到(或没有)就新建一个团,自己是第一个人
+     * 并发靠 UPDATE 原子 CAS,循环重试最多 {@link #DEFAULT_RESERVE_RETRY} 次
+     *
+     * @param activityId 活动ID
+     * @param userId     下单用户ID
+     * @return 预占成功返回团ID;返回 null 表示预占失败(调用方应中断下单)
+     */
+    Long reserveGroupSlot(Long activityId, Long userId);
+
+    /** 下单抢名额最大重试次数 */
+    int DEFAULT_RESERVE_RETRY = 5;
+
+    /**
+     * 支付成功后落地团购参与详情。
+     * 走到这里时订单已经在下单阶段抢好了 group_buy_id:
+     *  1. 订单已有 group_buy_id → 写 item(uk_group_user 幂等),按当前 join_num 判断是否成团
+     *  2. 订单没有 group_buy_id(历史单或极端异常)→ 退回到"回调匹配团"兜底逻辑
+     *
+     * @param order 已支付订单
+     */
+    void handleAfterPay(FsStoreOrderScrm order);
+
+    /**
+     * 释放名额:超时未支付取消订单时调用,把 join_num 回滚 1。
+     * 已成团(status=1)的团不回滚,避免把已成团的位置退回导致整团信息不一致。
+     *
+     * @param groupBuyId 团ID
+     * @return 是否释放成功
+     */
+    boolean releaseGroupSlot(Long groupBuyId);
+
+    /**
+     * 已付款订单退款时释放名额:同时回滚 join_num 和 paid_num。
+     * <p>适用场景:申请售后、后台手动退款等已付款订单的退款路径;
+     * 如果不把 paid_num 也回滚,后来的团员付款时会被虚高的 paid_num 误判成团。</p>
+     * <p>SQL 层做了 status=0 保护,已成团(status=1) 或 已失败(status=2) 的团调下来是空操作,
+     * 调用方无脑调即可,不用自己判状态。</p>
+     *
+     * @param groupBuyId 团ID
+     * @return true=实际回滚了一行;false=团已非进行中,跳过
+     */
+    boolean releasePaidGroupSlot(Long groupBuyId);
+
+    // ==================== 后台管理:列表 / 详情 ====================
+
+    /**
+     * 后台团购列表查询(用于 /store/productGroupBuy/list 分页)。
+     * 调用方请先 startPage() 再调这个方法。所有参数均可空。
+     */
+    List<FsStoreGroupBuyListVO> selectGroupBuyListForAdmin(String groupNo,
+                                                            Long productId,
+                                                            String productName,
+                                                            Integer status,
+                                                            String beginTime,
+                                                            String endTime);
+
+    /**
+     * 按商品ID查历史拼团。调用方请先 startPage() 再调这个方法。
+     */
+    List<FsStoreGroupBuyListVO> selectGroupBuyListByProduct(Long productId);
+
+    /**
+     * 后台团购详情:主信息 + 团员列表(含关联订单状态)。
+     * 找不到返回 null。
+     */
+    FsStoreGroupBuyListVO selectGroupBuyDetailForAdmin(Long id);
+}

+ 35 - 3
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -165,7 +165,8 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
 
     @Autowired
     IFsUserScrmService userService;
-
+    @Autowired
+    IFsStoreProductGroupBuyService groupBuyService;
     @Autowired
     IPayService ybPayService;
     @Autowired
@@ -366,8 +367,14 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         if("1".equals(configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"))
                 && StringUtils.isEmpty(order.getExtendOrderId())
                 && !CloudHostUtils.hasCloudHostName("康年堂")){
-            logger.info("erpOpen:{}",configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"));
-            return R.error("仓库未生成订单,暂时不能申请退款,请联系客服");
+            // 团购未成团的订单压根没推过 ERP,这种情况该让用户能退 —— 放行,不走下面的 ERP 取消调用
+            boolean isUnformedGroupOrder = order.getOrderType() != null
+                    && order.getOrderType() == 8
+                    && order.getGroupBuyId() != null;
+            if (!isUnformedGroupOrder) {
+                logger.info("erpOpen:{}", configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"));
+                return R.error("仓库未生成订单,暂时不能申请退款,请联系客服");
+            }
         }
         if(order.getStatus()== OrderInfoEnum.STATUS_NE3.getValue()){
             return R.error("已取消订单不能申请售后");
@@ -472,6 +479,8 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         request.setRefund_state(1);
         request.setStoreAfterSalesId(storeAfterSales.getId());
         request.setOrderStatus(orderStatus);
+        // 团购订单申请退款时先把占的名额还回去,SQL 内部只对未成团的团生效,已成团的不会动
+        releaseGroupSlotIfNeeded(order);
         if (StringUtils.isNotBlank(order.getExtendOrderId())){
             BaseResponse response=erpOrderService.refundUpdateScrm(request);
             if(response.getSuccess()){
@@ -485,6 +494,29 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         return R.ok();
     }
 
+    /**
+     * 团购订单申请退款时还回占的名额。
+     * <p>仅对 orderType=8 且已有 groupBuyId 的订单生效;
+     * 走的是 releasePaidGroupSlot,同时回滚 join_num + paid_num(售后前提就是已付款);
+     * Mapper SQL 只改 status=0 的未成团团,已成团的调下来不会误伤其他团友。</p>
+     * <p>异常不中断退款主流程,打个错误日志给运维看就行。</p>
+     */
+    private void releaseGroupSlotIfNeeded(FsStoreOrderScrm order) {
+        if (order == null || order.getOrderType() == null || order.getOrderType() != 8) {
+            return;
+        }
+        Long groupBuyId = order.getGroupBuyId();
+        if (groupBuyId == null) {
+            return;
+        }
+        try {
+            groupBuyService.releasePaidGroupSlot(groupBuyId);
+        } catch (Exception e) {
+            logger.error("团购订单申请退款释放名额失败,orderId={},groupBuyId={},需人工核查",
+                    order.getId(), groupBuyId, e);
+        }
+    }
+
     private IErpOrderService getErpService() {
         FsSysConfig sysConfig = configUtil.getSysConfig();
         Integer erpOpen = sysConfig.getErpOpen();

+ 94 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCouponIssueScrmServiceImpl.java

@@ -7,6 +7,7 @@ import java.util.List;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.mapper.*;
@@ -235,4 +236,97 @@ public class FsStoreCouponIssueScrmServiceImpl implements IFsStoreCouponIssueScr
     }
 
 
+
+    /**
+     * 用户领取优惠券
+     *
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R receiveCoupon(Long issueId, Long userId) {
+        if (issueId == null) {
+            throw new CustomException("优惠券发布ID不能为空");
+        }
+        if (userId == null) {
+            throw new CustomException("用户ID不能为空");
+        }
+
+        // 1. 查询发布详情和优惠券模板
+        FsStoreCouponIssueScrm issue = fsStoreCouponIssueMapper.selectFsStoreCouponIssueById(issueId);
+        if (issue == null) {
+            throw new CustomException("优惠券不存在");
+        }
+        if(issue.getStatus().equals(0)){
+            return R.error("此优惠券已停止领取");
+        }
+
+        Date now = new Date();
+
+        // 2. 校验领取时间
+        if (issue.getStartTime() != null && now.before(issue.getStartTime())) {
+            throw new CustomException("优惠券领取未开始");
+        }
+
+        if (issue.getLimitTime() != null && now.after(issue.getLimitTime())) {
+            throw new CustomException("优惠券领取已结束");
+        }
+
+        // 3. 校验是否已领取
+        int receivedCount = fsStoreCouponIssueUserMapper.countUserReceived(userId, issueId);
+        if (receivedCount > 0) {
+            throw new CustomException("您已经领取过该优惠券");
+        }
+
+         //4. 非无限张数,需要扣减库存
+        if (issue.getIsPermanent() == null || issue.getIsPermanent() != 1) {
+            int deductRows = fsStoreCouponIssueMapper.deductRemainCount(issueId);
+
+            if (deductRows <= 0) {
+                throw new CustomException("优惠券已领完");
+            }
+        }
+
+         //5. 插入领取记录
+        try {
+            FsStoreCouponIssueUserScrm couponIssueUser = new FsStoreCouponIssueUserScrm();
+            couponIssueUser.setUserId(userId);
+            couponIssueUser.setIssueId(issueId);
+            couponIssueUser.setCreateTime(new Date());
+            couponIssueUser.setIsDel(0);
+            fsStoreCouponIssueUserMapper.insertFsStoreCouponIssueUser(couponIssueUser);
+        } catch (Exception e) {
+            throw new CustomException("插入领取记录失败");
+        }
+
+        // 6. 计算用户优惠券有效期
+//        Date userCouponLimitTime = buildUserCouponLimitTime(issue.getCouponTime());
+
+        // 7. 插入用户实际可用优惠券
+//        couponReceiveMapper.insertUserCoupon(
+//                issue,
+//                userId,
+//                userCouponLimitTime,
+//                "receive"
+//        );
+
+        FsStoreCouponScrm coupon=couponMapper.selectFsStoreCouponById(issue.getCouponId());
+        FsStoreCouponUserScrm couponUser=new FsStoreCouponUserScrm();
+        couponUser.setCouponId(issue.getCouponId());
+        couponUser.setUserId(userId);
+        couponUser.setCouponTitle(issue.getCouponName());
+        couponUser.setCouponPrice(coupon.getCouponPrice());
+        couponUser.setUseMinPrice(coupon.getUseMinPrice());
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(new Date());
+        calendar.add(Calendar.DATE,coupon.getCouponTime().intValue());
+        couponUser.setLimitTime(calendar.getTime());
+        couponUser.setCreateTime(new Date());
+        couponUser.setType("Claim");
+        couponUser.setStatus(0);
+        fsStoreCouponUserMapper.insertFsStoreCouponUser(couponUser);
+
+
+//
+        return R.ok("领取成功");
+    }
 }

+ 177 - 52
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -248,6 +248,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private RedissonClient redissonClient;
 
+    @Autowired
+    private com.fs.hisStore.service.IFsStoreProductGroupBuyService groupBuyService;
+
     @Autowired
     private com.fs.common.core.redis.service.ActivityStockService activityStockService;
 
@@ -851,7 +854,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             }
         }
 
-        if(cartParam.getProductType() != null && (cartParam.getProductType() == 6 || cartParam.getProductType() == 7) ){//更新金额
+        if(cartParam.getProductType() != null && (cartParam.getProductType() == 6 || cartParam.getProductType() == 7 || cartParam.getProductType() == 8) ){//更新金额
             for (FsStoreCartQueryVO c : carts){
                 //获取对应商品金额
                 FsStoreProductActivity activity = activityMapper.selectActivityByProductIdAndSpecId(c.getProductId(), c.getProductAttrValueId());
@@ -861,7 +864,18 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 //更新购物车信息
                 FsStoreCartScrm cartScrm = new FsStoreCartScrm();
                 cartScrm.setId(c.getId());
-                BigDecimal price = cartParam.getProductType() == 7?activity.getDiscountPrice():activity.getFlashPrice();
+                // 按活动类型取价:7=折扣价 / 8=团购价 / 6=秒杀价
+                BigDecimal price;
+                if (cartParam.getProductType() == 7) {
+                    price = activity.getDiscountPrice();
+                } else if (cartParam.getProductType() == 8) {
+                    price = activity.getGroupPrice();
+                    if (price == null) {
+                        return R.error("操作失败,团购价未设置!");
+                    }
+                } else {
+                    price = activity.getFlashPrice();
+                }
                 cartScrm.setChangePrice(price);
                 c.setPrice(price);
                 c.setChangePrice(price);
@@ -1347,61 +1361,86 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     public R createActivityOrder(long userId, FsStoreOrderCreateParam param) {
         Long associatedId = null;
         Integer orderType = param.getOrderType();
+        int activityDeductNum = 1; // Redis库存扣减数量(仅秒杀/限时折扣使用,异常时用于回滚)
+        Long reservedGroupBuyId = null; // 团购下单阶段预占到的团ID,异常时用于释放名额
     
-        // 1. 活动参数校验
-        if (orderType == null || (orderType != 6 && orderType != 7)) {
+        //活动参数校验(6=秒杀 7=限时折扣 8=限时团购)
+        if (orderType == null || (orderType != 6 && orderType != 7 && orderType != 8)) {
             return R.error("无效的活动类型");
         }
         if (param.getAssociatedId() == null || param.getAssociatedId() <= 0) {
             return R.error("活动ID不能为空");
         }
-    
-        // 2. 校验活动时间和状态
-        com.fs.common.core.redis.service.ActivityValidateResult validateResult =
-                activityStockService.validateActivityWithDetail(orderType, param.getAssociatedId());
-        if (!validateResult.isValid()) {
-            return R.error(validateResult.getMessage());
-        }
-    
-        // 3. 确保Redis活动信息和规格库存已初始化(活动无独立库存,用规格库存)
+
+        //取活动信息
         FsStoreProductActivity activityInfo = activityService.selectFsStoreProductActivityById(param.getAssociatedId());
         if (activityInfo == null) {
             return R.error("活动信息不存在");
         }
-        activityStockService.initActivityInfo(
-                param.getAssociatedId(), 1,
-                activityInfo.getStartTime().getTime(), activityInfo.getEndTime().getTime(),
-                activityInfo.getProductId(), activityInfo.getSpecId(), null
-        );
-        if (activityInfo.getSpecId() != null && activityInfo.getSpecStock() != null) {
-            activityStockService.initProductSpecStock(activityInfo.getSpecId(), activityInfo.getSpecStock());
-        }
-
-        // 3.5 提前获取购物车,计算活动商品总件数,确保Lua扣减数量与DB扣减数量一致
-        String preCartIds = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
-        List<FsStoreCartQueryVO> preCarts = null;
-        int activityDeductNum = 1; // 默认扣减1件
-        if (preCartIds != null) {
-            preCarts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
-            if (preCarts != null) {
-                activityDeductNum = 0;
-                for (FsStoreCartQueryVO cart : preCarts) {
-                    if (cart.getCartNum() != null) {
-                        activityDeductNum += cart.getCartNum();
+
+        if (orderType == 8) {
+            // 限时团购:不走 Redis/Lua 预扣,库存交由后续 deStockIncSale 走 DB 统一扣减
+            long nowTs = System.currentTimeMillis();
+            if (activityInfo.getStatus() == null || activityInfo.getStatus() != 1) {
+                return R.error("活动已下架");
+            }
+            if (activityInfo.getStartTime() == null || nowTs < activityInfo.getStartTime().getTime()) {
+                return R.error("活动尚未开始");
+            }
+            if (activityInfo.getEndTime() == null || nowTs > activityInfo.getEndTime().getTime()) {
+                return R.error("活动已结束");
+            }
+            associatedId = param.getAssociatedId();
+
+            // 下单时先预占一个团的名额:优先找未满未过期的老团拼团,都没有就自己单独开团
+            reservedGroupBuyId = groupBuyService.reserveGroupSlot(associatedId, userId);
+            if (reservedGroupBuyId == null) {
+                return R.error("团购名额预占失败,请稍后重试");
+            }
+        } else {
+            // 秒杀/限时折扣:保持原有 Redis + Lua 预扣路径
+            //校验活动时间和状态
+            com.fs.common.core.redis.service.ActivityValidateResult validateResult =
+                    activityStockService.validateActivityWithDetail(orderType, param.getAssociatedId());
+            if (!validateResult.isValid()) {
+                return R.error(validateResult.getMessage());
+            }
+
+            //确保Redis活动信息和规格库存已初始化(活动无独立库存,用规格库存)
+            activityStockService.initActivityInfo(
+                    param.getAssociatedId(), 1,
+                    activityInfo.getStartTime().getTime(), activityInfo.getEndTime().getTime(),
+                    activityInfo.getProductId(), activityInfo.getSpecId(), null
+            );
+            if (activityInfo.getSpecId() != null && activityInfo.getSpecStock() != null) {
+                activityStockService.initProductSpecStock(activityInfo.getSpecId(), activityInfo.getSpecStock());
+            }
+
+            //提前获取购物车,计算活动商品总件数,确保Lua扣减数量与DB扣减数量一致
+            String preCartIds = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
+            List<FsStoreCartQueryVO> preCarts = null;
+            if (preCartIds != null) {
+                preCarts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
+                if (preCarts != null) {
+                    activityDeductNum = 0;
+                    for (FsStoreCartQueryVO cart : preCarts) {
+                        if (cart.getCartNum() != null) {
+                            activityDeductNum += cart.getCartNum();
+                        }
+                    }
+                    if (activityDeductNum <= 0) {
+                        activityDeductNum = 1;
                     }
-                }
-                if (activityDeductNum <= 0) {
-                    activityDeductNum = 1;
                 }
             }
-        }
 
-        // 4. Lua原子扣减活动库存(已移除 getStock() 预检查——先查再扣存在竞态窗口,Lua脚本本身会原子判断库存是否充足)
-        boolean deductSuccess = activityStockService.deductStock(orderType, param.getAssociatedId(), activityDeductNum);
-        if (!deductSuccess) {
-            return R.error("活动商品已售罄,请稍后重试");
+            //Lua原子扣减活动库存(已移除 getStock() 预检查——先查再扣存在竞态窗口,Lua脚本本身会原子判断库存是否充足)
+            boolean deductSuccess = activityStockService.deductStock(orderType, param.getAssociatedId(), activityDeductNum);
+            if (!deductSuccess) {
+                return R.error("活动商品已售罄,请稍后重试");
+            }
+            associatedId = param.getAssociatedId();
         }
-        associatedId = param.getAssociatedId();
 
         try {
             FsUserCompanyUser fsUserCompanyUser = fsUserCompanyUserMapper.selectFsUserCompanyUserByUserId(userId);
@@ -1618,6 +1657,10 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 //活动商品只扣Redis(已在上文扣减),DB延后扣减
                 // storeOrder.setAssociatedId
                 storeOrder.setAssociatedId(associatedId);
+                // 团购订单:回写下单时预占到的团ID,支付回调就按这个团落详情
+                if (reservedGroupBuyId != null) {
+                    storeOrder.setGroupBuyId(reservedGroupBuyId);
+                }
                 Integer flag = fsStoreOrderMapper.insertFsStoreOrder(storeOrder);
                 if (flag == 0) {
                     return R.error("订单创建失败");
@@ -1741,14 +1784,24 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 return R.error("订单已过期");
             }
         } catch (Exception e) {
-            // Redis已扣减成功,订单创建过程异常,必须回滚Redis库存
-            try {
-                activityStockService.rollbackStock(orderType, param.getAssociatedId(), activityDeductNum);
-                log.info("订单创建异常,Redis库存已回滚,associatedId={}, orderType={}, 回滚数量={}",
-                        param.getAssociatedId(), orderType, activityDeductNum);
-            } catch (Exception rollbackEx) {
-                log.error("订单创建异常后Redis库存回滚失败!associatedId={},orderType={},需要人工处理",
-                        param.getAssociatedId(), orderType, rollbackEx);
+            // 团购未走 Redis 预扣,无需回滚 Redis;秒杀/限时折扣 Redis 已扣减成功,订单创建异常必须回滚
+            if (orderType != null && orderType != 8) {
+                try {
+                    activityStockService.rollbackStock(orderType, param.getAssociatedId(), activityDeductNum);
+                    log.info("订单创建异常,Redis库存已回滚,associatedId={}, orderType={}, 回滚数量={}",
+                            param.getAssociatedId(), orderType, activityDeductNum);
+                } catch (Exception rollbackEx) {
+                    log.error("订单创建异常后Redis库存回滚失败!associatedId={},orderType={},需要人工处理",
+                            param.getAssociatedId(), orderType, rollbackEx);
+                }
+            }
+            // 团购订单:下单预占到的名额因订单异常无效,要把名额还回去,免得团被卡死
+            if (orderType != null && orderType == 8 && reservedGroupBuyId != null) {
+                try {
+                    groupBuyService.releaseGroupSlot(reservedGroupBuyId);
+                } catch (Exception releaseEx) {
+                    log.error("订单创建异常后释放团购名额失败,groupBuyId={},需人工处理", reservedGroupBuyId, releaseEx);
+                }
             }
             log.error("活动订单创建异常,orderType={}, associatedId={}", orderType, param.getAssociatedId(), e);
             throw e;
@@ -1897,6 +1950,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             this.refundCoupon(order);
             //退回库存
             this.refundStock(order);
+            // 团购订单取消时把占的位置还回去,SQL 内部只对未成团的团生效,已成团的不会动
+            releaseGroupSlotIfNeeded(order);
             fsStoreOrderMapper.cancelOrder(orderId);
             //添加记录
             orderStatusService.create(order.getId(), OrderLogEnum.CANCEL_ORDER.getValue(),
@@ -2767,6 +2822,15 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         } catch (Exception e) {
             log.error("创建限购商品失败:{}", e.getMessage());
         }
+        // 限时团购(activity_type=8):支付成功后才真正加入/新建团,并回写 group_buy_id
+        // 自身事务独立,异常吃掉不影响支付回调主流程(汇付需 return SUCCESS 避免重试)
+        if (order.getOrderType() != null && order.getOrderType() == 8) {
+            try {
+                groupBuyService.handleAfterPay(order);
+            } catch (Exception e) {
+                log.error("团购订单支付后处理失败,orderId={}", order.getId(), e);
+            }
+        }
         return "SUCCESS";
     }
 
@@ -3133,11 +3197,18 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         if (order.getStatus() != 1 && order.getStatus() != 2) {
             return R.error("非法操作");
         }
+
+        Integer originalRefundStatus = order.getRefundStatus();
         if (erpConfig.getErpOpen() != null
                 && erpConfig.getErpOpen() == 1
                 && order.getExtendOrderId() == null
                 && !CloudHostUtils.hasCloudHostName("康年堂")) {
-            return R.error("暂未推送至erp,请稍后再试!");
+            boolean isUnformedGroupOrder = order.getOrderType() != null
+                    && order.getOrderType() == 8
+                    && order.getGroupBuyId() != null;
+            if (!isUnformedGroupOrder) {
+                return R.error("暂未推送至erp,请稍后再试!");
+            }
         }
         if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
             ErpRefundUpdateRequest request = new ErpRefundUpdateRequest();
@@ -3180,6 +3251,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         order.setRefundStatus(OrderInfoEnum.REFUND_STATUS_2.getValue());
         fsStoreOrderMapper.updateFsStoreOrder(order);
 
+        // 团购已付款订单退款:同时回滚 join_num + paid_num。
+        if (originalRefundStatus == null || originalRefundStatus == 0) {
+            releasePaidGroupSlotIfNeeded(order);
+        }
+
         //退库存
         //获取订单下的商品
         List<FsStoreOrderItemVO> orderItemVOS = fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderId(order.getId());
@@ -3189,8 +3265,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             refundActivityStock(order, orderItemVOS);
         }
 
+        // 团购订单(orderType=8)下单时走的是 deStockIncSale 直接扣 fs_store_product_attr_value_scrm 规格库存(stock-/sales+),
+        // 没走 isAfterSales 的售后流程;超时自动退款 / 直接退款路径下 isAfterSales 还是 0,
+        // 不无条件回的话 fs_store_product_attr_value_scrm 的 stock 就泄漏了。
+        boolean isGroupBuyOrder = order.getOrderType() != null && order.getOrderType() == 8;
         for (FsStoreOrderItemVO vo : orderItemVOS) {
-            if (vo.getIsAfterSales() == 1) {
+            if (vo.getIsAfterSales() == 1 || isGroupBuyOrder) {
                 productService.incProductStock(vo.getNum(), vo.getProductId(), vo.getProductAttrValueId());
             }
 
@@ -4032,6 +4112,51 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 && order.getAssociatedId() > 0;
     }
 
+    /**
+     * 团购订单未付款取消时还回占的名额。
+     * <p>仅对 orderType=8 且已有 groupBuyId 的订单生效;
+     * Mapper 层的 SQL 从来只改 status=0 的未成团团,已成团的订单调下来不会误伤。</p>
+     * <p>异常不中断主流程,打个错误日志给运维看就行。</p>
+     */
+    private void releaseGroupSlotIfNeeded(FsStoreOrderScrm order) {
+        if (order == null || order.getOrderType() == null || order.getOrderType() != 8) {
+            return;
+        }
+        Long groupBuyId = order.getGroupBuyId();
+        if (groupBuyId == null) {
+            return;
+        }
+        try {
+            groupBuyService.releaseGroupSlot(groupBuyId);
+        } catch (Exception e) {
+            log.error("团购订单释放名额失败,orderId={},groupBuyId={},需人工核查",
+                    order.getId(), groupBuyId, e);
+        }
+    }
+
+    /**
+     * 已付款订单退款时还名额:同时回滚 join_num + paid_num。
+     * <p>跟 {@link #releaseGroupSlotIfNeeded} 的区别是这个用于"已付款"场景(refundOrderMoney),
+     * 前者用于"未付款取消"场景(cancelOrder)。paid_num 只有在已付款路径才加过 1,
+     * 退款时需要同步扣回,不然 paid_num 会虚高让后续团员被误判成团。</p>
+     * <p>SQL 层做了 status=0 保护,团已成团/已失败时空转不会误改,调用方无需自己判。</p>
+     */
+    private void releasePaidGroupSlotIfNeeded(FsStoreOrderScrm order) {
+        if (order == null || order.getOrderType() == null || order.getOrderType() != 8) {
+            return;
+        }
+        Long groupBuyId = order.getGroupBuyId();
+        if (groupBuyId == null) {
+            return;
+        }
+        try {
+            groupBuyService.releasePaidGroupSlot(groupBuyId);
+        } catch (Exception e) {
+            log.error("团购订单退款释放名额失败,orderId={},groupBuyId={},需人工核查",
+                    order.getId(), groupBuyId, e);
+        }
+    }
+
     /**
      * 活动订单退款时回滚Redis规格库存(Lua原子操作)。
      * 异常不中断退款主流程,但记录错误日志以便人工排查。

+ 11 - 11
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java

@@ -1145,12 +1145,12 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
             type = "wxApp";
         }
         //支付宝可以不需要appid(在没有appid的情况下)【ps:小程序的支付宝没传appid 就G】
-        if ((PaymentMethodEnum.ALIPAY == payOrderParam.getPaymentMethod())
-                && StringUtils.isBlank(payOrderParam.getAppId())) {
-            String json = configService.selectConfigByKey("his.pay");
-            PayConfigDTO payConfigDTO = JSONUtil.toBean(json, PayConfigDTO.class);
-            payConfig.setType(payConfigDTO.getType());
-        } else {
+//        if ((PaymentMethodEnum.ALIPAY == payOrderParam.getPaymentMethod())
+//                && StringUtils.isBlank(payOrderParam.getAppId())) {
+//            String json = configService.selectConfigByKey("his.pay");
+//            PayConfigDTO payConfigDTO = JSONUtil.toBean(json, PayConfigDTO.class);
+//            payConfig.setType(payConfigDTO.getType());
+//        } else {
             if (StringUtils.isBlank(payOrderParam.getAppId())) {
                 throw new IllegalArgumentException("appId不能为空");
             }
@@ -1173,7 +1173,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
             payConfig.setAppId(fsCoursePlaySourceConfig.getAppid());
 
             logger.debug("支付配置 his.pay: {}", payConfig);
-        }
+//        }
 
 
 //        FsPayConfig payConfig = JSONUtil.toBean(json, FsPayConfig.class);
@@ -1191,7 +1191,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
             LiveOrderPayment liveOrderPayment = createLiveStorePayment(payConfig, user, payOrderParam);
             BeanUtils.copyProperties(liveOrderPayment, storePayment);
         } else {
-            storePayment = createStorePaymentScrm(payConfig, user, payOrderParam);
+            storePayment = createStorePaymentScrm(payConfig, user, payOrderParam,merchantAppConfig.getMerchantId());
         }
 
         // 根据配置类型创建第三方支付订单
@@ -1305,7 +1305,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
         order.setReqSeqId(payOrderParam.getBusinessType().getPrefix() + "-" + storePayment.getPayCode());
         order.setTransAmt(storePayment.getPayMoney().toString());
         order.setGoodsDesc(payOrderParam.getBusinessType().getDesc());
-
+        order.setAppId(payOrderParam.getAppId());
         // 微信支付需要设置openid
         if (isWechatPayment(payOrderParam.getPaymentMethod())) {
             order.setOpenid(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));
@@ -1429,7 +1429,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
     /**
      * 创建支付订单
      */
-    private FsStorePaymentScrm createStorePaymentScrm(FsPayConfig payConfig, FsUserScrm user, PayOrderParam payOrderParam) {
+    private FsStorePaymentScrm createStorePaymentScrm(FsPayConfig payConfig, FsUserScrm user, PayOrderParam payOrderParam,String merchantId) {
         String payCode = OrderCodeUtils.getOrderSn();
         if (StringUtils.isEmpty(payCode)) {
             throw new CustomException("订单生成失败,请重试");
@@ -1452,7 +1452,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
         storePayment.setStoreId(payOrderParam.getStoreId());
         storePayment.setUserId(user.getUserId());
         storePayment.setBusinessId(payOrderParam.getOrderId().toString());
-
+        storePayment.setMerConfigId(Long.valueOf(merchantId));
         // 设置openId(如果是微信支付)
         if (isWechatPayment(payOrderParam.getPaymentMethod())) {
             storePayment.setOpenId(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));

+ 31 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductActivityServiceImpl.java

@@ -68,7 +68,27 @@ public class FsStoreProductActivityServiceImpl implements IFsStoreProductActivit
         if (activityType != null && activityType != 0) {
             FsStoreProductActivity ongoing = activityMapper.selectOngoingActivity(productId);
             if (ongoing != null) {
-                throw new RuntimeException("当前商品正在" + (ongoing.getActivityType() == 6 ? "秒杀" : "限时折扣") + "活动中,活动进行期间无法修改活动设置");
+                String typeName;
+                Integer ongoingType = ongoing.getActivityType();
+                if (ongoingType != null && ongoingType == 6) {
+                    typeName = "秒杀";
+                } else if (ongoingType != null && ongoingType == 8) {
+                    typeName = "限时团购";
+                } else {
+                    typeName = "限时折扣";
+                }
+                throw new RuntimeException("当前商品正在" + typeName + "活动中,活动进行期间无法修改活动设置");
+            }
+        }
+
+        // 限时团购(activity_type=8)专有校验:团购人数必须在 2~5 区间,未传或范围外则报错
+        // (平台规定:单团最多 5 人;已由前端 el-input-number min=2 max=5 约束,后端再做一道兜底防止绕过前端调用)
+        if (activityType != null && activityType == 8 && activityList != null && !activityList.isEmpty()) {
+            for (FsStoreProductActivity item : activityList) {
+                Integer gn = item.getGroupNum();
+                if (gn == null || gn < 2 || gn > 5) {
+                    throw new RuntimeException("团购人数必填且限 2~5 人(默认 5 人)");
+                }
             }
         }
 
@@ -241,4 +261,14 @@ public class FsStoreProductActivityServiceImpl implements IFsStoreProductActivit
     public List<FsStoreProductActivity> selectActivitySpecsByProductIdAndType(Long productId, Integer activityType) {
         return activityMapper.selectActivitySpecsByProductIdAndType(productId, activityType);
     }
+
+    @Override
+    public List<FsStoreProductActivity> selectUpcomingGroupBuyActivityList() {
+        return activityMapper.selectUpcomingGroupBuyActivityList();
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectGroupBuyActivityByProductId(Long productId) {
+        return activityMapper.selectGroupBuyActivityByProductId(productId);
+    }
 }

+ 458 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductGroupBuyServiceImpl.java

@@ -0,0 +1,458 @@
+package com.fs.hisStore.service.impl;
+
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.domain.FsStoreProductGroupBuy;
+import com.fs.hisStore.domain.FsStoreProductGroupBuyItem;
+import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductActivityMapper;
+import com.fs.hisStore.mapper.FsStoreProductGroupBuyItemMapper;
+import com.fs.hisStore.mapper.FsStoreProductGroupBuyMapper;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.hisStore.service.IFsStoreProductGroupBuyService;
+import com.fs.hisStore.service.IFsUserScrmService;
+import com.fs.hisStore.vo.FsStoreGroupBuyListVO;
+import com.fs.hisStore.vo.FsStoreGroupBuyMemberVO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 限时团购业务Service实现
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Service
+public class FsStoreProductGroupBuyServiceImpl implements IFsStoreProductGroupBuyService {
+
+    private static final Logger log = LoggerFactory.getLogger(FsStoreProductGroupBuyServiceImpl.class);
+
+    @Autowired
+    private FsStoreProductGroupBuyMapper groupBuyMapper;
+
+    @Autowired
+    private FsStoreProductGroupBuyItemMapper groupBuyItemMapper;
+
+    @Autowired
+    private FsStoreProductActivityMapper activityMapper;
+
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderMapper;
+
+    @Autowired
+    private IFsUserScrmService userService;
+
+    /** 打破与 FsStoreOrderScrmServiceImpl 的循环依赖:对方注入本类,这里反向注入要 @Lazy */
+    @Lazy
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderService;
+
+    // ================================================================
+    // 1. 下单阶段:预占名额
+    // ================================================================
+
+    /**
+     * 用独立事务(REQUIRES_NEW)。订单主事务即便后续因为其他原因回滚,
+     * 这里的名额占用也已提交;但若下单最终失败,调用方应该自行调 {@link #releaseGroupSlot}
+     * 把名额还回去,避免"占座不下单"把团卡死。
+     */
+    @Override
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    public Long reserveGroupSlot(Long activityId, Long userId) {
+        if (activityId == null || userId == null) {
+            return null;
+        }
+        FsStoreProductActivity activity = activityMapper.selectFsStoreProductActivityById(activityId);
+        if (activity == null || activity.getGroupNum() == null || activity.getGroupNum() <= 0) {
+            log.warn("[团购预占] 活动信息不全,activityId={}", activityId);
+            return null;
+        }
+        if (activity.getEndTime() != null && activity.getEndTime().before(new Date())) {
+            log.warn("[团购预占] 活动已结束,activityId={}", activityId);
+            return null;
+        }
+
+        // 先尝试加入已有未满团,循环重试应对并发抢
+        for (int i = 0; i < DEFAULT_RESERVE_RETRY; i++) {
+            FsStoreProductGroupBuy candidate = groupBuyMapper.selectJoinableGroup(activityId, userId);
+            if (candidate == null) {
+                break;
+            }
+            int affected = groupBuyMapper.tryJoinGroup(candidate.getId());
+            if (affected == 1) {
+                return candidate.getId();
+            }
+            // 被别人抢走了,重新找
+        }
+
+        // 没有可加入的团 → 自己开一团
+        FsStoreProductGroupBuy created = createNewGroup(activity, userId);
+        return created.getId();
+    }
+
+    @Override
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    public boolean releaseGroupSlot(Long groupBuyId) {
+        if (groupBuyId == null) {
+            return false;
+        }
+        int affected = groupBuyMapper.releaseJoin(groupBuyId);
+        if (affected == 0) {
+            log.info("[团购释放名额] 无需释放,groupId={}", groupBuyId);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 已付款订单退款路径:同时回滚 join_num 和 paid_num。
+     * 走独立事务,主退款事务即使后续出异常回滚,名额也已经扣进去(跟 releaseGroupSlot 设计一致)。
+     */
+    @Override
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    public boolean releasePaidGroupSlot(Long groupBuyId) {
+        if (groupBuyId == null) {
+            return false;
+        }
+        int affected = groupBuyMapper.releasePaidAndJoin(groupBuyId);
+        if (affected == 0) {
+            // 团已非进行中(成团或已失败),这时不动 paid_num 正是预期行为:
+            // - status=1 成团的订单退款不影响后续成团判定(团已终态)
+            // - status=2 失败团同理,后续孤儿退款走的就是这条路径
+            log.info("[团购退款释放名额] 团已终态无需回滚,groupId={}", groupBuyId);
+            return false;
+        }
+        return true;
+    }
+
+    // ================================================================
+    // 2. 支付回调阶段:写详情 + 成团判断
+    // ================================================================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void handleAfterPay(FsStoreOrderScrm order) {
+        if (order == null || order.getOrderType() == null || order.getOrderType() != 8) {
+            return;
+        }
+        // 主路径:下单已预占到团,回调这里只需落详情 + 判定是否成团
+        if (order.getGroupBuyId() != null) {
+            finishPaidOrderInGroup(order);
+            return;
+        }
+
+        // 兜底:订单没有 group_buy_id(历史单或预占阶段异常),退回到"回调匹配团"
+        log.warn("[团购回调] 订单无 group_buy_id 走兜底匹配,orderId={}", order.getId());
+        fallbackMatchGroupOnPay(order);
+    }
+
+    /**
+     * 已预占好团的订单:写 item + 原子推进 paid_num,满员才真正成团
+     * <p>幂等保护:如果 insertItemSafely 返回 false(uk_group_user 已存在、回调重入),
+     * 说明本笔订单之前已被处理过一次,paid_num 已加入团,这里绝不能再 +1,不然会虚高。</p>
+     */
+    private void finishPaidOrderInGroup(FsStoreOrderScrm order) {
+        Long groupId = order.getGroupBuyId();
+        FsStoreProductGroupBuy group = groupBuyMapper.selectFsStoreProductGroupBuyById(groupId);
+        if (group == null) {
+            log.error("[团购回调] 团已不存在,orderId={}, groupId={}", order.getId(), groupId);
+            return;
+        }
+        // 极端并发兜底:回调到达时团已被定时任务判失败(status=2)——不写 item、不推 ERP,
+        // 订单照常完成支付流程,留给定时任务的孤儿订单扫描兜底退款
+        if (Integer.valueOf(2).equals(group.getStatus())) {
+            log.warn("[团购回调] 订单回调到达时团已判失败,等定时任务兜底退款,orderId={}, groupId={}",
+                    order.getId(), groupId);
+            return;
+        }
+        FsStoreProductActivity activity = activityMapper.selectFsStoreProductActivityById(order.getAssociatedId());
+        boolean freshInsert = insertItemSafely(group, order, activity);
+        if (!freshInsert) {
+            // 回调重入(上游 payConfirm 锁理论上拦的住,这里是额外防御):之前已成功推过 paid_num,
+            // 直接返回不能再 +1,避免 paid_num 虚高误判成团
+            log.info("[团购回调] 重入,item 已存在且 paid_num 已加过,skip,orderId={}", order.getId());
+            return;
+        }
+
+        // 原子 CAS 推进 paid_num + 满员置 status=1,避免多线程同时付款时丢成团
+        int affected = groupBuyMapper.tryMarkPaidAndComplete(groupId);
+        if (affected == 0) {
+            // 走到这里说明团已经不是进行中(被定时任务或其他路径置成成功/失败)——再查一次
+            FsStoreProductGroupBuy latest = groupBuyMapper.selectFsStoreProductGroupBuyById(groupId);
+            if (latest != null && Integer.valueOf(2).equals(latest.getStatus())) {
+                log.warn("[团购回调] 团推进付款数失败,团已失败,等定时任务兜底,orderId={}", order.getId());
+            }
+            return;
+        }
+        // 推进成功:重新查最新状态,若此单是满员的最后一笔,latest.status=1 才触发成团钩子
+        FsStoreProductGroupBuy latest = groupBuyMapper.selectFsStoreProductGroupBuyById(groupId);
+        if (latest != null && Integer.valueOf(1).equals(latest.getStatus())) {
+            onGroupComplete(latest);
+        }
+    }
+
+    /**
+     * 兜底分支:支付回调到了但订单没绑团,按旧逻辑动态匹配一次
+     */
+    private void fallbackMatchGroupOnPay(FsStoreOrderScrm order) {
+        if (order.getAssociatedId() == null) {
+            return;
+        }
+        FsStoreProductActivity activity = activityMapper.selectFsStoreProductActivityById(order.getAssociatedId());
+        if (activity == null || activity.getGroupNum() == null || activity.getGroupNum() <= 0) {
+            return;
+        }
+        if (activity.getEndTime() != null && activity.getEndTime().before(new Date())) {
+            log.warn("[团购回调-兜底] 活动已结束,等待兜底退款,orderId={}", order.getId());
+            return;
+        }
+
+        for (int i = 0; i < DEFAULT_RESERVE_RETRY; i++) {
+            FsStoreProductGroupBuy target = groupBuyMapper.selectJoinableGroup(activity.getId(), order.getUserId());
+            if (target == null) {
+                break;
+            }
+            if (groupBuyMapper.tryJoinGroup(target.getId()) == 1) {
+                attachOrderToGroup(order, target.getId());
+                boolean freshInsert = insertItemSafely(target, order, activity);
+                // 兼容路径也走原子推进 paid_num,满员才触发成团;若 item 已存在(重入)跳过 +1 避免虚高
+                if (freshInsert && groupBuyMapper.tryMarkPaidAndComplete(target.getId()) == 1) {
+                    FsStoreProductGroupBuy after = groupBuyMapper.selectFsStoreProductGroupBuyById(target.getId());
+                    if (after != null && Integer.valueOf(1).equals(after.getStatus())) {
+                        onGroupComplete(after);
+                    }
+                }
+                return;
+            }
+        }
+
+        FsStoreProductGroupBuy created = createNewGroup(activity, order.getUserId());
+        attachOrderToGroup(order, created.getId());
+        boolean freshInsert = insertItemSafely(created, order, activity);
+        // 新开团的第一笔付款也按正常路径推进;重入时 item 已存在不再 +1 paid_num
+        if (freshInsert && groupBuyMapper.tryMarkPaidAndComplete(created.getId()) == 1) {
+            FsStoreProductGroupBuy after = groupBuyMapper.selectFsStoreProductGroupBuyById(created.getId());
+            if (after != null && Integer.valueOf(1).equals(after.getStatus())) {
+                onGroupComplete(after);
+            }
+        }
+    }
+
+    // ================================================================
+    // 3. 成团钩子
+    // ================================================================
+
+    /**
+     * 成团后:把团里所有待推 ERP 的订单统一推送出去
+     * <p><b>关键</b>:必须在当前事务 <b>提交之后</b> 再推 ERP。</p>
+     * <p>原因:{@code createOmsOrder} 自带 {@code @Transactional},ERP 外部接口超时/异常会把当前事务
+     * 标记为 rollback-only。即便我们 try/catch 吞掉了异常,事务提交时依然会抛
+     * {@code UnexpectedRollbackException},导致 {@code tryMarkPaidAndComplete} 的成团标记(status=1)
+     * 和 paid_num+1、item 插入被连锁回滚——用户钱收了、团却没成团,数据就错乱了。</p>
+     * <p>所以这里用 afterCommit 钩子延后推 ERP;单个订单推失败不影响其他订单;
+     * 漏推的由原有 {@code PushErp} 定时任务兜底,业务最终一致。</p>
+     */
+    private void onGroupComplete(FsStoreProductGroupBuy group) {
+        log.info("[团购成团] groupId={}, groupNo={}, joinNum={}, groupNum={}",
+                group.getId(), group.getGroupNo(), group.getJoinNum(), group.getGroupNum());
+        final Long groupId = group.getId();
+        if (TransactionSynchronizationManager.isSynchronizationActive()) {
+            // 事务提交后再推 ERP,ERP 的任何异常都不会影响本事务落库的成团状态
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    pushErpForGroupOrders(groupId);
+                }
+            });
+        } else {
+            // 兜底:理论上 handleAfterPay 一定带事务,这里走到只可能是调用链被人改过,直接同步推
+            log.warn("[团购成团] 当前无事务同步上下文,降级同步推 ERP,groupId={}", groupId);
+            pushErpForGroupOrders(groupId);
+        }
+        // TODO: 后续接入发货 / 模板消息推送(同样建议放在 afterCommit 里)
+    }
+
+    /**
+     * 实际执行团内订单 ERP 推送:单个失败不影响其他;漏推的由原有定时任务 PushErp 兜底。
+     */
+    private void pushErpForGroupOrders(Long groupId) {
+        try {
+            List<Long> orderIds = fsStoreOrderMapper.selectOrderIdsByGroupBuyId(groupId);
+            if (orderIds == null || orderIds.isEmpty()) {
+                return;
+            }
+            for (Long orderId : orderIds) {
+                try {
+                    fsStoreOrderService.createOmsOrder(orderId);
+                } catch (Exception e) {
+                    log.error("[成团推ERP失败] orderId={}, groupId={}", orderId, groupId, e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("[成团钩子异常] groupId={}", groupId, e);
+        }
+    }
+
+    // ================================================================
+    // 4. 工具方法
+    // ================================================================
+
+    /**
+     * 新建一个团,初始 join_num=1(已占位)、paid_num=0(还没付款)、status=0。
+     * 即使 groupNum=1 的极端配置也不在这里提前置成团,统一由支付回调 tryMarkPaidAndComplete 标成
+     */
+    private FsStoreProductGroupBuy createNewGroup(FsStoreProductActivity activity, Long userId) {
+        FsStoreProductGroupBuy g = new FsStoreProductGroupBuy();
+        g.setGroupNo(generateGroupNo());
+        g.setActivityId(activity.getId());
+        g.setProductId(activity.getProductId());
+        g.setGroupNum(activity.getGroupNum());
+        g.setJoinNum(1);
+        g.setPaidNum(0);
+        g.setStatus(0);
+        g.setStartTime(new Date());
+        g.setEndTime(activity.getEndTime());
+        g.setDelFlag(0);
+        g.setCreateBy(userId == null ? null : String.valueOf(userId));
+        g.setCreateTime(new Date());
+
+        groupBuyMapper.insertFsStoreProductGroupBuy(g);
+        return g;
+    }
+
+    /**
+     * 回写订单 group_buy_id(主路径下下单就写好了,这里只给兜底分支用)
+     */
+    private void attachOrderToGroup(FsStoreOrderScrm order, Long groupId) {
+        FsStoreOrderScrm up = new FsStoreOrderScrm();
+        up.setId(order.getId());
+        up.setGroupBuyId(groupId);
+        fsStoreOrderMapper.updateFsStoreOrder(up);
+        order.setGroupBuyId(groupId);
+    }
+
+    /**
+     * 插入参与详情,依靠唯一键 uk_group_user 保证幂等
+     * @return true = 真新插入(第一次处理);false = 重入(本用户在本团的 item 已存在)
+     */
+    private boolean insertItemSafely(FsStoreProductGroupBuy group,
+                                     FsStoreOrderScrm order,
+                                     FsStoreProductActivity activity) {
+        FsStoreProductGroupBuyItem item = new FsStoreProductGroupBuyItem();
+        item.setGroupId(group.getId());
+        item.setProductId(group.getProductId());
+        if (activity != null) {
+            item.setSpecId(activity.getSpecId());
+            item.setActivityId(activity.getId());
+        }
+        item.setUserId(order.getUserId());
+
+        try {
+            FsUserScrm user = userService.selectFsUserById(order.getUserId());
+            if (user != null) {
+                item.setNickName(user.getNickName());
+                item.setAvatar(user.getAvatar());
+            }
+        } catch (Exception e) {
+            log.warn("[团购] 查询用户信息失败,跳过昵称头像,userId={}", order.getUserId(), e);
+        }
+
+        item.setPayStatus(1);
+        item.setPayTime(order.getPayTime() == null ? new Date() : order.getPayTime());
+        item.setJoinTime(new Date());
+        item.setDelFlag(0);
+        item.setCreateBy(order.getUserId() == null ? null : String.valueOf(order.getUserId()));
+        item.setCreateTime(new Date());
+        try {
+            groupBuyItemMapper.insertFsStoreProductGroupBuyItem(item);
+            return true;
+        } catch (DuplicateKeyException e) {
+            // 回调重入,item 已存在,视为已处理
+            log.info("[团购] item 已存在,跳过重复插入,groupId={}, userId={}", group.getId(), order.getUserId());
+            return false;
+        }
+    }
+
+    /**
+     * 团号:G + 13位时间戳 + 6位随机串,便于业务侧识别
+     */
+    private String generateGroupNo() {
+        String ts = String.valueOf(System.currentTimeMillis());
+        String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
+        return "G" + ts + suffix;
+    }
+
+    // ==================== 后台查询:列表 / 详情 ====================
+
+    @Override
+    public List<FsStoreGroupBuyListVO> selectGroupBuyListForAdmin(String groupNo,
+                                                                   Long productId,
+                                                                   String productName,
+                                                                   Integer status,
+                                                                   String beginTime,
+                                                                   String endTime) {
+        List<FsStoreGroupBuyListVO> list = groupBuyMapper.selectGroupBuyListForAdmin(
+                null, groupNo, productId, productName, status, beginTime, endTime);
+        fillStatusText(list);
+        return list;
+    }
+
+    @Override
+    public List<FsStoreGroupBuyListVO> selectGroupBuyListByProduct(Long productId) {
+        if (productId == null) {
+            return java.util.Collections.emptyList();
+        }
+        List<FsStoreGroupBuyListVO> list = groupBuyMapper.selectGroupBuyListForAdmin(
+                null, null, productId, null, null, null, null);
+        fillStatusText(list);
+        return list;
+    }
+
+    @Override
+    public FsStoreGroupBuyListVO selectGroupBuyDetailForAdmin(Long id) {
+        if (id == null) {
+            return null;
+        }
+        List<FsStoreGroupBuyListVO> list = groupBuyMapper.selectGroupBuyListForAdmin(
+                id, null, null, null, null, null, null);
+        if (list == null || list.isEmpty()) {
+            return null;
+        }
+        FsStoreGroupBuyListVO vo = list.get(0);
+        fillStatusText(java.util.Collections.singletonList(vo));
+        // 拉团员,包含各自的订单状态
+        List<FsStoreGroupBuyMemberVO> members = groupBuyMapper.selectGroupBuyMembers(id);
+        vo.setMembers(members != null ? members : java.util.Collections.emptyList());
+        return vo;
+    }
+
+    /** 给列表/详情填上状态文本,前端少写 if-else */
+    private void fillStatusText(List<FsStoreGroupBuyListVO> list) {
+        if (list == null) return;
+        for (FsStoreGroupBuyListVO vo : list) {
+            if (vo.getStatus() == null) {
+                vo.setStatusText("");
+                continue;
+            }
+            switch (vo.getStatus()) {
+                case 0: vo.setStatusText("进行中"); break;
+                case 1: vo.setStatusText("拼团成功"); break;
+                case 2: vo.setStatusText("拼团失败"); break;
+                default: vo.setStatusText("未知");
+            }
+        }
+    }
+}

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreCouponIssueVO.java

@@ -43,6 +43,13 @@ public class FsStoreCouponIssueVO implements Serializable
 
     private Integer isDel;
 
+    /**
+     * 适用商品
+     */
+    private String productIds;
+
+    private Integer receiveStatus;
+
     private Integer receiveCount;
 
     private BigDecimal couponPrice;

+ 75 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreGroupBuyListVO.java

@@ -0,0 +1,75 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 团购后台列表/详情 VO
+ * <p>同时服务于列表页(不填 members)和详情页(填 members)。</p>
+ */
+@Data
+public class FsStoreGroupBuyListVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 团购主表ID */
+    private Long id;
+
+    /** 团号 */
+    private String groupNo;
+
+    /** 活动ID */
+    private Long activityId;
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 商品名 */
+    private String productName;
+
+    /** 商品图 */
+    private String productImage;
+
+    /** 活动价(团购价,冗余展示用) */
+    private BigDecimal groupPrice;
+
+    /** 活动原价 */
+    private BigDecimal originalPrice;
+
+    /** 满团人数 */
+    private Integer groupNum;
+
+    /** 已参团人数(含未付款) */
+    private Integer joinNum;
+
+    /** 已付款人数(成团判定依据) */
+    private Integer paidNum;
+
+    /** 团状态 0=进行中 1=拼团完成 2=拼团失败 */
+    private Integer status;
+
+    /** 团状态文本(进行中/拼团完成/拼团失败) */
+    private String statusText;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date completeTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    private String remark;
+
+    /** 团员列表(仅详情接口填充) */
+    private List<FsStoreGroupBuyMemberVO> members;
+}

+ 75 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreGroupBuyMemberVO.java

@@ -0,0 +1,75 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 团购后台详情页 - 团员行 VO。
+ * 关联 fs_store_product_group_buy_item + fs_store_order_scrm,
+ * 把"团员维度"和"订单维度"的关键状态打平在一起返回,方便后台一屏查清楚。
+ */
+@Data
+public class FsStoreGroupBuyMemberVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    // ===== item 侧字段 =====
+    /** item 主键 */
+    private Long itemId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 昵称 */
+    private String nickName;
+
+    /** 头像 */
+    private String avatar;
+
+    /** 选中的规格ID */
+    private Long specId;
+
+    /** 规格名称(sku) */
+    private String specName;
+
+    /** 团员支付状态 0=未支付 1=已支付 2=已退款 */
+    private Integer payStatus;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date joinTime;
+
+    // ===== order 侧字段(fs_store_order_scrm)=====
+    /** 订单ID */
+    private Long orderId;
+
+    /** 订单号 */
+    private String orderCode;
+
+    /**
+     * 订单状态(参考 fs_store_order_scrm.status):
+     * 1=待发货 2=待收货 3=已完成 -1=已取消 -2=已退款 等
+     */
+    private Integer orderStatus;
+
+    /** 退款状态 0/null=未退款 1=申请中 2=已退款 */
+    private Integer refundStatus;
+
+    /** 实付金额 */
+    private BigDecimal payMoney;
+
+    /** 退款金额 */
+    private BigDecimal refundPrice;
+
+    /** 外部订单号(推送ERP后才有值,可用来判断是否已推送) */
+    private String extendOrderId;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date orderCreateTime;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListVO.java

@@ -5,6 +5,7 @@ import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.util.Date;
 @Data
 /**
  * 商品对象 fs_store_product
@@ -115,4 +116,16 @@ public class FsStoreProductListVO  implements Serializable
 
     private String storeId;
     private String storeName;
+
+    /**
+     * 活动类型(0=无活动,6=限时秒杀,7=限时折扣,8=限时团购)
+     * <p>同步于 fs_store_product_scrm.activity_type,给前端列表页判断是否活动中、控制修改/删除按钮是否可见。</p>
+     */
+    private Integer activityType;
+
+    /** 活动开始时间(活动创建时写入商品表,活动进行期间不允许修改商品/删商品) */
+    private Date activityStartTime;
+
+    /** 活动结束时间 */
+    private Date activityEndTime;
 }

+ 11 - 0
fs-service/src/main/java/com/fs/hisStore/vo/ReceiveCouponDto.java

@@ -0,0 +1,11 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+@Data
+public class ReceiveCouponDto {
+
+    private Long userId;
+
+    private Long issueId;
+}

+ 2 - 2
fs-admin/src/main/java/com/fs/kdniaoNew/config/KdniaoUniversalConfig.java → fs-service/src/main/java/com/fs/kdniao/config/KdniaoUniversalConfig.java

@@ -1,6 +1,6 @@
-package com.fs.kdniaoNew.config;
+package com.fs.kdniao.config;
 
-import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniao.domain.KdniaoCarrierConfig;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;

+ 1 - 1
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoAddServiceNew.java → fs-service/src/main/java/com/fs/kdniao/domain/KdniaoAddServiceNew.java

@@ -1,4 +1,4 @@
-package com.fs.kdniaoNew.domain;
+package com.fs.kdniao.domain;
 
 import lombok.Data;
 

+ 1 - 1
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCarrierConfig.java → fs-service/src/main/java/com/fs/kdniao/domain/KdniaoCarrierConfig.java

@@ -1,4 +1,4 @@
-package com.fs.kdniaoNew.domain;
+package com.fs.kdniao.domain;
 
 import lombok.Data;
 

+ 1 - 1
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCommodityNew.java → fs-service/src/main/java/com/fs/kdniao/domain/KdniaoCommodityNew.java

@@ -1,4 +1,4 @@
-package com.fs.kdniaoNew.domain;
+package com.fs.kdniao.domain;
 
 import lombok.Data;
 

+ 1 - 1
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoPersonNew.java → fs-service/src/main/java/com/fs/kdniao/domain/KdniaoPersonNew.java

@@ -1,4 +1,4 @@
-package com.fs.kdniaoNew.domain;
+package com.fs.kdniao.domain;
 
 import lombok.Data;
 

Some files were not shown because too many files changed in this diff