Selaa lähdekoodia

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

caoliqin 2 viikkoa sitten
vanhempi
commit
0c7332cb05
100 muutettua tiedostoa jossa 5360 lisäystä ja 212 poistoa
  1. 7 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java
  2. 24 0
      fs-admin/src/main/java/com/fs/his/controller/FsDoctorController.java
  3. 22 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  4. 1 1
      fs-admin/src/main/java/com/fs/his/task/userIntegralTask.java
  5. 8 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  6. 56 0
      fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java
  7. 9 0
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  8. 1 1
      fs-admin/src/main/java/com/fs/third/controller/WeizouController.java
  9. 7 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java
  10. 8 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  11. 2 0
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  12. 13 4
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  13. 11 0
      fs-service/src/main/java/com/fs/common/service/ISmsService.java
  14. 150 5
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  15. 15 9
      fs-service/src/main/java/com/fs/company/domain/CompanySms.java
  16. 19 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySmsMapper.java
  17. 49 8
      fs-service/src/main/java/com/fs/company/service/ICompanySmsService.java
  18. 164 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySmsServiceImpl.java
  19. 1 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  20. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  21. 3 3
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCommentMapper.java
  22. 3 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  23. 13 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCommentServiceImpl.java
  24. 1 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseCommentListUVO.java
  25. 90 0
      fs-service/src/main/java/com/fs/fastgptApi/param/ChatImgParam.java
  26. 2 0
      fs-service/src/main/java/com/fs/fastgptApi/service/ChatService.java
  27. 90 0
      fs-service/src/main/java/com/fs/fastgptApi/service/Impl/ChatServiceImpl.java
  28. 17 7
      fs-service/src/main/java/com/fs/his/domain/FsIntegralCount.java
  29. 26 0
      fs-service/src/main/java/com/fs/his/mapper/FsDoctorMapper.java
  30. 3 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java
  31. 1 2
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java
  32. 50 0
      fs-service/src/main/java/com/fs/his/param/DoctorPrescriptionImgLogParam.java
  33. 75 0
      fs-service/src/main/java/com/fs/his/param/DoctorPrescriptionParam.java
  34. 27 3
      fs-service/src/main/java/com/fs/his/service/IFsDoctorService.java
  35. 4 0
      fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java
  36. 684 3
      fs-service/src/main/java/com/fs/his/service/impl/FsDoctorServiceImpl.java
  37. 428 43
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  38. 3 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsVo.java
  39. 1 0
      fs-service/src/main/java/com/fs/his/vo/OptionsVO.java
  40. 8 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  41. 2 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  42. 28 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  43. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/FsMyStoreOrderQueryParam.java
  44. 1 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreAfterSalesQueryParam.java
  45. 3 1
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  46. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  47. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  48. 47 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  49. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsMyStoreOrderListQueryVO.java
  50. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesQueryVO.java
  51. 7 0
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  52. 147 99
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  53. 33 0
      fs-service/src/main/java/com/fs/qw/bo/SendMsgLogBo.java
  54. 77 0
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionLinkInfo.java
  55. 55 0
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionSendMsgLog.java
  56. 64 0
      fs-service/src/main/java/com/fs/qw/domain/QwCourseLinkSendMsgLog.java
  57. 32 0
      fs-service/src/main/java/com/fs/qw/dto/BatchAddAcquisitionLinkDTO.java
  58. 45 0
      fs-service/src/main/java/com/fs/qw/dto/IpadBlindAddDto.java
  59. 33 0
      fs-service/src/main/java/com/fs/qw/enums/SmsLogType.java
  60. 91 0
      fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionLinkInfoMapper.java
  61. 62 0
      fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionSendMsgLogMapper.java
  62. 62 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCourseLinkSendMsgLogMapper.java
  63. 1 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  64. 8 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  65. 26 0
      fs-service/src/main/java/com/fs/qw/service/ICorporateWeChatSpaceService.java
  66. 106 0
      fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionLinkInfoService.java
  67. 8 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  68. 158 0
      fs-service/src/main/java/com/fs/qw/service/impl/ICorporateWeChatSpaceServiceImpl.java
  69. 823 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionLinkInfoServiceImpl.java
  70. 11 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  71. 19 0
      fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategy.java
  72. 54 0
      fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategyManager.java
  73. 35 0
      fs-service/src/main/java/com/fs/qw/strategy/impl/AcquisitionLinkLogStrategyImpl.java
  74. 38 0
      fs-service/src/main/java/com/fs/qw/strategy/impl/CourseLinkLogStrategyImpl.java
  75. 18 0
      fs-service/src/main/java/com/fs/qw/strategy/impl/NoOpSmsLogStrategy.java
  76. 61 0
      fs-service/src/main/java/com/fs/qw/utils/WeChatTokenUtil.java
  77. 38 0
      fs-service/src/main/java/com/fs/qw/utils/WeComSignatureUtil.java
  78. 4 0
      fs-service/src/main/java/com/fs/qw/vo/QwExternalContactVO.java
  79. 27 0
      fs-service/src/main/java/com/fs/qw/vo/QwSessionConfigVo.java
  80. 22 0
      fs-service/src/main/java/com/fs/qwApi/domain/SpecProgramCallRequest.java
  81. 27 0
      fs-service/src/main/java/com/fs/qwApi/domain/SpecProgramCallResponse.java
  82. 22 0
      fs-service/src/main/java/com/fs/qwApi/domain/SpecProgramResultResponse.java
  83. 5 0
      fs-service/src/main/java/com/fs/system/service/ISysConfigService.java
  84. 19 0
      fs-service/src/main/java/com/fs/system/service/impl/SysConfigServiceImpl.java
  85. 11 0
      fs-service/src/main/resources/application-config-dev.yml
  86. 1 1
      fs-service/src/main/resources/application-config-druid-sxsm.yml
  87. 51 6
      fs-service/src/main/resources/mapper/company/CompanySmsMapper.xml
  88. 1 1
      fs-service/src/main/resources/mapper/company/CompanyWithdrawDetailMapper.xml
  89. 359 0
      fs-service/src/main/resources/mapper/his/FsDoctorMapper.xml
  90. 2 0
      fs-service/src/main/resources/mapper/his/FsIntegralCountMapper.xml
  91. 52 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreAfterSalesScrmMapper.xml
  92. 6 3
      fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  93. 1 1
      fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml
  94. 213 0
      fs-service/src/main/resources/mapper/qw/QwAcquisitionLinkInfoMapper.xml
  95. 105 0
      fs-service/src/main/resources/mapper/qw/QwAcquisitionSendMsgLogMapper.xml
  96. 115 0
      fs-service/src/main/resources/mapper/qw/QwCourseLinkSendMsgLogMapper.xml
  97. 1 1
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml
  98. 5 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml
  99. 12 0
      fs-spec-zone/Dockerfile
  100. BIN
      fs-spec-zone/libWeWorkSpecSDK.so

+ 7 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java

@@ -211,6 +211,13 @@ public class FsCourseRedPacketLogController extends BaseController
         return R.ok().put("list", optionsVOS);
     }
 
+    @GetMapping(value = "/videoListByWatch/{id}")
+    public R videoListByWatch(@PathVariable("id") Long id)
+    {
+        List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllListByWatch(id);
+        return R.ok().put("list", optionsVOS);
+    }
+
 
     /**
     * 红包消耗统计

+ 24 - 0
fs-admin/src/main/java/com/fs/his/controller/FsDoctorController.java

@@ -63,6 +63,30 @@ public class FsDoctorController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 处方图片列表审核
+     * @param param
+     * @return
+     */
+    @PostMapping("/prescriptionList")
+    public TableDataInfo prescriptionList(@RequestBody DoctorPrescriptionImgLogParam param)
+    {
+        startPage();
+        List<DoctorPrescriptionImgLogParam> doctorList = fsDoctorService.selectPrescriptionLogList(param);
+        return getDataTable(doctorList);
+    }
+
+    /**
+     * 处方图片审核
+     * @param param
+     * @return
+     */
+    @PostMapping("/auditPrescription")
+    public AjaxResult auditPrescription(@RequestBody DoctorPrescriptionParam param)
+    {
+        return toAjax(fsDoctorService.updateFsDoctorPrescription(param));
+    }
+
     /**
      * 导出医生管理列表
      */

+ 22 - 0
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -53,6 +53,7 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.FsSubOrderResultVO;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStorePaymentScrmService;
@@ -239,6 +240,8 @@ public class Task {
 
     @Autowired
     private FsUserCompanyUserMapper fsUserCompanyUserMapper;
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
 
     /**
      * 定时任务,处理ai禁止回复之后的消息
@@ -924,7 +927,12 @@ public class Task {
 
     public void CreateWeizouErpPush() {
         List<Long> omsList = fsStoreOrderMapper.selectFsStoreOrderNoCreateOms();
+        List<Long> integrals = fsIntegralOrderMapper.selectFsStoreOrderNoCreateOms();
+        List<Long> scrms = fsStoreOrderScrmMapper.selectFsStoreOrderNoCreateOms();
         logger.info("推送订单id====>{}", omsList);
+        logger.info("推送积分订单id====>{}", integrals);
+        logger.info("推送SCRM订单id====>{}", scrms);
+
         for (Long l : omsList) {
             try {
                 fsStoreOrderService.weizouPush(l);
@@ -932,6 +940,20 @@ public class Task {
                 logger.error("推送订单异常:", e);
             }
         }
+        for (Long l : integrals) {
+            try {
+                fsStoreOrderService.weizouPushIntergral(l);
+            } catch (Exception e) {
+                logger.error("推送积分订单异常:", e);
+            }
+        }
+        for (Long l : scrms) {
+            try {
+                fsStoreOrderService.weizouPushScrm(l);
+            } catch (Exception e) {
+                logger.error("推送SCRM订单异常:", e);
+            }
+        }
     }
     public void CreateOmsAndHis() {
         List<Long> omsList = fsStoreOrderMapper.selectFsStoreOrderNoCreateOms();

+ 1 - 1
fs-admin/src/main/java/com/fs/his/task/userIntegralTask.java

@@ -22,7 +22,7 @@ public class userIntegralTask {
     @Autowired
     private IFsIntegralCountService integralCountService;
 
-    public void UserIntegralCount(){
+    public void userIntegralCount(){
         log.info("=====用户积分每日消耗统计开始=====");
         try {
             List<FsIntegralCount> list = integralLogsService.selectYesterdayIntegralGroupByLogType();

+ 8 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -762,10 +762,17 @@ public class FsStoreOrderScrmController extends BaseController {
                 if("恒春来".equals(cloudHostProper.getCompanyName())
                         && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
                     if (lastFourNumber.contains("-")) {
-                        lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                        String beforeDash = lastFourNumber.split("-")[0];
+                        lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
                     }else{
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
+                    expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    if(expressInfoDTO!=null && !expressInfoDTO.isSuccess()){
+                        lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
+                        expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    }
+                    return R.ok().put("data",expressInfoDTO);
                 }
                 // 原逻辑
                 else if ((lastFourNumber = order.getUserPhone()).length() == 11) {

+ 56 - 0
fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java

@@ -0,0 +1,56 @@
+package com.fs.qw.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.exception.CustomException;
+import com.fs.qw.service.ICorporateWeChatSpaceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 企业微信专区-统一前端 API 接口
+ * */
+@RestController
+@RequestMapping("/weChatSpace")
+@RequiredArgsConstructor
+public class CorporateWeChatSpaceController extends BaseController {
+
+    private final ICorporateWeChatSpaceService weChatSpaceService;
+
+    // 企业微信会话专区中转接口
+    @GetMapping("/conversations")
+    public JSONObject getConversations(
+            @RequestParam(defaultValue = "0") long seq,
+            @RequestParam(defaultValue = "100") long limit,
+            @RequestParam(defaultValue = "0") long proxy,
+            @RequestParam(defaultValue = "30") long timeout,
+            @RequestParam(required = false) String customerId,
+            @RequestParam(required = false) String staffUserId) throws Exception {
+        if (customerId == null|| customerId.isEmpty()) {
+            throw new CustomException("客户id不能为空");
+        }else if (staffUserId == null|| staffUserId.isEmpty()) {
+            throw new CustomException("员工id不能为空");
+        }
+        return weChatSpaceService.fetchConversations(seq, limit, proxy, timeout, customerId,staffUserId);
+    }
+
+
+    // agentConfig 签名
+    @GetMapping("/getAgentConfigSignature")
+    public JSONObject getAgentConfigSignature(@RequestParam("url") String url) {
+        return weChatSpaceService.getAgentConfigSignature(url);
+    }
+
+    // Web 登录
+    @PostMapping("/login")
+    public JSONObject login(@RequestBody JSONObject param) {
+        return weChatSpaceService.login(param.getString("code"));
+    }
+
+    //获取企业微信专区会话配置
+    @GetMapping("/getQwSessionConfig")
+    public AjaxResult getQwSessionConfig() {
+        return AjaxResult.success(weChatSpaceService.getQwSessionConfig());
+    }
+}

+ 9 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -157,6 +157,15 @@ public class QwUserController extends BaseController {
         return getDataTable(list);
     }
 
+    /**
+     * 查询所有企微员工列表
+     * */
+    @PreAuthorize("@ss.hasPermi('qw:user:list')")
+    @GetMapping("/listAllQwUserList")
+    public AjaxResult listAllQwUserList(QwUser qwUser) {
+        List<QwUser> list = qwUserService.selectNotDelQwUserList(qwUser);
+        return AjaxResult.success(list);
+    }
 
     /**
      * 导出企微员工列表

+ 1 - 1
fs-admin/src/main/java/com/fs/third/controller/WeizouController.java

@@ -33,7 +33,7 @@ public class WeizouController extends BaseController {
         if (map.get("orderCode") == null) {
             return error("参数异常请核验参数");
         }
-        fsStoreOrder.setOrderId(Long.valueOf((String) map.get("orderCode")));
+        fsStoreOrder.setOrderCode((String) map.get("orderCode"));
         fsStoreOrder.setOperator("-1");
 // 校验物流单号
         if (map.get("deliverySn") == null) {

+ 7 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java

@@ -267,4 +267,11 @@ public class FsCourseRedPacketLogController extends BaseController
         List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllList(id);
         return R.ok().put("list", optionsVOS);
     }
+
+    @GetMapping(value = "/videoListByWatch/{id}")
+    public R videoListByWatch(@PathVariable("id") Long id)
+    {
+        List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllListByWatch(id);
+        return R.ok().put("list", optionsVOS);
+    }
 }

+ 8 - 1
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -435,10 +435,17 @@ public class FsStoreOrderScrmController extends BaseController
                 if("恒春来".equals(cloudHostProper.getCompanyName())
                         && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
                     if (lastFourNumber.contains("-")) {
-                        lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                        String beforeDash = lastFourNumber.split("-")[0];
+                        lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
                     }else{
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
+                    expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    if(expressInfoDTO!=null && !expressInfoDTO.isSuccess()){
+                        lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
+                        expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    }
+                    return R.ok().put("data",expressInfoDTO);
                 }
                 // 原逻辑
                 else if ((lastFourNumber = order.getUserPhone()).length() == 11) {

+ 2 - 0
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -351,6 +351,8 @@ public class Task {
                     lotteryVo.setPrizeLevel(liveLotteryProductListVo.getPrizeLevel());
                     lotteryVo.setProductName(liveLotteryProductListVo.getProductName());
                     lotteryVo.setProductId(liveLotteryProductListVo.getProductId());
+                    //设置中奖记录id
+                    lotteryVo.setRecordId(record.getId());
                     lotteryVos.add(lotteryVo);
                 }
             }

+ 13 - 4
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -341,14 +341,23 @@ public class QwMsgController {
                         WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
                         ste.setMsgid(wxWorkMessageDTO.getMsg_id());
                         ste.setUuid(wxWorkMsgResp.getUuid());
+
                         WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO> dto = wxWorkService.SpeechToTextEntity(ste, serverId);
                         System.out.println(dto);
-                        if(dto.getErrcode() != 0 || Objects.isNull(dto.getData()) || StringUtils.isBlank(dto.getData().getText())){
+
+                        int maxRetries = 3;
+                        int retryCount = 0;
+
+                        while((dto.getErrcode() != 0 || Objects.isNull(dto.getData()) || StringUtils.isBlank(dto.getData().getText()))
+                                && retryCount < maxRetries) {
                             try {
-                                TimeUnit.SECONDS.sleep(2); // 阻塞2秒
+                                TimeUnit.SECONDS.sleep(1);
+                                retryCount++;
+                                log.info("id:{}, 语音转换第{}次重试", id, retryCount);
                             } catch (InterruptedException e) {
-                                Thread.currentThread().interrupt(); // 处理中断异常
-                                log.info("id:{}, 第一次语音转换失败", id);
+                                Thread.currentThread().interrupt();
+                                log.info("id:{}, 语音转换等待被中断", id);
+                                break;
                             }
                             dto = wxWorkService.SpeechToTextEntity(ste, serverId);
                         }

+ 11 - 0
fs-service/src/main/java/com/fs/common/service/ISmsService.java

@@ -1,9 +1,12 @@
 package com.fs.common.service;
 
 import com.fs.common.core.domain.R;
+import com.fs.company.domain.CompanySmsTemp;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.param.SmsSendParam;
 import com.fs.crm.param.SmsSendUserParam;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.enums.SmsLogType;
 
 
 public interface ISmsService
@@ -26,4 +29,12 @@ public interface ISmsService
 
     R sendUrl(String phone, String content, String code,Long uuid,Integer smsIndex,String deleteKey,Long companyId,Long companyUserId);
 
+    /**
+     *  根据号码、内容、模板发送短信(简洁版)
+     *  @param phone 号码
+     *  @param content 内容
+     *  @param temp 模板
+     * */
+    R simpleSmsSend(String phone, String content, CompanySmsTemp temp, SmsLogType logType, SendMsgLogBo sendMsgLogBo);
+
 }

+ 150 - 5
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -9,6 +9,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 
 import com.fs.common.service.ISmsService;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsSendItemVO;
@@ -23,6 +24,7 @@ import com.fs.company.service.ICompanySmsTempService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.impl.CompanyVoiceRoboticCallBlacklistServiceImpl;
 import com.fs.company.vo.CompanyVoiceRoboticCallBlacklistCheckVO;
+import com.fs.course.config.CourseConfig;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.param.SmsSendParam;
@@ -34,15 +36,22 @@ import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.mapper.FsPackageOrderMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.vo.FsPackageOrderVO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
 import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.mapper.QwAcquisitionLinkInfoMapper;
 import com.fs.qw.mapper.QwSopSmsLogsMapper;
 import com.fs.qw.service.IQwSopSmsLogsService;
+import com.fs.qw.strategy.SmsLogStrategyManager;
 import com.fs.sms.domain.SendSmsReturn;
 import com.fs.sms.service.impl.SmsTServiceImpl;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
+import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.google.gson.Gson;
 import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
@@ -61,6 +70,8 @@ import com.fs.company.service.ICompanySmsCommonLogsService;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
 @Service
 @Slf4j
 public class SmsServiceImpl implements ISmsService
@@ -109,6 +120,20 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private RedissonClient redissonClient;
 
+    //获客链接短信模板code
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
+
+    @Autowired
+    private QwAcquisitionLinkInfoMapper qwAcquisitionLinkInfoMapper;
+
+    @Autowired
+    private SmsLogStrategyManager smsLogStrategyManager;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    private static final String  LINK_SUFFIX = "?customer_channel=up:";
+
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -785,7 +810,88 @@ public class SmsServiceImpl implements ISmsService
         return R.ok();
     }
 
+    /**
+     * 发送简单短信
+     * @param phone 接收方手机号
+     * @param content 短信内容
+     * @param temp 短信模板
+     * @param logType 日志记录类型,用于区分调用方
+     * @param sendMsgLogBo 特定业务的上下文对象,如qwAcquisitionId或externalContactId
+     * @return R 响应结果
+     */
+    @Override
+    public R simpleSmsSend(String phone, String content, CompanySmsTemp temp, SmsLogType logType, SendMsgLogBo sendMsgLogBo) {
+        String urls = null;
+        R response; // 存储最终响应
+        Integer number = calculateSmsCount(content);
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
+        FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
+
+        try {
+            urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + phone + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+        } catch (UnsupportedEncodingException e) {
+            log.error("{}发送失败", phone, e);
+            response = R.error("短信发送失败:" + e.getMessage());
+            // 发送失败也要记录特定业务日志
+            smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+            return response;
+        }
+
+        String post = HttpRequest.get(urls).execute().body();
+        SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+
+        if (vo.getStatus().equals(0)) {
+            boolean anySuccess = false;
+            for (SmsSendItemVO itemVO : vo.getList()) {
+                if (itemVO.getResult().equals("0")) {
+                    anySuccess = true;
+                    // 记录通用日志
+                    CompanySmsLogs logs = new CompanySmsLogs();
+                    logs.setContent(content);
+                    logs.setTempCode(temp.getTempCode());
+                    logs.setTempId(temp.getTempId());
+                    logs.setPhone(phone);
+                    logs.setSendTime(new Date());
+                    logs.setStatus(0);
+                    logs.setType(sms.getType());
+                    logs.setMid(itemVO.getMid());
+                    logs.setNumber(number);
+                    logs.setCompanyId(sendMsgLogBo.getCompanyId());
+                    logs.setCompanyUserId(sendMsgLogBo.getCompanyUserId());
+                    logs.setCustomerId(sendMsgLogBo.getCustomerId());
+                    smsLogsService.insertCompanySmsLogs(logs);
+                    //子记录表关联主表的id
+                    sendMsgLogBo.setCompanySmsLogsId(logs.getLogsId());
+                }
+            }
+            if(anySuccess) {
+                response = R.ok();
+                // 记录特定业务日志
+                smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+            } else {
+                response = R.error("发送短信失败,服务商返回无成功项!");
+                smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+            }
+        } else {
+            response = R.error("发送短信失败!状态码: " + vo.getStatus());
+            // 发送失败也要记录特定业务日志
+            smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+        }
+        return response;
+    }
 
+    // 将计算短信条数的逻辑提取出来,方便复用
+    private int calculateSmsCount(String content) {
+        if (content == null) return 1;
+        int counts = content.length() / 67;
+        if (content.length() % 67 > 0) {
+            counts = counts + 1;
+        }
+        if (counts == 0) {
+            counts = 1;
+        }
+        return counts;
+    }
 
     @Override
     @Synchronized
@@ -965,6 +1071,19 @@ public class SmsServiceImpl implements ISmsService
                 content=content.replace("${sms.senderName}",param.getSenderName());
             }
 
+            if (param.getTempCode()!=null &&SMS_LINK_TEMPLATE_CODE.equals(temp.getTempCode())){
+
+                String json = configService.selectConfigByKey("course.config");
+                CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+
+                String randomStr = ShortCodeGeneratorUtils.generate8();
+                String replaceText=config.getSmsAcquisitionName()+randomStr;
+                content = content.replace("${sms.friendLink}",replaceText);
+                //添加获客链接记录
+                addAcquisitionLinkInfo(null,crmCustomer.getMobile(),param.getCardUrl(),randomStr, param.getCompanyUserId());
+            }
+
+
             String urls= null;
             // 通知类的不加 退订回T 只有营销类的加
             //最多500个手机号
@@ -972,12 +1091,18 @@ public class SmsServiceImpl implements ISmsService
             FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
             if (sms.getType().equals("rf")){
                 try {
-                    if(temp.getTempType().equals(1)){
-                        urls = sms.getRfUrl1()+"sms?action=send&account="+sms.getRfAccount1()+"&password="+sms.getRfPassword1()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content, "UTF-8")+"&extno="+sms.getRfCode1()+"&rt=json";
-                    }
-                    else if(temp.getTempType().equals(2)){
-                        urls = sms.getRfUrl2()+"sms?action=send&account="+sms.getRfAccount2()+"&password="+sms.getRfPassword2()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content+"拒收请回复R", "UTF-8")+"&extno="+sms.getRfCode2()+"&rt=json";
+
+                    if (temp.getTempType().equals(2) && SMS_LINK_TEMPLATE_CODE.equals(temp.getTempCode())){
+                        urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + crmCustomer.getMobile() + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+                    } else {
+                        if(temp.getTempType().equals(1)){
+                            urls = sms.getRfUrl1()+"sms?action=send&account="+sms.getRfAccount1()+"&password="+sms.getRfPassword1()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content, "UTF-8")+"&extno="+sms.getRfCode1()+"&rt=json";
+                        }
+                        else if(temp.getTempType().equals(2)){
+                            urls = sms.getRfUrl2()+"sms?action=send&account="+sms.getRfAccount2()+"&password="+sms.getRfPassword2()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content+"拒收请回复R", "UTF-8")+"&extno="+sms.getRfCode2()+"&rt=json";
+                        }
                     }
+
                 } catch (UnsupportedEncodingException e) {
                     e.printStackTrace();
                 }
@@ -1074,6 +1199,26 @@ public class SmsServiceImpl implements ISmsService
         }
     }
 
+
+
+    /**
+     * 添加链接生成记录
+     * */
+    public int addAcquisitionLinkInfo(Long qwAcquisitionAssistantId,String originalPhone,String originalLink,String randomStr,Long createBy){
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setCreateBy(createBy);
+        qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
+        qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
+        //加密手机号
+        String phonePlus = encryptPhone(originalPhone);
+        String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        return addResult;
+    }
+
     /**
      * 根据 uuid(QwSopSmsLogs.id)和索引更新子记录,并检查主记录完成状态
      */

+ 15 - 9
fs-service/src/main/java/com/fs/company/domain/CompanySms.java

@@ -7,7 +7,7 @@ import org.apache.commons.lang3.builder.ToStringStyle;
 
 /**
  * 公司短信对象 company_sms
- * 
+ *
  * @author fs
  * @date 2023-01-09
  */
@@ -30,43 +30,49 @@ public class CompanySms extends BaseEntity
     @Excel(name = "总短信数")
     private Long totalSmsCount;
 
-    public void setSmsId(Long smsId) 
+    /** 乐观锁版本号 */
+    private Long version;
+
+    public void setSmsId(Long smsId)
     {
         this.smsId = smsId;
     }
 
-    public Long getSmsId() 
+    public Long getSmsId()
     {
         return smsId;
     }
-    public void setCompanyId(Long companyId) 
+    public void setCompanyId(Long companyId)
     {
         this.companyId = companyId;
     }
 
-    public Long getCompanyId() 
+    public Long getCompanyId()
     {
         return companyId;
     }
-    public void setRemainSmsCount(Long remainSmsCount) 
+    public void setRemainSmsCount(Long remainSmsCount)
     {
         this.remainSmsCount = remainSmsCount;
     }
 
-    public Long getRemainSmsCount() 
+    public Long getRemainSmsCount()
     {
         return remainSmsCount;
     }
-    public void setTotalSmsCount(Long totalSmsCount) 
+    public void setTotalSmsCount(Long totalSmsCount)
     {
         this.totalSmsCount = totalSmsCount;
     }
 
-    public Long getTotalSmsCount() 
+    public Long getTotalSmsCount()
     {
         return totalSmsCount;
     }
 
+    public Long getVersion() {
+        return version;
+    }
     @Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

+ 19 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanySmsMapper.java

@@ -90,4 +90,23 @@ public interface CompanySmsMapper {
     Long debitSmsCount(Long companyId);
 
 
+    // ========== 扣减/增加方法 ==========
+    /** 直接扣减(配合悲观锁) */
+    int decrementRemainSmsCount(@Param("companyId") Long companyId,
+                                @Param("number") int number);
+
+    /** 直接增加(配合悲观锁) */
+    int incrementRemainSmsCount(@Param("companyId") Long companyId,
+                                @Param("number") int number);
+
+    /** 乐观锁扣减 */
+    int decrementRemainSmsCountWithVersion(@Param("companyId") Long companyId,
+                                           @Param("number") int number,
+                                           @Param("currentVersion") Long currentVersion);
+
+    /** 乐观锁增加 */
+    int incrementRemainSmsCountWithVersion(@Param("companyId") Long companyId,
+                                           @Param("number") int number,
+                                           @Param("currentVersion") Long currentVersion);
+
 }

+ 49 - 8
fs-service/src/main/java/com/fs/company/service/ICompanySmsService.java

@@ -6,15 +6,15 @@ import com.fs.company.vo.CompanySmsListVO;
 
 /**
  * 公司短信Service接口
- * 
+ *
  * @author fs
  * @date 2023-01-09
  */
-public interface ICompanySmsService 
+public interface ICompanySmsService
 {
     /**
      * 查询公司短信
-     * 
+     *
      * @param smsId 公司短信ID
      * @return 公司短信
      */
@@ -22,7 +22,7 @@ public interface ICompanySmsService
 
     /**
      * 查询公司短信列表
-     * 
+     *
      * @param companySms 公司短信
      * @return 公司短信集合
      */
@@ -30,7 +30,7 @@ public interface ICompanySmsService
 
     /**
      * 新增公司短信
-     * 
+     *
      * @param companySms 公司短信
      * @return 结果
      */
@@ -38,7 +38,7 @@ public interface ICompanySmsService
 
     /**
      * 修改公司短信
-     * 
+     *
      * @param companySms 公司短信
      * @return 结果
      */
@@ -46,7 +46,7 @@ public interface ICompanySmsService
 
     /**
      * 批量删除公司短信
-     * 
+     *
      * @param smsIds 需要删除的公司短信ID
      * @return 结果
      */
@@ -54,7 +54,7 @@ public interface ICompanySmsService
 
     /**
      * 删除公司短信信息
-     * 
+     *
      * @param smsId 公司短信ID
      * @return 结果
      */
@@ -68,4 +68,45 @@ public interface ICompanySmsService
     int addCompanySms(Long companyId, int number);
 
     CompanySms selectCompanySmsByCompanyIdForUpdate(Long companyId);
+
+
+    // ========== 查询方法 ==========
+    /** 普通查询(不带锁) */
+    CompanySms selectCompanySms(Long companyId);
+
+    /** 悲观锁查询(for update) */
+    CompanySms selectCompanySmsForUpdate(Long companyId);
+
+    // ========== 扣减/增加方法 ==========
+    /** 直接扣减(配合悲观锁使用) */
+    int decrementRemainSmsCount(Long companyId, int number);
+
+    /** 直接增加(配合悲观锁使用) */
+    int incrementRemainSmsCount(Long companyId, int number);
+
+    /** 乐观锁扣减(配合重试机制使用) */
+    int decrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion);
+
+    /** 乐观锁增加(配合重试机制使用) */
+    int incrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion);
+
+    // ========== 缓存方法 ==========
+    /** 获取余额(优先从缓存获取) */
+    Long getBalance(Long companyId);
+
+    /** 删除缓存(数据库变更后调用) */
+    void evictBalance(Long companyId);
+
+    /**
+     * 更新缓存中的余额(数据库更新后调用)
+     */
+    void updateCacheBalance(Long companyId, int delta);
+
+    /**
+     * 充值短信条数(带缓存更新)
+     * @param companyId 公司ID
+     * @param number 充值条数
+     * @return 是否成功
+     */
+    boolean rechargeSms(Long companyId, int number);
 }

+ 164 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanySmsServiceImpl.java

@@ -1,13 +1,17 @@
 package com.fs.company.service.impl;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
+import com.fs.common.core.redis.RedisCache;
 import com.fs.company.vo.CompanySmsListVO;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.company.mapper.CompanySmsMapper;
 import com.fs.company.domain.CompanySms;
 import com.fs.company.service.ICompanySmsService;
+import org.springframework.transaction.annotation.Transactional;
 
 /**
  * 公司短信Service业务层处理
@@ -15,12 +19,23 @@ import com.fs.company.service.ICompanySmsService;
  * @author fs
  * @date 2023-01-09
  */
+@Slf4j
 @Service
 public class CompanySmsServiceImpl implements ICompanySmsService
 {
     @Autowired
     private CompanySmsMapper companySmsMapper;
 
+    @Autowired
+    private RedisCache redisCache;
+
+    // 公司短信余额缓存key前缀
+    private static final String BALANCE_KEY_PREFIX = "sms:balance:";
+    // 缓存有效期
+    private static final int CACHE_EXPIRE_SECONDS = 18000; // 300分钟
+    // 最大重试次数
+    private static final int MAX_RETRY_TIMES = 3;
+
     /**
      * 查询公司短信
      *
@@ -119,4 +134,153 @@ public class CompanySmsServiceImpl implements ICompanySmsService
     public CompanySms selectCompanySmsByCompanyIdForUpdate(Long companyId) {
         return companySmsMapper.selectCompanySmsByCompanyIdForUpdate(companyId);
     }
+
+    // ========== 查询方法 ==========
+    @Override
+    public CompanySms selectCompanySms(Long companyId) {
+        return companySmsMapper.selectCompanySmsByCompanyId(companyId);
+    }
+
+    @Override
+    public CompanySms selectCompanySmsForUpdate(Long companyId) {
+        return companySmsMapper.selectCompanySmsByCompanyIdForUpdate(companyId);
+    }
+
+    // ========== 扣减/增加方法 ==========
+    @Override
+    public int decrementRemainSmsCount(Long companyId, int number) {
+        return companySmsMapper.decrementRemainSmsCount(companyId, number);
+    }
+
+    @Override
+    public int incrementRemainSmsCount(Long companyId, int number) {
+        return companySmsMapper.incrementRemainSmsCount(companyId, number);
+    }
+
+    @Override
+    public int decrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion) {
+        return companySmsMapper.decrementRemainSmsCountWithVersion(companyId, number, currentVersion);
+    }
+
+    @Override
+    public int incrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion) {
+        return companySmsMapper.incrementRemainSmsCountWithVersion(companyId, number, currentVersion);
+    }
+
+    // ========== 缓存方法 ==========
+    @Override
+    public Long getBalance(Long companyId) {
+        String cacheKey = BALANCE_KEY_PREFIX + companyId;
+        try {
+            Long balance = redisCache.getCacheObject(cacheKey);
+            if (balance != null) {
+                log.debug("从缓存获取余额成功, companyId={}, balance={}", companyId, balance);
+                return balance;
+            }
+            CompanySms companySms = companySmsMapper.selectCompanySmsByCompanyId(companyId);
+            if (companySms == null) {
+                return null;
+            }
+            Long realBalance = companySms.getRemainSmsCount();
+            redisCache.setCacheObject(cacheKey, realBalance, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
+            log.debug("从数据库加载余额并写入缓存, companyId={}, balance={}", companyId, realBalance);
+            return realBalance;
+        } catch (Exception e) {
+            log.error("获取余额缓存失败, companyId={}", companyId, e);
+            CompanySms companySms = companySmsMapper.selectCompanySmsByCompanyId(companyId);
+            return companySms != null ? companySms.getRemainSmsCount() : null;
+        }
+    }
+
+    @Override
+    public void updateCacheBalance(Long companyId, int delta) {
+        String cacheKey = BALANCE_KEY_PREFIX + companyId;
+        String lockKey = "sms:balance:lock:" + companyId;
+
+        try {
+            Boolean locked = redisCache.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
+
+            if (Boolean.TRUE.equals(locked)) {
+                try {
+                    Long currentBalance = redisCache.getCacheObject(cacheKey);
+                    if (currentBalance != null) {
+                        Long newBalance = currentBalance + delta;
+                        redisCache.setCacheObject(cacheKey, newBalance, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
+                        log.debug("更新缓存余额成功, companyId={}, old={}, new={}, delta={}",
+                                companyId, currentBalance, newBalance, delta);
+                    } else {
+                        log.debug("缓存不存在,跳过更新, companyId={}", companyId);
+                    }
+                } finally {
+                    redisCache.deleteObject(lockKey);
+                }
+            } else {
+                log.warn("获取缓存锁失败,删除缓存, companyId={}", companyId);
+                redisCache.deleteObject(cacheKey);
+            }
+        } catch (Exception e) {
+            log.error("更新缓存余额失败, companyId={}, delta={}", companyId, delta, e);
+            redisCache.deleteObject(cacheKey);
+        }
+    }
+
+    @Override
+    public void evictBalance(Long companyId) {
+        String cacheKey = BALANCE_KEY_PREFIX + companyId;
+        try {
+            redisCache.deleteObject(cacheKey);
+            log.debug("删除余额缓存成功, companyId={}", companyId);
+        } catch (Exception e) {
+            log.error("删除余额缓存失败, companyId={}", companyId, e);
+        }
+    }
+
+    // ========== 充值方法 ==========
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean rechargeSms(Long companyId, int number) {
+        if (number <= 0) {
+            log.warn("充值条数无效, companyId={}, number={}", companyId, number);
+            return false;
+        }
+
+        int maxRetries = 3;
+
+        for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
+            // 获取最新数据(含version)
+            CompanySms latestSms = selectCompanySms(companyId);
+            if (latestSms == null) {
+                log.error("充值失败,公司短信配置不存在, companyId={}", companyId);
+                return false;
+            }
+
+            Long version = latestSms.getVersion() != null ? latestSms.getVersion() : 0L;
+            int updateCount = companySmsMapper.incrementRemainSmsCountWithVersion(companyId, number, version);
+
+            if (updateCount > 0) {
+                // 充值成功,更新缓存
+                updateCacheBalance(companyId, number);
+                log.info("充值成功, companyId={}, number={}, newBalance={}",
+                        companyId, number, latestSms.getRemainSmsCount() + number);
+                return true;
+            }
+
+            log.warn("乐观锁充值失败,第{}次重试, companyId={}", retryCount + 1, companyId);
+
+            if (retryCount == maxRetries - 1) {
+                log.error("充值失败,重试次数用尽, companyId={}, number={}", companyId, number);
+                return false;
+            }
+
+            try {
+                Thread.sleep(50L * (retryCount + 1));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("充值被中断, companyId={}", companyId);
+                return false;
+            }
+        }
+
+        return false;
+    }
 }

+ 1 - 0
fs-service/src/main/java/com/fs/course/config/CourseConfig.java

@@ -28,6 +28,7 @@ public class CourseConfig implements Serializable {
     private String realLinkH5LiveName;//H5通用直播域名
     private String authDomainName;//网页授权域名
     private String smsDomainName;//短信推送域名
+    private String smsAcquisitionName;//短信推送域名
     private String smsDomain;//短信推送域名
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名

+ 1 - 1
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -940,7 +940,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             "WHERE user_id = #{userId} " +
             "AND duration > 0 " +
             "AND (send_type = 1 OR send_type = 2) " +
-            "ORDER BY create_time DESC " +
+            "ORDER BY update_time DESC " +
             "LIMIT 1")
     FsCourseWatchLog selectLatestWatchLogByUserId(@Param("userId") Long userId);
 }

+ 3 - 3
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCommentMapper.java

@@ -88,7 +88,7 @@ public interface FsUserCourseCommentMapper
     @Select("select c.*,u.nick_name,u.phone,u,avatar from fs_user_course_comment c LEFT JOIN fs_user u ON c.user_id=u.user_id where parent_id=#{pid}")
     List<FsUserCourseCommentVO> selectFsUserCourseCommentAllListVO(Long pid);
     @Select({"<script> " +
-            "select c.*,u.nick_name,u.avatar,tu.nick_name to_nick_name, IFNULL(l.id, 0) AS is_like from fs_user_course_comment c " +
+            "select c.*,u.nick_name as nickNameOriginal,u.avatar,tu.nick_name to_nick_name, IFNULL(l.id, 0) AS is_like from fs_user_course_comment c " +
             "LEFT JOIN fs_user u ON c.user_id=u.user_id " +
             "LEFT  JOIN fs_user tu ON tu.user_id =c.to_user_id " +
             "LEFT JOIN fs_user_course_comment_like l ON c.comment_id=l.comment_id and #{userId}=l.user_id "+
@@ -100,7 +100,7 @@ public interface FsUserCourseCommentMapper
             "</script>"})
     List<FsUserCourseCommentListUVO> selectFsUserCourseCommentListUVO(FsUserCourseCommentUParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name,u.avatar,tu.nick_name to_nick_name from fs_user_course_comment c LEFT JOIN fs_user u ON c.user_id=u.user_id LEFT  JOIN fs_user tu ON tu.user_id =c.to_user_id " +
+            "select c.*,u.nick_name as nickNameOriginal,u.avatar,tu.nick_name to_nick_name from fs_user_course_comment c LEFT JOIN fs_user u ON c.user_id=u.user_id LEFT  JOIN fs_user tu ON tu.user_id =c.to_user_id " +
             "where c.user_id=#{userId} " +
             "<if test='courseId != null'> and c.course_id = #{courseId}</if>" +
             "<if test='videoId != null'> and c.video_id = #{videoId}</if>" +
@@ -143,7 +143,7 @@ public interface FsUserCourseCommentMapper
     List<FsUserCourseCommentReplyListUVO> selectFsUserCourseCommentReplyListUVO(@Param("commentId") Long commentId, @Param("userId") String userId);
 
     @Select({"<script> " +
-            "select c.*,u.nick_name,u.avatar,tu.nick_name to_nick_name from fs_user_course_comment c " +
+            "select c.*,u.nick_name as nickNameOriginal,u.avatar,tu.nick_name to_nick_name from fs_user_course_comment c " +
             "LEFT JOIN fs_user u ON c.user_id=u.user_id " +
             "LEFT  JOIN fs_user tu ON tu.user_id =c.to_user_id " +
             "where c.is_del = 0 and  c.type = 1  \n" +

+ 3 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -158,6 +158,9 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
     @Select("select video_id dict_value, title dict_label  from fs_user_course_video where course_id=#{id} and is_del = 0 order by course_sort")
     List<OptionsVO> selectFsUserCourseVodeAllList(Long id);
 
+    @Select("select video_id dict_value, title dict_label,is_del  from fs_user_course_video where course_id=#{id}  order by course_sort")
+    List<OptionsVO> selectFsUserCourseVodeAllListByWatch(Long id);
+
     @Select("select video_id dict_value, title dict_label  from fs_user_course_video where course_id=#{id} and is_del = 0 and is_on_put = 0 order by course_sort, video_id")
     List<OptionsVO> selectFsUserCourseVideoAllListV2(@Param("id") Long id);
 

+ 13 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCommentServiceImpl.java

@@ -3,6 +3,7 @@ package com.fs.course.service.impl;
 import java.util.Collections;
 import java.util.List;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.course.param.FsUserCourseCommentParam;
 import com.fs.course.param.FsUserCourseCommentUParam;
 import com.fs.course.vo.FsUserCourseCommentListUVO;
@@ -124,12 +125,23 @@ public class FsUserCourseCommentServiceImpl implements IFsUserCourseCommentServi
         }else {
             list = fsUserCourseCommentMapper.selectFsUserCourseCommentListUVO(param);
         }
+        list.forEach(comment -> {
+            if (StringUtils.isEmpty(comment.getNickName())) {
+                comment.setNickName(comment.getNickNameOriginal());
+            }
+        });
         return list;
     }
 
     @Override
     public List<FsUserCourseCommentListUVO> selectFsUserCourseCommentMyListUVO(FsUserCourseCommentUParam param) {
-        return fsUserCourseCommentMapper.selectFsUserCourseCommentMyListUVO(param);
+        List<FsUserCourseCommentListUVO> list = fsUserCourseCommentMapper.selectFsUserCourseCommentMyListUVO(param);
+        list.forEach(comment -> {
+            if (StringUtils.isEmpty(comment.getNickName())) {
+                comment.setNickName(comment.getNickNameOriginal());
+            }
+        });
+        return list;
     }
 
     @Override

+ 1 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseCommentListUVO.java

@@ -20,6 +20,7 @@ public class FsUserCourseCommentListUVO {
     @Excel(name = "课堂id")
     private Long courseId;
     private String nickName;
+    private String nickNameOriginal;
     private String toNickName;
     private String avatar;
     /** 评论类型 1:评论,2:回复 */

+ 90 - 0
fs-service/src/main/java/com/fs/fastgptApi/param/ChatImgParam.java

@@ -0,0 +1,90 @@
+package com.fs.fastgptApi.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+* 对话接口
+*/
+@Data
+public class ChatImgParam {
+
+    /**
+    * 聊天id
+    */
+    private String chatId;
+
+    //知识库id
+//    private String dataId;
+//    private String datasetId;
+
+    /**
+    * 是否开启 stream模式 (stream模式下会通过event进行区分/非stream模式结果保存在responseData)
+    */
+    private Boolean stream;
+
+    /**
+    * 是否返回中间值(模块状态,响应的完整结果等)
+    */
+    private Boolean detail;
+
+
+    /**
+    * 模块变量,一个对象,会替换模块中,输入框内容里的{{key}}
+    */
+    private Variables variables;
+
+    /**
+    * 聊天信息(对话框)
+    */
+    private List<Message> messages;
+
+
+    @Data
+    public static class Variables {
+        //客户的id?
+        private String uid;
+        /**
+        * 客户名称
+        */
+        private String name;
+
+    }
+
+
+    @Data
+    public static class Message {
+        //知识库id
+        private String dataId;
+//        private String datasetId;
+
+        /**
+        *  问题
+        */
+        private List<content> content;
+        /**
+        * 用户权限
+         * 字段用来定义消息的发送者角色,具体包括三种选择:system、user、和 assistant。
+         *
+         * system(系统):通常用于设置聊天的上下文或者提供系统级别的指示和配置信息。
+         * user(用户):代表实际的用户输入,即用户向聊天系统提出的问题或者发起的对话内容。
+         * assistant(助手):代表智能助手的回复或者动作,是模型根据用户输入给出的响应。
+        */
+        private String role;
+
+        @Data
+        public static class content {
+            private String type;
+            private String text;
+            private ImageUrl image_url;
+        }
+
+        @Data
+        public static class ImageUrl {
+            private String url;
+        }
+
+    }
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/fastgptApi/service/ChatService.java

@@ -2,6 +2,7 @@ package com.fs.fastgptApi.service;
 
 
 import com.fs.common.core.domain.R;
+import com.fs.fastgptApi.param.ChatImgParam;
 import com.fs.fastgptApi.param.ChatParam;
 
 /**
@@ -13,4 +14,5 @@ public interface ChatService {
     * 发起对话
     */
     R initiatingTakeChat(ChatParam param,String url,String appKey);
+    R initiatingTakeChatNew(ChatImgParam param, String url, String appKey);
 }

+ 90 - 0
fs-service/src/main/java/com/fs/fastgptApi/service/Impl/ChatServiceImpl.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.fastgptApi.config.FastGptApiConfig;
+import com.fs.fastgptApi.param.ChatImgParam;
 import com.fs.fastgptApi.param.ChatParam;
 import com.fs.fastgptApi.result.ChatDetailFStreamFResult;
 import com.fs.fastgptApi.result.ChatDetailTStreamFResult;
@@ -116,6 +117,95 @@ public class ChatServiceImpl implements ChatService {
         return null;
     }
 
+    @Override
+    public R initiatingTakeChatNew(ChatImgParam param, String url, String appKey) {
+
+        String json = HttpUtil.sendPost(param, url+FastGptApiConfig.completionsUrl, appKey);
+
+        JSONObject jsonObject = JSON.parseObject(json);
+
+        // 判断是否有回应消息
+        if(jsonObject.containsKey("choices")){
+            // 获取消息列表
+            JSONArray choices = jsonObject.getJSONArray("choices");
+            // 回复消息不为空
+            if(!choices.isEmpty()){
+                // 判断是否转人工
+                if(JSON.parseObject(choices.get(0).toString()).getJSONObject("message").getString("content").contains("【转人工】")){
+                    jsonObject.put("artificial", true);
+                }
+                if(JSON.parseObject(choices.get(0).toString()).getJSONObject("message").getString("content").contains("FunctionCallBegin")){
+                    jsonObject.put("artificial", true);
+                }
+                if(JSON.parseObject(choices.get(0).toString()).getJSONObject("message").getString("content").contains("【长对话】")){
+                    jsonObject.put("longText", true);
+                }
+
+
+
+                // 替换AI回复里面的所有的【】包括里面的字符串
+                List<JSONObject> list = choices.stream().map(e -> {
+                    JSONObject result = JSON.parseObject(e.toString());
+                    JSONObject message = result.getJSONObject("message");
+                    message.put("content", message.getString("content"));
+                    return result;
+                }).collect(Collectors.toList());
+                jsonObject.put("choices", list);
+            }
+        }
+        // 判断是否匹配知识库逻辑
+        if(jsonObject.containsKey("responseData")){
+            // 遍历查找主对话框
+            JSONArray responseData = jsonObject.getJSONArray("responseData");
+            // 筛选出主对话框里面的信息,里面的historyPreview是匹配信息
+            responseData.stream().filter(e -> "主对话框".equals(JSON.parseObject(e.toString()).getString("moduleName"))).findFirst().ifPresent(e -> {
+                // 进入方法证明以及查找主对话框
+                JSONObject eJson = JSON.parseObject(e.toString());
+                // 如果主对话框里面信息为空则不处理
+                if(!eJson.containsKey("historyPreview") || StringUtils.isEmpty(eJson.getString("historyPreview"))) return;
+                // 循环里面的所有数据
+                JSONArray historyPreview = eJson.getJSONArray("historyPreview");
+                // 判断是否包含未匹配信息  -> true:未匹配到知识库  false:已匹配到知识库
+                boolean knowledge = historyPreview.stream().anyMatch(h -> {
+                    JSONObject hJson = JSON.parseObject(h.toString());
+                    return hJson.getString("value").contains("<Data>\n\n</Data>");
+                });
+                // 之所以取反,是为了后面实体类方便查看
+                jsonObject.put("knowledge", !knowledge);
+            });
+        }
+
+
+        if (jsonObject.containsKey("code") && !("200".equals(jsonObject.getString("code")))) {
+            return R.error().put("data", JSON.parseObject(json, KnowledgeBaseResult.class));
+        }
+        if (!param.getDetail() && !param.getStream()){
+
+            ChatDetailFStreamFResult result = jsonObject.toJavaObject(ChatDetailFStreamFResult.class);
+
+            return R.ok().put("data", result);
+        }
+        // flase true 的方式不建议(即stream流的方式),都是输出完才返回,没意义
+        if (!param.getDetail() && param.getStream()){
+
+//            ChatDetailFStreamTResult result = jsonObject.toJavaObject(ChatDetailFStreamTResult.class);
+
+            return R.ok().put("data", json);
+        }
+
+        if (param.getDetail()&&!param.getStream()){
+            ChatDetailTStreamFResult result = jsonObject.toJavaObject(ChatDetailTStreamFResult.class);
+            return R.ok().put("data", result);
+        }
+
+        // true true 的方式不建议(即stream流的方式),都是输出完才返回,没意义
+        if (param.getDetail()&&param.getStream()){
+
+            return R.ok().put("data", json);
+        }
+        return null;
+    }
+
 
 
 

+ 17 - 7
fs-service/src/main/java/com/fs/his/domain/FsIntegralCount.java

@@ -1,12 +1,13 @@
 package com.fs.his.domain;
 
-import java.util.Date;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.baomidou.mybatisplus.annotation.TableId;
+
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
-import com.fs.common.core.domain.BaseEntity;
-import lombok.EqualsAndHashCode;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.Date;
 
 /**
  * 通用积分消耗退还统计(不含看课领的积分)对象 fs_integral_count
@@ -21,9 +22,17 @@ public class FsIntegralCount{
     private Long id;
 
     /** $column.columnComment */
-    @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+    @Excel(name = "时间",dateFormat = "yyyy-MM-dd")
     private Date consumptionDate;
 
+    @TableField(exist = false)
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private String beginDate;
+
+    @TableField(exist = false)
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private String endDate;
+
     /** 通用的积分消耗 */
     @Excel(name = "通用的积分消耗")
     private String integralConsume;
@@ -35,5 +44,6 @@ public class FsIntegralCount{
     @Excel(name = "积分类型",dictType="sys_integral_log_type")
     private Integer logType;
 
-
+    @Excel(name = "时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
 }

+ 26 - 0
fs-service/src/main/java/com/fs/his/mapper/FsDoctorMapper.java

@@ -5,6 +5,8 @@ import java.util.Map;
 
 import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsDoctor;
+import com.fs.his.param.DoctorPrescriptionImgLogParam;
+import com.fs.his.param.DoctorPrescriptionParam;
 import com.fs.his.param.FsDoctorListUParam;
 import com.fs.his.param.FsDoctorParam;
 import com.fs.his.vo.*;
@@ -223,4 +225,28 @@ public interface FsDoctorMapper
      */
     List<FsDoctorListUVO> getFsDoctorListUVOListByIds(@Param("doctorIds") List<Long> doctorIds);
     String selectDoctorNameByIds(@Param("doctorIds") String doctorIds);
+
+    Integer selectFsDoctorPrescriptionByCode(@Param("code") String prescriptionCode);
+
+    int insertFsDoctorPrescription(DoctorPrescriptionParam processResult);
+
+    int insertFsDoctorPrescriptionImgLog(DoctorPrescriptionImgLogParam imgLogParam);
+
+    List<DoctorPrescriptionImgLogParam> selectFsDoctorPrescribeImgByDoctorId(DoctorPrescriptionImgLogParam imgLogParam);
+
+    int updateFsDoctorPrescriptionImgLog(DoctorPrescriptionImgLogParam param1);
+
+    List<DoctorPrescriptionImgLogParam> selectAllPrescriptionImgList(@Param("doctorId") String doctorId,@Param("param") DoctorPrescriptionImgLogParam param);
+
+    DoctorPrescriptionParam selectPrescriptionImgInfo(@Param("id") Long prescriptionId);
+
+    int updateFsDoctorPrescription(DoctorPrescriptionParam processResult);
+
+    int updateFsDoctorPrescriptionByLogId(DoctorPrescriptionParam param);
+
+    DoctorPrescriptionParam selectPrescriptionImgInfoByLogId(@Param("logId") Long logId);
+
+    DoctorPrescriptionImgLogParam selectFsDoctorPrescribeImgById(@Param("id") Long imgLogId);
+
+    List<DoctorPrescriptionImgLogParam> selectPrescriptionLogList(@Param("param") DoctorPrescriptionImgLogParam param);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java

@@ -123,4 +123,7 @@ public interface FsIntegralOrderMapper extends BaseMapper<FsIntegralOrder>
     List<FsIntegralOrder> findOrderByIds(@Param("orderIds") List<Long> orderIds);
 
     List<FsIntegralOrderListVO> selectFsIntegralOrderListJn(FsIntegralOrderParam fsIntegralOrder);
+
+    @Select("select order_id from fs_integral_order WHERE `status`= 1 and  extend_order_id is null ")
+    List<Long> selectFsStoreOrderNoCreateOms();
 }

+ 1 - 2
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java

@@ -169,8 +169,7 @@ public interface FsUserIntegralLogsMapper
             "SUM(CASE WHEN integral < 0 THEN ABS(integral) ELSE 0 END) AS integral_refund, " +
             "DATE(create_time) AS consumption_date " +
             "FROM fs_user_integral_logs " +
-            "WHERE log_type != 17 " +
-            "AND create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)\n" +
+            "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)\n" +
             "  AND create_time < CURDATE() " +
             "GROUP BY log_type, DATE(create_time)")
     List<FsIntegralCount> selectYesterdayIntegralGroupByLogType();

+ 50 - 0
fs-service/src/main/java/com/fs/his/param/DoctorPrescriptionImgLogParam.java

@@ -0,0 +1,50 @@
+package com.fs.his.param;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class DoctorPrescriptionImgLogParam extends BaseEntity {
+
+    @Excel(name = "ID")
+    private Long id;
+
+    @Excel(name = "医生id")
+    private Long doctorId;
+
+    @Excel(name = "医生名称")
+    private String doctorName;
+
+    @Excel(name = "图片链接")
+    private String url;
+
+    /**
+     *  状态 0上传成功 1解析成功 2解析失败 3解析中
+     */
+    @Excel(name = "状态 0上传成功 1解析成功 2解析失败 3解析中")
+    private Integer status;
+
+    @Excel(name = "是否删除")
+    private Integer isDel;
+
+    @Excel(name = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "处方信息对象")
+    private DoctorPrescriptionParam prescription;
+
+    @ApiModelProperty(value = "医生信息对象")
+    private FsDoctorParam doctorInfo;
+
+    @ApiModelProperty(value = "页码,默认为1")
+    private Integer pageNum =1;
+    @ApiModelProperty(value = "页大小,默认为10")
+    private Integer pageSize = 10;
+    @ApiModelProperty(value = "开始时间")
+    private String beginTime;
+    @ApiModelProperty(value = "结束时间")
+    private String endTime;
+
+}

+ 75 - 0
fs-service/src/main/java/com/fs/his/param/DoctorPrescriptionParam.java

@@ -0,0 +1,75 @@
+package com.fs.his.param;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+public class DoctorPrescriptionParam extends BaseEntity {
+
+    @Excel(name = "ID")
+    private Long id;
+
+    @Excel(name = "是否解析")
+    private Integer isParse;
+
+    @Excel(name = "处方编号")
+    private String prescriptionCode;
+
+    @Excel(name = "年龄")
+    private Integer age;
+
+    @Excel(name = "性别",readConverterExp = "1=男,2=女,0=未知")
+    private Integer sex;
+
+    @NotNull(message = "处方图片不能为空")
+    private String prescriptionImg;
+
+    @Excel(name = "处方信息")
+    private String prescriptionInfo;
+
+    @Excel(name = "主诉")
+    private String chiefComplaint;
+
+    @Excel(name = "现病史")
+    private String historyOfPresentIllness;
+
+    @Excel(name = "既往史")
+    private String pastMedicalHistory;
+
+    @Excel(name = "个人史")
+    private String personalHistory;
+
+    @Excel(name = "婚育史")
+    private String obstetricHistory;
+
+    @Excel(name = "家族史")
+    private String familyHistory;
+
+    @Excel(name = "建议")
+    private String suggest;
+
+    /**
+     * 状态 0=待完善、1=已创建、2=待提交、3=待审核、4=审核通过、5=审核驳回、6=已完成
+     */
+    @Excel(name = "状态",readConverterExp = "状态 0=待完善、1=已创建、2=待提交、3=待审核、4=审核通过、5=审核驳回、6=已完成")
+    private Integer status;
+
+    private Integer isDel;
+
+    @Excel(name = "医生ID")
+    private Long doctorId;
+
+    @Excel(name = "日志ID")
+    private Long logId;
+
+    /*@Excel(name = "备注")
+    private String remark;*/
+
+
+
+
+}

+ 27 - 3
fs-service/src/main/java/com/fs/his/service/IFsDoctorService.java

@@ -1,10 +1,10 @@
 package com.fs.his.service;
 
+import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsDoctor;
-import com.fs.his.param.FsDoctorListUParam;
-import com.fs.his.param.FsDoctorParam;
-import com.fs.his.param.FsUpdateFollowParam;
+import com.fs.his.param.*;
 import com.fs.his.vo.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 import java.util.Map;
@@ -115,4 +115,28 @@ public interface IFsDoctorService
      * 查询医生选择列表
      */
     List<FsDoctorChooseVO> getChooseDoctorListByMap(Map<String, Object> params);
+
+    R uploadPrescriptionImg(MultipartFile file,String doctorId);
+
+    R cleanPrescriptionImg(String doctorId);
+
+    R selectPrescriptionImgList(String doctorId, DoctorPrescriptionImgLogParam param);
+
+    R selectPrescriptionImgInfo(Long prescriptionId);
+
+    R cleanPrescriptionImgSecond(Long prescriptionId);
+
+    R uploadPrescriptionImg(MultipartFile file, Long logId);
+
+    R deletePrescription(Long imgLogId, Long doctorId);
+
+    R cleanPrescriptionImgOne(Long imgLogId);
+
+    R savePrescriptionInfo(DoctorPrescriptionParam param);
+
+    R commitPrescriptionInfo(DoctorPrescriptionParam param);
+
+    List<DoctorPrescriptionImgLogParam> selectPrescriptionLogList(DoctorPrescriptionImgLogParam param);
+
+    int updateFsDoctorPrescription(DoctorPrescriptionParam param);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java

@@ -290,4 +290,8 @@ public interface IFsStoreOrderService
     Integer sendGoodsWeizou(FsStoreOrder fsStoreOrder);
 
     BigDecimal selectPayPriceByYear(String userId);
+
+    void weizouPushIntergral(Long l);
+
+    void weizouPushScrm(Long l);
 }

+ 684 - 3
fs-service/src/main/java/com/fs/his/service/impl/FsDoctorServiceImpl.java

@@ -3,20 +3,23 @@ package com.fs.his.service.impl;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.core.domain.R;
 import com.fs.common.enums.ImTypeEnum;
 import com.fs.common.exception.CustomException;
+import com.fs.common.exception.file.OssException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.sign.Md5Utils;
+import com.fs.fastgptApi.param.ChatImgParam;
+import com.fs.fastgptApi.result.ChatDetailTStreamFResult;
+import com.fs.fastgptApi.service.ChatService;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.mapper.FsDoctorMapper;
 import com.fs.his.mapper.FsFollowMapper;
 import com.fs.his.mapper.FsFollowTempMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
-import com.fs.his.param.FsDoctorListUParam;
-import com.fs.his.param.FsDoctorParam;
-import com.fs.his.param.FsUpdateFollowParam;
+import com.fs.his.param.*;
 import com.fs.his.service.IFsDoctorService;
 import com.fs.his.service.IFsPrescribeService;
 import com.fs.his.utils.ConfigUtil;
@@ -32,12 +35,16 @@ import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.oss.CloudStorageService;
 import com.fs.system.oss.OSSFactory;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import com.google.zxing.WriterException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.imageio.ImageIO;
 import java.awt.*;
@@ -78,6 +85,9 @@ public class FsDoctorServiceImpl implements IFsDoctorService
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private ChatService chatService;
+
     /**
      * 查询医生管理
      *
@@ -551,4 +561,675 @@ public class FsDoctorServiceImpl implements IFsDoctorService
         return fsDoctorMapper.getChooseDoctorListByMap(params);
     }
 
+    /**
+     * 上传处方图片
+     * @param file
+     * @param doctorId
+     * @return
+     */
+    @Override
+    public R uploadPrescriptionImg(MultipartFile file, String doctorId) {
+        if (file == null) {
+            return R.error("请选择要上传的文件");
+        }
+
+        return uploadSingleFile(file, doctorId);
+
+    }
+
+    /*//上传处方图片
+    @Override
+    public R uploadPrescriptionImg(MultipartFile[] files, String doctorId) {
+        if (files == null || files.length == 0) {
+            return R.error("请选择要上传的文件");
+        }
+
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        int total = files.length;
+        int successCount = 0;
+        int failCount = 0;
+
+        for (MultipartFile file : files) {
+            Map<String, Object> resultMap = uploadSingleFile(file, doctorId);
+            resultList.add(resultMap);
+
+            if ("success".equals(resultMap.get("status"))) {
+                successCount++;
+            } else {
+                failCount++;
+            }
+        }
+
+        return R.ok()
+                .put("total", total)
+                .put("successCount", successCount)
+                .put("failCount", failCount)
+                .put("data", resultList);
+    }*/
+
+    private R uploadSingleFile(MultipartFile file, String doctorId) {
+        Map<String, Object> resultMap = new HashMap<>();
+        String fileName = file.getOriginalFilename();
+        resultMap.put("name", fileName);
+
+        try {
+            //validateFile(file, fileName);
+
+            CloudStorageService storage = OSSFactory.build();
+            String suffix = getFileSuffix(fileName);
+            String url = storage.uploadSuffix(file.getBytes(), suffix);
+
+            DoctorPrescriptionImgLogParam imgLogParam = new DoctorPrescriptionImgLogParam();
+            boolean saveSuccess = savePrescriptionLog(imgLogParam,url, doctorId);
+
+            if (saveSuccess) {
+                resultMap.put("id", imgLogParam.getId());
+                resultMap.put("url", url);
+                return R.ok().put("result",resultMap).put("msg", "图片上传成功!");
+            } else {
+                resultMap.put("id", imgLogParam.getId());
+                resultMap.put("url", "");
+                return R.error().put("result",resultMap).put("msg", "保存记录失败!");
+            }
+        } catch (OssException e) {
+            log.error("图片格式或内容校验失败:{}", fileName, e);
+            resultMap.put("url", "");
+            return R.error().put("result",resultMap).put("msg", e.getMessage());
+        } catch (IOException e) {
+            log.error("读取文件失败:{}", fileName, e);
+            resultMap.put("url", "");
+            return R.error().put("result",resultMap).put("msg", "文件读取失败!");
+        } catch (Exception e) {
+            log.error("图片上传失败:{}", fileName, e);
+            resultMap.put("url", "");
+            resultMap.put("msg", "");
+            return R.error().put("result",resultMap).put("msg", "图片上传失败!");
+        }
+    }
+
+    private void validateFile(MultipartFile file, String fileName) throws OssException {
+        if (file.isEmpty()) {
+            throw new OssException("上传文件不能为空");
+        }
+
+        if (fileName == null || fileName.isEmpty()) {
+            throw new OssException("文件名不能为空");
+        }
+
+        String suffix = getFileSuffix(fileName).toLowerCase();
+        if (!isValidImageType(suffix)) {
+            throw new OssException("只支持上传图片格式(jpg、png、gif、bmp)");
+        }
+
+        long fileSize = file.getSize();
+        if (fileSize > 10 * 1024 * 1024) {
+            throw new OssException("文件大小不能超过10MB");
+        }
+    }
+
+    private String getFileSuffix(String fileName) {
+        int lastDotIndex = fileName.lastIndexOf(".");
+        if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
+            return fileName.substring(lastDotIndex);
+        }
+        return "";
+    }
+
+    private boolean isValidImageType(String suffix) {
+        return ".jpg".equals(suffix) || ".jpeg".equals(suffix)
+                || ".png".equals(suffix) || ".gif".equals(suffix)
+                || ".bmp".equals(suffix) || ".webp".equals(suffix);
+    }
+
+    private boolean savePrescriptionLog(DoctorPrescriptionImgLogParam imgLogParam,String url, String doctorId) {
+        try {
+            imgLogParam.setDoctorId(Long.parseLong(doctorId));
+            imgLogParam.setUrl(url);
+            imgLogParam.setStatus(0);
+
+            int insertResult = fsDoctorMapper.insertFsDoctorPrescriptionImgLog(imgLogParam);
+
+            if (insertResult != 1) {
+                log.error("插入处方图片日志失败,doctorId: {}, url: {}", doctorId, url);
+                return false;
+            }
+
+            DoctorPrescriptionParam prescriptionParam = new DoctorPrescriptionParam();
+            prescriptionParam.setPrescriptionImg(url);
+            prescriptionParam.setDoctorId(Long.parseLong(doctorId));
+            prescriptionParam.setIsParse(0);
+            prescriptionParam.setStatus(0);
+            prescriptionParam.setLogId(imgLogParam.getId());
+
+            int prescriptionResult = fsDoctorMapper.insertFsDoctorPrescription(prescriptionParam);
+
+            if (prescriptionResult != 1) {
+                log.error("插入处方记录失败,logId: {}, url: {}", imgLogParam.getId(), url);
+                return false;
+            }
+
+            return true;
+        } catch (NumberFormatException e) {
+            log.error("医生ID格式错误:{}", doctorId, e);
+            return false;
+        } catch (Exception e) {
+            log.error("保存处方记录异常,doctorId: {}, url: {}", doctorId, url, e);
+            return false;
+        }
+    }
+
+
+    /**
+     * 解析处方图片
+     */
+    @Override
+    public R cleanPrescriptionImg(String doctorId) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        DoctorPrescriptionImgLogParam imgLogParam = new DoctorPrescriptionImgLogParam();
+        imgLogParam.setDoctorId(Long.parseLong(doctorId));
+        imgLogParam.setStatus(1);
+        List<DoctorPrescriptionImgLogParam> paramList = fsDoctorMapper.selectFsDoctorPrescribeImgByDoctorId(imgLogParam);
+
+        if (paramList == null || paramList.isEmpty()) {
+            return R.error("请先上传能解析的图片")
+                    .put("data", resultList);
+        }
+
+        int total = paramList.size();
+        int successCount = 0;
+        int failCount = 0;
+
+        for (DoctorPrescriptionImgLogParam logParam : paramList) {
+            Map<String, Object> resultMap = processSingleImage(logParam, doctorId);
+            resultList.add(resultMap);
+
+            if ((Boolean) resultMap.get("status")) {
+                successCount++;
+            } else {
+                failCount++;
+            }
+        }
+
+        return R.ok()
+                .put("total", total)
+                .put("successCount", successCount)
+                .put("failCount", failCount)
+                .put("data", resultList);
+    }
+
+    private Map<String, Object> processSingleImage(DoctorPrescriptionImgLogParam logParam, String doctorId) {
+        Map<String, Object> resultMap = new HashMap<>();
+        String url = logParam.getUrl();
+        resultMap.put("url", url);
+
+        DoctorPrescriptionImgLogParam updateParam = new DoctorPrescriptionImgLogParam();
+        updateParam.setId(logParam.getId());
+        updateParam.setStatus(3);
+        updateParam.setRemark("图像解析中...");
+        fsDoctorMapper.updateFsDoctorPrescriptionImgLog(updateParam);
+
+        try {
+            DoctorPrescriptionParam processResult = imageProcess(url, doctorId);
+            processResult.setLogId(logParam.getId());
+
+            if (processResult.getIsParse() != 1 || processResult.getPrescriptionCode() == null) {
+                updateParam.setStatus(2);
+                updateParam.setRemark("图像解析失败,请重新上传!");
+                resultMap.put("status", false);
+                resultMap.put("result", "图像解析失败,请重新上传!");
+            } else {
+                Integer existCount = fsDoctorMapper.selectFsDoctorPrescriptionByCode(processResult.getPrescriptionCode());
+                if (existCount >= 1) {
+                    updateParam.setStatus(2);
+                    updateParam.setRemark("处方单号已存在,请重新上传!");
+                    resultMap.put("status", false);
+                    resultMap.put("result", "处方单号已存在,请重新上传!");
+                } else {
+                    boolean saveSuccess = savePrescription(processResult, logParam.getId());
+                    if (saveSuccess) {
+                        updateParam.setStatus(1);
+                        updateParam.setRemark("解析成功");
+                        resultMap.put("status", true);
+                        resultMap.put("result", processResult);
+                    } else {
+                        updateParam.setStatus(2);
+                        updateParam.setRemark("保存失败,请重新解析!");
+                        resultMap.put("status", false);
+                        resultMap.put("result", "保存失败,请重新解析!");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            updateParam.setStatus(2);
+            updateParam.setRemark("处理异常:" + e.getMessage());
+            resultMap.put("status", false);
+            resultMap.put("result", "处理异常,请稍后重试!");
+        }
+
+        fsDoctorMapper.updateFsDoctorPrescriptionImgLog(updateParam);
+        return resultMap;
+    }
+
+    private boolean savePrescription(DoctorPrescriptionParam processResult, Long logId) {
+        DoctorPrescriptionParam existingParam = fsDoctorMapper.selectPrescriptionImgInfoByLogId(logId);
+
+        if (existingParam != null) {
+            processResult.setId(existingParam.getId());
+            processResult.setStatus(0);
+            return fsDoctorMapper.updateFsDoctorPrescription(processResult) > 0;
+        } else {
+            processResult.setStatus(0);
+            return fsDoctorMapper.insertFsDoctorPrescription(processResult) > 0;
+        }
+    }
+
+
+    @Override
+    public R selectPrescriptionImgList(String doctorId, DoctorPrescriptionImgLogParam param) {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<DoctorPrescriptionImgLogParam> param1 = fsDoctorMapper.selectAllPrescriptionImgList(doctorId,param);
+        return R.ok().put("result",new PageInfo<>(param1));
+    }
+
+    @Override
+    public R selectPrescriptionImgInfo(Long imgLogId) {
+        DoctorPrescriptionParam param = fsDoctorMapper.selectPrescriptionImgInfoByLogId(imgLogId);
+        return R.ok().put("result",param);
+    }
+
+    @Override
+    public R cleanPrescriptionImgSecond(Long prescriptionId) {
+        if (prescriptionId == null) {
+            return R.error("处方ID不能为空");
+        }
+
+        DoctorPrescriptionParam param = fsDoctorMapper.selectPrescriptionImgInfo(prescriptionId);
+        if (param == null) {
+            return R.error("未找到对应的处方记录");
+        }
+
+        String url = param.getPrescriptionImg();
+        if (url == null || url.isEmpty()) {
+            return R.error("处方图片不存在");
+        }
+
+        Map<String, Object> resultMap = reprocessPrescriptionImage(param, url);
+        return R.ok().put("data", resultMap);
+    }
+
+    private Map<String, Object> reprocessPrescriptionImage(DoctorPrescriptionParam prescriptionParam, String url) {
+        Map<String, Object> resultMap = new HashMap<>();
+        resultMap.put("url", url);
+
+        DoctorPrescriptionImgLogParam logUpdateParam = new DoctorPrescriptionImgLogParam();
+        logUpdateParam.setId(prescriptionParam.getLogId());
+
+        try {
+            DoctorPrescriptionParam processResult = imageProcess(url, String.valueOf(prescriptionParam.getDoctorId()));
+
+            if (processResult.getIsParse() != 1 || processResult.getPrescriptionCode() == null) {
+                updateLogStatus(logUpdateParam, 2, "图像解析失败,请重新上传!");
+                resultMap.put("status", false);
+                resultMap.put("result", "图像解析失败,请重新上传!");
+            } else {
+                handleParsedPrescription(processResult, logUpdateParam, resultMap);
+            }
+        } catch (Exception e) {
+            log.error("重新解析处方图片失败,prescriptionId: {}", prescriptionParam.getId(), e);
+            updateLogStatus(logUpdateParam, 2, "解析异常:" + e.getMessage());
+            resultMap.put("status", false);
+            resultMap.put("result", "解析异常,请稍后重试!");
+        }
+
+        return resultMap;
+    }
+
+    private void handleParsedPrescription(DoctorPrescriptionParam processResult,
+                                          DoctorPrescriptionImgLogParam logUpdateParam,
+                                          Map<String, Object> resultMap) {
+        Integer existCount = fsDoctorMapper.selectFsDoctorPrescriptionByCode(processResult.getPrescriptionCode());
+        DoctorPrescriptionParam param = fsDoctorMapper.selectPrescriptionImgInfoByLogId(logUpdateParam.getId());
+        boolean status = param != null && !processResult.getPrescriptionCode().equals(param.getPrescriptionCode()) && param.getIsDel() == 0;
+        if (existCount >= 1 && status) {
+            updateLogStatus(logUpdateParam, 2, "处方单号已存在,请重新上传!");
+            resultMap.put("status", false);
+            resultMap.put("result", "处方单号已存在,请重新上传!");
+        } else {
+            processResult.setLogId(logUpdateParam.getId());
+            resultMap.put("status", true);
+            resultMap.put("result", processResult);
+
+            /*boolean saveSuccess = saveNewPrescription(processResult);
+            if (saveSuccess) {
+                updateLogStatus(logUpdateParam, 1, "解析成功");
+                resultMap.put("status", true);
+                resultMap.put("result", processResult);
+            } else {
+                updateLogStatus(logUpdateParam, 2, "保存失败,请重新解析!");
+                resultMap.put("status", false);
+                resultMap.put("result", "保存失败,请重新解析!");
+            }*/
+        }
+    }
+
+    private boolean saveNewPrescription(DoctorPrescriptionParam processResult) {
+        try {
+            processResult.setStatus(0);
+            int updated = fsDoctorMapper.updateFsDoctorPrescriptionByLogId(processResult);
+            return updated > 0;
+        } catch (Exception e) {
+            log.error("保存新处方记录失败,处方号: {}", processResult.getPrescriptionCode(), e);
+            return false;
+        }
+    }
+
+    @Override
+    public R uploadPrescriptionImg(MultipartFile file, Long logId) {
+        if (file == null || file.isEmpty()) {
+            return R.error("上传文件不能为空");
+        }
+
+        if (logId == null) {
+            return R.error("日志ID不能为空");
+        }
+
+
+        Map<String, Object> resultMap = uploadAndUpdateSingleFile(file, logId);
+
+        if ("success".equals(resultMap.get("status"))) {
+            return R.ok().put("data", resultMap);
+        } else {
+            return R.error((String) resultMap.get("message")).put("data", resultMap);
+        }
+    }
+
+    private Map<String, Object> uploadAndUpdateSingleFile(MultipartFile file, Long logId) {
+        Map<String, Object> resultMap = new HashMap<>();
+        String fileName = file.getOriginalFilename();
+        resultMap.put("name", fileName);
+
+        try {
+            validateFile(file, fileName);
+
+            CloudStorageService storage = OSSFactory.build();
+            String suffix = getFileSuffix(fileName);
+            String url = storage.uploadSuffix(file.getBytes(), suffix);
+
+            boolean updateSuccess = updatePrescriptionWithNewImage(url, logId);
+
+            if (updateSuccess) {
+                resultMap.put("id", logId);
+                resultMap.put("status", "success");
+                resultMap.put("url", url);
+                resultMap.put("message", "图片上传成功!");
+            } else {
+                resultMap.put("id", 0);
+                resultMap.put("status", "error");
+                resultMap.put("url", "");
+                resultMap.put("message", "更新记录失败!");
+            }
+        } catch (OssException e) {
+            log.error("图片格式或内容校验失败:{}", fileName, e);
+            resultMap.put("id", 0);
+            resultMap.put("status", "error");
+            resultMap.put("url", "");
+            resultMap.put("message", e.getMessage());
+        } catch (IOException e) {
+            log.error("读取文件失败:{}", fileName, e);
+            resultMap.put("id", 0);
+            resultMap.put("status", "error");
+            resultMap.put("url", "");
+            resultMap.put("message", "文件读取失败!");
+        } catch (Exception e) {
+            log.error("图片上传失败:{}", fileName, e);
+            resultMap.put("id", 0);
+            resultMap.put("status", "error");
+            resultMap.put("url", "");
+            resultMap.put("message", "图片上传失败!");
+        }
+
+        return resultMap;
+    }
+
+    private boolean updatePrescriptionWithNewImage(String url, Long logId) {
+        try {
+            DoctorPrescriptionImgLogParam imgLogParam = new DoctorPrescriptionImgLogParam();
+            imgLogParam.setUrl(url);
+            imgLogParam.setId(logId);
+
+            int logUpdateResult = fsDoctorMapper.updateFsDoctorPrescriptionImgLog(imgLogParam);
+            if (logUpdateResult != 1) {
+                log.error("更新处方图片日志失败,logId: {}, url: {}", logId, url);
+                return false;
+            }
+
+            DoctorPrescriptionParam prescriptionParam = new DoctorPrescriptionParam();
+            prescriptionParam.setPrescriptionImg(url);
+            prescriptionParam.setLogId(logId);
+            prescriptionParam.setIsParse(0);
+
+            int prescriptionUpdateResult = fsDoctorMapper.updateFsDoctorPrescriptionByLogId(prescriptionParam);
+            if (prescriptionUpdateResult < 1) {
+                log.error("更新处方记录失败,logId: {}, url: {}", logId, url);
+                return false;
+            }
+
+            return true;
+        } catch (Exception e) {
+            log.error("更新处方记录异常,logId: {}, url: {}", logId, url, e);
+            return false;
+        }
+    }
+
+    private void updateLogStatus(DoctorPrescriptionImgLogParam logParam, Integer status, String remark) {
+        logParam.setStatus(status);
+        logParam.setRemark(remark);
+        fsDoctorMapper.updateFsDoctorPrescriptionImgLog(logParam);
+    }
+
+
+    // 自定义图片处理方法
+    private DoctorPrescriptionParam imageProcess(String url,String doctorId) {
+        ChatImgParam param=new ChatImgParam();
+        param.setChatId(doctorId);
+        param.setStream(false);
+        param.setDetail(true);
+        List<ChatImgParam.Message> messageList=new ArrayList<ChatImgParam.Message>();
+        ChatImgParam.Message message = new ChatImgParam.Message();
+        message.setRole("user");
+        List<ChatImgParam.Message.content> contents = new ArrayList<>();
+
+        ChatImgParam.Message.content content = new ChatImgParam.Message.content();
+        content.setType("text");
+        content.setText("请根据提示词识别图片内容");
+        contents.add(content);
+
+        //设置上传的图片
+        ChatImgParam.Message.content contentImg = new ChatImgParam.Message.content();
+        contentImg.setType("image_url");
+        ChatImgParam.Message.ImageUrl imageUrl = new ChatImgParam.Message.ImageUrl();
+        imageUrl.setUrl(url);
+        contentImg.setImage_url(imageUrl);
+        contents.add(contentImg);
+
+        message.setContent(contents);
+        messageList.add(message);
+        param.setMessages(messageList);
+
+        R r = chatService.initiatingTakeChatNew(param, "http://129.28.170.206:3000/api", "fastgpt-t3Z1s8Lb6pKMILsSNUHDCOZ4PWgcRlwcWJT7DvrxtvqJfyZbEyWLqS8Up");
+        if(!r.get("code").equals(200)){
+            DoctorPrescriptionParam doctorPrescriptionParam = new DoctorPrescriptionParam();
+            doctorPrescriptionParam.setIsParse(0);
+            return doctorPrescriptionParam;
+        }
+        ChatDetailTStreamFResult result=(ChatDetailTStreamFResult)r.get("data");
+
+        String contentKh = result.getChoices().get(0).getMessage().getContent();
+        System.out.println("模型解析图片结果:"+contentKh);
+        Gson gson = new Gson();
+        DoctorPrescriptionParam fromJson = gson.fromJson(contentKh, DoctorPrescriptionParam.class);
+        fromJson.setPrescriptionImg(url);
+        fromJson.setDoctorId(Long.parseLong(doctorId));
+        return fromJson;
+    }
+
+    @Override
+    @Transactional
+    public R deletePrescription(Long imgLogId, Long doctorId) {
+        DoctorPrescriptionParam param = fsDoctorMapper.selectPrescriptionImgInfoByLogId(imgLogId);
+        if(param == null || param.getId()==null){
+            return R.error("未找到对应的处方记录");
+        }
+        Long prescriptionId = param.getId();
+
+        if (doctorId == null) {
+            return R.error("医生ID不能为空");
+        }
+
+        DoctorPrescriptionParam prescription = fsDoctorMapper.selectPrescriptionImgInfo(prescriptionId);
+        if (prescription == null) {
+            return R.error("未找到对应的处方记录");
+        }
+
+        if (!prescription.getDoctorId().equals(doctorId)) {
+            return R.error("无权删除该处方记录");
+        }
+
+        if (prescription.getIsDel() != null && prescription.getIsDel() == 1) {
+            return R.error("该处方已被删除");
+        }
+
+        Integer status = prescription.getStatus();
+        if (status != null && (status == 3 || status == 4 || status == 6)) {
+            String statusMsg = getStatusDescription(status);
+            return R.error("当前处方状态为" + statusMsg + ",不允许删除");
+        }
+
+        try {
+            DoctorPrescriptionParam updateParam = new DoctorPrescriptionParam();
+            updateParam.setId(prescriptionId);
+            updateParam.setIsDel(1);
+            int prescriptionResult = fsDoctorMapper.updateFsDoctorPrescription(updateParam);
+
+            if (prescriptionResult <= 0) {
+                log.error("软删除处方记录失败,prescriptionId: {}", prescriptionId);
+                return R.error("删除处方记录失败");
+            }
+
+            if (prescription.getLogId() != null) {
+                DoctorPrescriptionImgLogParam logUpdateParam = new DoctorPrescriptionImgLogParam();
+                logUpdateParam.setId(prescription.getLogId());
+                logUpdateParam.setIsDel(1);
+                int logResult = fsDoctorMapper.updateFsDoctorPrescriptionImgLog(logUpdateParam);
+
+                if (logResult <= 0) {
+                    log.warn("更新处方图片日志状态失败,logId: {}", prescription.getLogId());
+                }
+            }
+
+            log.info("成功删除处方记录,prescriptionId: {}, doctorId: {}", prescriptionId, doctorId);
+            return R.ok("删除成功");
+        } catch (Exception e) {
+            log.error("删除处方记录异常,prescriptionId: {}", prescriptionId, e);
+            throw new RuntimeException("删除处方记录失败");
+        }
+    }
+
+    @Override
+    public R cleanPrescriptionImgOne(Long imgLogId) {
+        DoctorPrescriptionParam param = fsDoctorMapper.selectPrescriptionImgInfoByLogId(imgLogId);
+        if(param == null || param.getId()==null){
+            return R.error("未找到对应的处方记录");
+        }
+        return this.cleanPrescriptionImgSecond(param.getId());
+    }
+
+    @Override
+    public R savePrescriptionInfo(DoctorPrescriptionParam param) {
+        Map<String, Object> resultMap = new HashMap<>();
+        Integer existCount = fsDoctorMapper.selectFsDoctorPrescriptionByCode(param.getPrescriptionCode());
+        DoctorPrescriptionParam paramDb = fsDoctorMapper.selectPrescriptionImgInfoByLogId(param.getId());
+        boolean status1 = paramDb != null && !param.getPrescriptionCode().equals(paramDb.getPrescriptionCode()) && paramDb.getIsDel() == 0;
+        if (existCount >= 1 && status1) {
+            DoctorPrescriptionImgLogParam imgLogParam = new DoctorPrescriptionImgLogParam();
+            imgLogParam.setId(param.getLogId());
+            updateLogStatus(imgLogParam, 2, "处方单号已存在,请重新上传!");
+            resultMap.put("status", false);
+            resultMap.put("result", "处方单号已存在,请重新上传!");
+        }
+
+        Integer status = param.getStatus();
+        if(status == 0){
+            int i = fsDoctorMapper.updateFsDoctorPrescriptionByLogId(param);
+            if(i <= 0){
+                resultMap.put("msg", "暂存处方信息失败");
+                return R.error().put("result", resultMap);
+            }
+            resultMap.put("msg", "暂存处方信息成功");
+            return R.ok().put("result", resultMap);
+        }else if(status == 1){
+            if(param.getPrescriptionCode() == null){
+                resultMap.put("msg", "处方单号未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            if(param.getAge() == null){
+                resultMap.put("msg", "年龄未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            if(param.getSex() == null){
+                resultMap.put("msg", "性别未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            if(param.getChiefComplaint() == null){
+                resultMap.put("msg", "主诉未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            if(param.getHistoryOfPresentIllness() == null){
+                resultMap.put("msg", "现病史未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            if(param.getSuggest() == null){
+                resultMap.put("msg", "医生建议未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            if(param.getPrescriptionInfo() == null){
+                resultMap.put("msg", "处方信息未填,请重新上传!");
+                return R.error().put("result", resultMap);
+            }
+            fsDoctorMapper.updateFsDoctorPrescriptionByLogId(param);
+            resultMap.put("msg", "保存处方信息成功");
+            return R.ok().put("result", resultMap);
+        }
+        return null;
+    }
+
+    @Override
+    public R commitPrescriptionInfo(DoctorPrescriptionParam param) {
+        int i = fsDoctorMapper.updateFsDoctorPrescriptionByLogId(param);
+        boolean status = i > 0;
+        return R.ok().put("success", status);
+    }
+
+    @Override
+    public List<DoctorPrescriptionImgLogParam> selectPrescriptionLogList(DoctorPrescriptionImgLogParam param) {
+        return fsDoctorMapper.selectPrescriptionLogList(param);
+    }
+
+    @Override
+    public int updateFsDoctorPrescription(DoctorPrescriptionParam param) {
+        return fsDoctorMapper.updateFsDoctorPrescriptionByLogId(param);
+    }
+
+    private String getStatusDescription(Integer status) {
+        switch (status) {
+            case 3:
+                return "待审核";
+            case 4:
+                return "审核通过";
+            case 6:
+                return "已完成";
+            default:
+                return "状态" + status;
+        }
+    }
 }

+ 428 - 43
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -59,9 +59,10 @@ import com.fs.his.service.*;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.*;
+import com.fs.hisStore.domain.FsStoreOrderLogsScrm;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
-import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
-import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.mapper.*;
 import com.fs.hisStore.param.FsStoreOrderRefundByProductParam;
 import com.fs.hisStore.service.IFsStoreOrderLogsScrmService;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
@@ -131,6 +132,7 @@ import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -328,6 +330,21 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
     private IFsStoreOrderScrmService orderScrmService;
     @Autowired
     private IFsStoreProductService fsStoreProductService;
+    @Autowired
+    private FsIntegralOrderMapper fsIntegralOrderMapper;
+    @Autowired
+    private FsIntegralGoodsMapper fsIntegralGoodsMapper;
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
+    @Autowired
+    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+    @Autowired
+    private FsIntegralOrderLogsMapper fsIntegralOrderLogsMapper;
+    @Autowired
+    private FsStoreOrderLogsScrmMapper fsStoreOrderLogsScrmMapper;
+
     @PostConstruct
     public void initErpServiceMap() {
         erpServiceMap = new HashMap<>();
@@ -4634,7 +4651,7 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         if (order.getShippingType() != null && order.getShippingType() == 2)return;
         WeizouApiPushOrderParam param = new WeizouApiPushOrderParam();
         param.setRealName(order.getUserName()).setPhone(order.getUserPhone().length() > 11 ? decryptPhone(order.getUserPhone()) : order.getUserPhone())
-                .setFreightPrice(order.getFreightPrice()).setExtendOrderId(order.getOrderId().toString())
+                .setFreightPrice(order.getFreightPrice()).setExtendOrderId(order.getOrderCode())
         ;
         //全款
         if (order.getPayType().equals(1)) {
@@ -4684,7 +4701,7 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
             } else {
                 param.setProvince(address[0]).setCity(address[1]).setDistrict(address[2]);
-                //处理地址多空问题
+                //处理地址多空问题
                 if (address.length > 3) {
                     StringBuffer addrs = new StringBuffer();
                     for (int i = 3; i < address.length; i++) {
@@ -4717,13 +4734,23 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         // 1. 参数校验
 //        validateSendGoodsParams(fsStoreOrder, opeName);//controller处理了
 
-        // 2. 查询原始订单
-        FsStoreOrder originalOrder = fsStoreOrderMapper.selectFsStoreOrderByOrderId(fsStoreOrder.getOrderId());
+
+        Object originalOrder = findFirstNonNull(
+                CompletableFuture.supplyAsync(() ->
+                        fsStoreOrderMapper.selectFsStoreOrderByOrderCode(fsStoreOrder.getOrderCode())
+                ),
+                CompletableFuture.supplyAsync(() ->
+                        fsStoreOrderMapper.selectFsStoreOrderScrmByOrderCode(fsStoreOrder.getOrderCode())
+                ),
+                CompletableFuture.supplyAsync(() ->
+                        fsIntegralOrderMapper.selectFsIntegralOrderByOrderCode(fsStoreOrder.getOrderCode())
+                )
+        );
+
         if (originalOrder == null) {
             throw new CustomException("订单不存在");
         }
 
-        // 3. 根据操作类型分发处理
         if (OPERATOR_CANCEL.equals(fsStoreOrder.getOperator())) {
             return processCancelDelivery(originalOrder, fsStoreOrder, "微走取消订单");
         }
@@ -4733,57 +4760,124 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             return processUpdateDelivery(originalOrder, fsStoreOrder, "微走订单更新");        }
     }
 
-    /**
-     * 处理订单状态更新//todo 到货后续业务逻辑需要对接
-     */
-    private int processUpdateDelivery(FsStoreOrder originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
-        //微走有多个状态,需要分别记录
-        if (originalOrder.getStatus() == STATUS_SHIPPED) {
-            updateOrderToEvaluated(fsStoreOrder);
+    @SafeVarargs
+    private final <T> T findFirstNonNull(CompletableFuture<? extends T>... futures) {
+        CompletableFuture.allOf(futures).join();
+        for (CompletableFuture<? extends T> future : futures) {
+            try {
+                T result = future.get();
+                if (result != null) {
+                    return result;
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
         }
+        return null;
+    }
 
-        // 记录日志
-        log.info("微走快递送达, 订单号: {}",
-                fsStoreOrder.getOrderId());
-        return saveOrderLog(fsStoreOrder, WeizouOrderExpressEnum.getDescByCode(fsStoreOrder.getOperator()), opeName);
+    private int processUpdateDelivery(Object order, FsStoreOrder fsStoreOrder, String opeName) {
+        OrderLogSaver logSaver = dispatchUpdate(order, fsStoreOrder);
+        log.info("微走快递送达, 订单号: {}", fsStoreOrder.getOrderId());
+        return logSaver.save(fsStoreOrder, WeizouOrderExpressEnum.getDescByCode(fsStoreOrder.getOperator()), opeName);
     }
     /**
      * 处理取消发货
      */
-    private int processCancelDelivery(FsStoreOrder originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
-        // 待发货状态才更新订单状态
-        if (originalOrder.getStatus() == STATUS_PENDING_SHIPMENT) {
-            updateOrderToCancelled(fsStoreOrder);
-        }
-
-        // 调用退款服务
-//        SpringUtils.getBean(IFsStoreOrderScrmService.class).refundOrder(fsStoreOrder.getOrderId());//todo 推给售后
-
-        // 记录日志
-        log.info("微走取消发货, 订单号: {}",
-                fsStoreOrder.getOrderId());
-
-        return saveOrderLog(fsStoreOrder, fsStoreOrder.getOperator(), opeName);
+    private int processCancelDelivery(Object order, FsStoreOrder fsStoreOrder, String opeName) {
+        OrderLogSaver logSaver = dispatchCancel(order, fsStoreOrder);
+        log.info("微走取消发货, 订单号: {}", fsStoreOrder.getOrderId());
+        return logSaver.save(fsStoreOrder, fsStoreOrder.getOperator(), opeName);
     }
 
     /**
      * 处理正常发货/修改快递
      */
-    private int processNormalDelivery(FsStoreOrder originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
-        int result;
+    private int processNormalDelivery(Object order, FsStoreOrder fsStoreOrder, String opeName) {
+        if (order instanceof FsStoreOrder) {
+            FsStoreOrder o = (FsStoreOrder) order;
+            fsStoreOrder.setOrderId(o.getOrderId());
+            if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
+                executeShipOrder(o, fsStoreOrder, opeName);
+            } else {
+                updateDeliveryInfo(o, fsStoreOrder, opeName);
+            }
+
+        }else
+        if (order instanceof FsStoreOrderScrm) {
+            FsStoreOrderScrm o = (FsStoreOrderScrm) order;
+            fsStoreOrder.setOrderId(o.getId());
+            if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
+                executeShipOrderScrm(o, fsStoreOrder, opeName);
+            } else {
+                updateDeliveryInfoScrm(o, fsStoreOrder, opeName);
+            }
+
+        }else{
+            FsIntegralOrder o = (FsIntegralOrder) order;
+            fsStoreOrder.setOrderId(o.getOrderId());
+            if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
+                executeShipOrderIntegral(o, fsStoreOrder, opeName);
+            } else {
+                updateDeliveryInfoIntegral(o, fsStoreOrder, opeName);
+            }
 
-        if (originalOrder.getStatus() == STATUS_PENDING_SHIPMENT) {
-            // 待发货状态:执行发货操作
-            result = executeShipOrder(originalOrder, fsStoreOrder, opeName);
-        } else {
-            // 其他状态:只更新快递信息
-            result = updateDeliveryInfo(originalOrder, fsStoreOrder, opeName);
         }
+        log.info("微走取消发货, 订单号: {}", fsStoreOrder.getOrderId());
+        return 1;
+    }
 
-        // 订阅快递信息(两种场景都需要)
-//        subscribeExpress(originalOrder, fsStoreOrder);//2026-3-20沟通后今正用微走接口调用的方式更新物流状态
+    @FunctionalInterface
+    private interface OrderLogSaver {
+        int save(FsStoreOrder order, String operator, String message);
+    }
 
-        return result;
+    private OrderLogSaver dispatchCancel(Object order, FsStoreOrder fsStoreOrder) {
+        if (order instanceof FsStoreOrder) {
+            FsStoreOrder o = (FsStoreOrder) order;
+            if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
+                fsStoreOrder.setOrderId(o.getOrderId());
+                updateOrderToCancelled(fsStoreOrder);
+            }
+            return this::saveOrderLog;
+        }
+        if (order instanceof FsStoreOrderScrm) {
+            FsStoreOrderScrm o = (FsStoreOrderScrm) order;
+            if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
+                fsStoreOrder.setOrderId(o.getId());
+                updateOrderToCancelledScrm(fsStoreOrder);
+            }
+            return this::saveOrderLogScrm;
+        }
+        FsIntegralOrder o = (FsIntegralOrder) order;
+        if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
+            fsStoreOrder.setOrderId(o.getOrderId());
+            updateOrderToCancelledIntegral(fsStoreOrder);
+        }
+        return this::saveOrderLogIntegral;
+    }
+
+    private OrderLogSaver dispatchUpdate(Object order, FsStoreOrder fsStoreOrder) {
+        if (order instanceof FsStoreOrder) {
+            FsStoreOrder o = (FsStoreOrder) order;
+            if (o.getStatus() == STATUS_SHIPPED) {
+                fsStoreOrder.setOrderId(o.getOrderId());
+                updateOrderToEvaluated(fsStoreOrder);
+            }
+            return this::saveOrderLog;
+        }
+        if (order instanceof FsStoreOrderScrm) {
+            FsStoreOrderScrm o = (FsStoreOrderScrm) order;
+            if (o.getStatus() == STATUS_SHIPPED) {
+                fsStoreOrder.setOrderId(o.getId());
+            }
+            return this::saveOrderLogScrm;
+        }
+        FsIntegralOrder o = (FsIntegralOrder) order;
+        if (o.getStatus() == STATUS_SHIPPED) {
+            fsStoreOrder.setOrderId(o.getOrderId());
+        }
+        return this::saveOrderLogIntegral;
     }
 
     /**
@@ -4810,6 +4904,47 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         return result;
     }
+    private int executeShipOrderScrm(FsStoreOrderScrm originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
+        // 1. 更新订单为已发货
+        FsStoreOrderScrm updateOrder = new FsStoreOrderScrm();
+        updateOrder.setId(fsStoreOrder.getOrderId());
+        updateOrder.setStatus(STATUS_PENDING_SHIPMENT);
+        updateOrder.setUpdateTime(new DateTime());
+        updateOrder.setDeliveryCode(fsStoreOrder.getDeliveryCode());
+        updateOrder.setDeliveryName(fsStoreOrder.getDeliveryName());
+        updateOrder.setDeliverySn(fsStoreOrder.getDeliverySn());
+        updateOrder.setDeliveryTime(getCurrentDateTimeString());
+        int result = fsStoreOrderScrmMapper.updateFsStoreOrder(updateOrder);
+        // 2. 处理公司扣款
+        if (originalOrder.getCompanyId() != null) {
+            companyService.subtractCompanyMoneyScrm(originalOrder);
+        }
+        // 3. 记录日志
+        String logMessage = opeName + " 订单发货";
+        saveOrderLogScrm(fsStoreOrder, opeName, logMessage);
+        log.info("微走订单发货成功, 订单号: {}, 快递公司: {}, 快递单号: {}",
+                fsStoreOrder.getOrderId(),
+                fsStoreOrder.getDeliveryName(),
+                fsStoreOrder.getDeliverySn());
+        return result;
+    }
+    private int executeShipOrderIntegral(FsIntegralOrder originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
+        // 1. 更新订单为已发货
+        FsIntegralOrder updateOrder = buildShipOrderUpdateIntegral(fsStoreOrder);
+        int result = fsIntegralOrderMapper.updateFsIntegralOrder(updateOrder);
+        // 2. 处理公司扣款 //积分商品暂未找到
+//        if (originalOrder.getCompanyId() != null) {
+//            companyService.subtractCompanyMoney(originalOrder);
+//        }
+        // 3. 记录日志
+        String logMessage = opeName + " 订单发货";
+        saveOrderLogIntegral(fsStoreOrder, opeName, logMessage);
+        log.info("微走订单发货成功, 订单号: {}, 快递公司: {}, 快递单号: {}",
+                fsStoreOrder.getOrderId(),
+                fsStoreOrder.getDeliveryName(),
+                fsStoreOrder.getDeliverySn());
+        return result;
+    }
 
     /**
      * 更新快递信息
@@ -4828,6 +4963,34 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         return result;
     }
+    private int updateDeliveryInfoScrm(FsStoreOrderScrm originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
+        // 1. 只更新快递相关信息
+        FsStoreOrderScrm updateOrder = buildDeliveryInfoUpdateScrm(fsStoreOrder);
+        int result = fsStoreOrderScrmMapper.updateFsStoreOrder(updateOrder);
+
+        // 2. 记录日志
+        String logMessage = opeName + " 订单单号更新";
+        saveOrderLogScrm(fsStoreOrder, opeName, logMessage);
+
+        log.info("微走修改快递单号, 订单号: {}, 新快递单号: {}",
+                fsStoreOrder.getOrderId(), fsStoreOrder.getDeliverySn());
+
+        return result;
+    }
+    private int updateDeliveryInfoIntegral(FsIntegralOrder originalOrder, FsStoreOrder fsStoreOrder, String opeName) {
+        // 1. 只更新快递相关信息
+        FsIntegralOrder updateOrder = buildDeliveryInfoUpdateIntegral(fsStoreOrder);
+        int result = fsIntegralOrderMapper.updateFsIntegralOrder(updateOrder);
+
+        // 2. 记录日志
+        String logMessage = opeName + " 订单单号更新";
+        saveOrderLogIntegral(fsStoreOrder, opeName, logMessage);
+
+        log.info("微走修改快递单号, 订单号: {}, 新快递单号: {}",
+                fsStoreOrder.getOrderId(), fsStoreOrder.getDeliverySn());
+
+        return result;
+    }
 
     /**
      * 更新订单为已取消状态
@@ -4841,6 +5004,24 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         fsStoreOrderMapper.updateFsStoreOrder(updateOrder);
     }
+    private void updateOrderToCancelledScrm(FsStoreOrder fsStoreOrder) {
+        FsStoreOrderScrm updateOrder = new FsStoreOrderScrm();
+        updateOrder.setId(fsStoreOrder.getOrderId());
+        updateOrder.setStatus(STATUS_PENDING_SHIPMENT);//待发货
+        updateOrder.setUpdateTime(new DateTime());
+        updateOrder.setDeliveryTime(getCurrentDateTimeString());
+
+        fsStoreOrderScrmMapper.updateFsStoreOrder(updateOrder);
+    }
+    private void updateOrderToCancelledIntegral(FsStoreOrder fsStoreOrder) {
+        FsIntegralOrder updateOrder = new FsIntegralOrder();
+        updateOrder.setOrderId(fsStoreOrder.getOrderId());
+        updateOrder.setStatus(STATUS_PENDING_SHIPMENT);//待发货
+        updateOrder.setUpdateTime(new DateTime());
+        updateOrder.setDeliveryTime(new DateTime());
+
+        fsIntegralOrderMapper.updateFsIntegralOrder(updateOrder);
+    }
     /**
      * 更新订单为待评价状态
      */
@@ -4869,6 +5050,19 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         return updateOrder;
     }
+    private FsIntegralOrder buildShipOrderUpdateIntegral(FsStoreOrder fsStoreOrder) {
+        FsIntegralOrder updateOrder = new FsIntegralOrder();
+        updateOrder.setOrderId(fsStoreOrder.getOrderId());
+        updateOrder.setStatus(STATUS_PENDING_SHIPMENT);
+        updateOrder.setUpdateTime(new DateTime());
+        updateOrder.setDeliveryCode(fsStoreOrder.getDeliveryCode());
+        updateOrder.setDeliveryName(fsStoreOrder.getDeliveryName());
+        updateOrder.setDeliverySn(fsStoreOrder.getDeliverySn());
+        updateOrder.setDeliveryTime(new DateTime());
+
+        return updateOrder;
+    }
+
 
     /**
      * 构建快递信息更新对象
@@ -4884,6 +5078,29 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         return updateOrder;
     }
+    private FsStoreOrderScrm buildDeliveryInfoUpdateScrm(FsStoreOrder fsStoreOrder) {
+        FsStoreOrderScrm updateOrder = new FsStoreOrderScrm();
+        updateOrder.setId(fsStoreOrder.getOrderId());
+        updateOrder.setDeliveryCode(fsStoreOrder.getDeliveryCode());
+        updateOrder.setDeliveryName(fsStoreOrder.getDeliveryName());
+        updateOrder.setDeliverySn(fsStoreOrder.getDeliverySn());
+        updateOrder.setUpdateTime(new DateTime());
+        // 注意:这里不更新状态和发货时间
+
+        return updateOrder;
+    }
+    private FsIntegralOrder buildDeliveryInfoUpdateIntegral(FsStoreOrder fsStoreOrder) {
+        FsIntegralOrder updateOrder = new FsIntegralOrder();
+        updateOrder.setOrderId(fsStoreOrder.getOrderId());
+        updateOrder.setDeliveryCode(fsStoreOrder.getDeliveryCode());
+        updateOrder.setDeliveryName(fsStoreOrder.getDeliveryName());
+        updateOrder.setDeliverySn(fsStoreOrder.getDeliverySn());
+        updateOrder.setUpdateTime(new DateTime());
+        // 注意:这里不更新状态和发货时间
+
+        return updateOrder;
+    }
+
 
     /**
      * 保存订单日志
@@ -4898,7 +5115,26 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         return fsStoreOrderLogsMapper.insertFsStoreOrderLogs(logs);
     }
+    private int saveOrderLogScrm(FsStoreOrder fsStoreOrder, String operator, String message) {
+        FsStoreOrderLogsScrm logs = new FsStoreOrderLogsScrm();
+        logs.setChangeMessage(message);
+        logs.setOrderId(fsStoreOrder.getOrderId());
+        logs.setChangeTime(new DateTime());
+        logs.setChangeType(LOG_TYPE_DELIVERY);
+        logs.setOperator(operator);
 
+        return fsStoreOrderLogsScrmMapper.insertFsStoreOrderLogs(logs);
+    }
+    private int saveOrderLogIntegral(FsStoreOrder fsStoreOrder, String operator, String message) {
+        FsIntegralOrderLogs logs = new FsIntegralOrderLogs();
+        logs.setChangeMessage(message);
+        logs.setOrderId(fsStoreOrder.getOrderId());
+        logs.setChangeTime(LocalDateTime.now());
+        logs.setChangeType(LOG_TYPE_DELIVERY);
+        logs.setOperator(operator);
+
+        return fsIntegralOrderLogsMapper.insertFsIntegralOrderLogs(logs);
+    }
     /**
      * 订阅快递信息
      */
@@ -4966,4 +5202,153 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
     public BigDecimal selectPayPriceByYear(String userId){
         return fsStoreOrderItemMapper.selectPayPriceByYear(userId);
     }
+
+    @Override
+    public void weizouPushIntergral(Long l) {
+        FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderId(l);
+        WeizouApiPushOrderParam param = new WeizouApiPushOrderParam();
+        param.setRealName(order.getUserName()).setPhone(order.getUserPhone().length() > 11 ? decryptPhone(order.getUserPhone()) : order.getUserPhone())
+                .setExtendOrderId(order.getOrderCode())
+        ;
+        // 积分订单全款
+            param.setPaymentType("01").setPaid(1)
+                    .setPayType("02").setDeliveryType("顺丰快递")
+//                    .setPayType("01").setDeliveryType("邮政快递")
+            ;
+        com.alibaba.fastjson.JSONArray goods = com.alibaba.fastjson.JSONArray.parseArray(order.getItemJson());
+        Set<Long> goodIds =
+        goods.stream().map(item -> ((com.alibaba.fastjson.JSONObject) item).getLong("goodsId")).collect(Collectors.toSet());
+        List<FsIntegralGoodsVo> fsIntegralGoodsVos = fsIntegralGoodsMapper.selectAllByGoodsIds(goodIds);
+        Map<Long, FsIntegralGoodsVo> goodsMap = fsIntegralGoodsVos.stream().collect(Collectors.toMap(FsIntegralGoodsVo::getGoodsId, Function.identity()));
+        //商品
+        ArrayList<WeizouOrderEntry> orders = new ArrayList<>();
+        goods.forEach(item -> {
+            WeizouOrderEntry entry = new WeizouOrderEntry();
+            FsIntegralGoodsVo good = goodsMap.get(((com.alibaba.fastjson.JSONObject) item).getLong("goodsId"));
+            entry.setSkuNumber(good.getBarCode()).setRetailPrice(good.getOtPrice())
+                    .setTaxPrice(good.getCash()).setTaxTotalAmount(good.getCash().multiply(new BigDecimal(((com.alibaba.fastjson.JSONObject) item).getInteger("num"))))
+                    .setCount(((com.alibaba.fastjson.JSONObject) item).getInteger("num"));
+            orders.add(entry);
+        });
+        //设置地址-省市区
+        String[] address = order.getUserAddress().split(" ");
+        try{
+            if (address.length < 3) {
+                String kdnAddress = fsUserAddressService.getKdnAddress(order.getUserAddress());
+                Map<String, Object> addDAta = (Map<String, Object>) JSON.parse(kdnAddress);
+                Map<String, String> add = (Map<String, String>) addDAta.get("Data");
+                param.setProvince(add.get("ProvinceName")).setCity(add.get("CityName"))
+                        .setDistrict(add.get("ExpAreaName"))
+                        .setUserAddress(add.get("StreetName") + add.get("Address"));
+
+            } else {
+                param.setProvince(address[0]).setCity(address[1]).setDistrict(address[2]);
+                //处理地址多空格问题
+                if (address.length > 3) {
+                    StringBuffer addrs = new StringBuffer();
+                    for (int i = 3; i < address.length; i++) {
+                        addrs.append(address[i]);
+                    }
+                    param.setUserAddress(addrs.toString());
+                } else {
+                    param.setUserAddress(address[2]);
+                }
+            }
+        }catch (Exception e){
+            log.error("地址错误:{}",e);
+            throw new CustomException("地址格式不对请正确写入详细地址!!");
+        }
+        param.setEntryList(orders);
+        String s ="";
+        try {
+            s = WeizouApiClient.pushOrder(param);
+
+        }catch (IOException e){
+            log.error("微走推送异常:{}",e);
+            return;
+        }
+        log.info("微走推送结果:{}",s);
+    }
+
+    @Override
+    public void weizouPushScrm(Long l) {
+        FsStoreOrderScrm order = fsStoreOrderScrmMapper.selectFsStoreOrderById(l);
+        if (order.getShippingType() != null && order.getShippingType() == 2)return;
+        WeizouApiPushOrderParam param = new WeizouApiPushOrderParam();
+        param.setRealName(order.getRealName()).setPhone(order.getUserPhone().length() > 11 ? decryptPhone(order.getUserPhone()) : order.getUserPhone())
+                .setFreightPrice(order.getFreightPrice()).setExtendOrderId(order.getOrderCode())
+        ;
+        //全款
+        if (order.getPayType().equals(1)) {
+            param.setPaymentType("01").setPaid(1)
+                    .setPayType("02").setDeliveryType("顺丰快递")
+//                    .setPayType("01").setDeliveryType("邮政快递")
+            ;
+            //部分支付
+        } else if (order.getPayType().equals(2)) {
+            param.setPaymentType("02").setPaid(2).setPrecollection(order.getPayPrice())
+                    .setPayType("04").setDeliveryType("顺丰快递")
+//                    .setPayType("03").setDeliveryType("邮政快递")
+            ;
+            //未付款
+        } else if (order.getPayType().equals(3)) {
+            param.setPaymentType("02").setPaid(0)
+                    .setPayType("04").setDeliveryType("顺丰快递")
+//                    .setPayType("03").setDeliveryType("邮政快递")
+            ;
+        }
+        //商品
+        List<com.fs.hisStore.vo.FsStoreOrderItemVO> items = fsStoreOrderItemScrmMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+        ArrayList<WeizouOrderEntry> orders = new ArrayList<>();
+
+        items.forEach(item -> {
+            FsStoreProductScrm product = fsStoreProductScrmMapper.selectFsStoreProductById(item.getProductId());
+
+            WeizouOrderEntry entry = new WeizouOrderEntry();
+
+            entry.setSkuNumber(product.getBarCode()).setRetailPrice(product.getPrice())
+                    .setTaxPrice(product.getPrice()).setTaxTotalAmount(product.getPrice().multiply(new BigDecimal(item.getNum())))
+                    .setCount(Integer.parseInt(item.getNum().toString()));
+            orders.add(entry);
+        });
+
+        //设置地址-省市区
+        String[] address = order.getUserAddress().split(" ");
+        try{
+            if (address.length < 3) {
+                String kdnAddress = fsUserAddressService.getKdnAddress(order.getUserAddress());
+                Map<String, Object> addDAta = (Map<String, Object>) JSON.parse(kdnAddress);
+                Map<String, String> add = (Map<String, String>) addDAta.get("Data");
+                param.setProvince(add.get("ProvinceName")).setCity(add.get("CityName"))
+                        .setDistrict(add.get("ExpAreaName"))
+                        .setUserAddress(add.get("StreetName") + add.get("Address"));
+
+            } else {
+                param.setProvince(address[0]).setCity(address[1]).setDistrict(address[2]);
+                //处理地址多空格问题
+                if (address.length > 3) {
+                    StringBuffer addrs = new StringBuffer();
+                    for (int i = 3; i < address.length; i++) {
+                        addrs.append(address[i]);
+                    }
+                    param.setUserAddress(addrs.toString());
+                } else {
+                    param.setUserAddress(address[2]);
+                }
+            }
+        }catch (Exception e){
+            log.error("地址错误:{}",e);
+            throw new CustomException("地址格式不对请正确写入详细地址!!");
+        }
+        param.setEntryList(orders);
+        String s ="";
+        try {
+            s = WeizouApiClient.pushOrder(param);
+
+        }catch (IOException e){
+            log.error("微走推送异常:{}",e);
+            return;
+        }
+        log.info("微走推送结果:{}",s);
+    }
 }

+ 3 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsVo.java

@@ -47,4 +47,7 @@ public class FsIntegralGoodsVo {
 
     @ApiModelProperty("商品数量")
     private Integer quantity;
+
+    @ApiModelProperty("需支付金额")
+    private BigDecimal cash;
 }

+ 1 - 0
fs-service/src/main/java/com/fs/his/vo/OptionsVO.java

@@ -8,4 +8,5 @@ public class OptionsVO implements Serializable {
     Long dictValue;
     String dictLabel;
     String dictImgUrl;
+    Integer isDel;
 }

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

@@ -3,7 +3,9 @@ package com.fs.hisStore.domain;
 import java.math.BigDecimal;
 import java.util.Date;
 
+import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
@@ -21,6 +23,7 @@ public class FsStoreOrderScrm extends BaseEntity
     private static final long serialVersionUID = 1L;
 
     /** 订单ID */
+    @TableId(type = IdType.AUTO)
     private Long id;
 
     /** 订单号 */
@@ -203,6 +206,8 @@ public class FsStoreOrderScrm extends BaseEntity
     private String itemJson;
 
     // 直播订单类型:2
+    /**订单类型(0商城,2直播,3点播,5直播中奖,6秒杀,7限时折扣)
+     */
     private Integer orderType;
 
     private Long packageId;
@@ -420,4 +425,7 @@ public class FsStoreOrderScrm extends BaseEntity
     /** 产品是否标记不分润(不入库;分佣流程传参,佣金按 0 入账并仍写公司流水) */
     @TableField(exist = false)
     private Boolean noCommission;
+
+    //直播间id
+    private Long liveId;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -327,4 +327,6 @@ public interface FsStoreAfterSalesScrmMapper
     @Select("SELECT s.*, s.reason_level1_text AS reasonValue1, s.reason_level2_text AS reasonValue2 " +
             "FROM fs_store_after_sales_scrm s WHERE s.id = #{id}")
     FsStoreAfterSalesScrm selectFsStoreAfterSalesByIdForDetail(@Param("id") Long id);
+
+    List<FsStoreAfterSalesQueryVO> selectFsStoreAfterSalesListQueryNew(@Param("maps") FsStoreAfterSalesQueryParam storeAfterSalesParam);
 }

+ 28 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -479,7 +479,7 @@ public interface FsStoreOrderScrmMapper
     List<FsPromotionOrderVO> selectFsPromotionOrderListVO(@Param("maps")FsStoreOrderParam param);
 
     @Select({"<script> " +
-            "select o.id,o.order_code,o.item_json,o.pay_price,o.status,o.is_package,o.package_json,o.delivery_id,o.finish_time,o.company_id,o.company_user_id  from fs_store_order_scrm o  " +
+            "select o.id,o.order_code,o.item_json,o.pay_price,o.status,o.is_package,o.package_json,o.delivery_id,o.finish_time,o.company_id,o.company_user_id,o.live_id  from fs_store_order_scrm o  " +
             "where o.is_del=0 and o.is_sys_del=0 " +
             "<if test = 'maps.status != null and maps.status != \"\"     '> " +
             "and o.status =#{maps.status} " +
@@ -1557,6 +1557,33 @@ public interface FsStoreOrderScrmMapper
      */
     @Select("SELECT * FROM fs_store_order_scrm WHERE status = 0 AND paid = 0 AND create_time < DATE_SUB(NOW(), INTERVAL #{unPayTime} MINUTE)")
     List<FsStoreOrderScrm> selectUnpayTimeoutOrderList(@Param("unPayTime") Integer unPayTime);
+    @Select({"<script> " +
+            "select o.id,o.order_code,o.item_json,o.pay_price,o.status,o.is_package,o.package_json,o.delivery_id,o.finish_time,o.company_id,o.company_user_id,o.live_id  from fs_store_order_scrm o  " +
+            "where o.is_del=0 and o.is_sys_del=0 " +
+            "<if test = 'maps.status != null and maps.status != \"\"     '> " +
+            "and o.status =#{maps.status} " +
+            "</if>" +
+            "<if test = 'maps.keyword != null and  maps.keyword !=\"\"    '> " +
+            "and o.order_code like CONCAT('%',#{maps.keyword},'%') " +
+            "</if>" +
+            "<if test = 'maps.deliveryStatus != null     '> " +
+            "and o.delivery_status =#{maps.deliveryStatus} " +
+            "</if>" +
+            "<if test = 'maps.liveId == null '> " +
+            "and o.live_id is null " +
+            "</if>" +
+            "<if test = 'maps.liveId != null and maps.liveId == 0 '> " +
+            "and o.live_id is not null " +
+            "</if>" +
+            "<if test = 'maps.userId != null     '> " +
+            "and o.user_id=#{maps.userId} " +
+            "</if>" +
+            "<if test = 'maps.appId != null and  maps.appId !=\"\"    '> " +
+            "and o.app_id =#{maps.appId} " +
+            "</if>" +
+            " order by o.id desc "+
+            "</script>"})
+    List<FsMyStoreOrderListQueryVO> selectFsMyStoreOrderListVONew(@Param("maps")FsMyStoreOrderQueryParam param);
 
     @Select("SELECT status FROM fs_store_order_scrm WHERE id = #{orderId}")
     FsStoreOrderScrm selectStatusById(@Param("orderId") Long orderId);

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

@@ -16,4 +16,6 @@ public class FsMyStoreOrderQueryParam extends BaseQueryParam implements Serializ
     private Integer deliveryStatus;
     @ApiModelProperty(value = "当前的appid")
     private String appId;
+    @ApiModelProperty(value = "直播间ID,null表示查询商城订单,0表示查询直播订单")
+    private Long liveId;
 }

+ 1 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreAfterSalesQueryParam.java

@@ -11,5 +11,6 @@ public class FsStoreAfterSalesQueryParam extends BaseQueryParam implements Seria
 
     private Integer status;
     private Long userId;
+    private Long liveId;
 
 }

+ 3 - 1
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java

@@ -48,11 +48,13 @@ public class FsStoreOrderCreateParam implements Serializable
     //订单创建类型 1普通订单 2套餐订单 3制单
     private Integer orderCreateType;
 
+    private Integer orderType; //订单类型(0商城,2直播,3点播,5直播中奖,6秒杀,7限时折扣)
+
     private Long customerId;
 
     private BigDecimal amount; //货到付款代收金额
 
-    private Integer orderType; //订单类型
+
     private Integer orderMedium; //媒体来源
 
     private Boolean isUserApp = true;

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

@@ -416,6 +416,8 @@ public interface IFsStoreOrderScrmService
 
     R zfbPayment(FsStoreOrderDoPayParam param);
 
+    List<FsMyStoreOrderListQueryVO> selectFsMyStoreOrderListVONew(FsMyStoreOrderQueryParam param);
+
 //    R getExpressMulti(FsStoreOrder order);
 
     R sendExpressInfoToWx(@PathVariable Long orderId);

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

@@ -626,7 +626,7 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
 
     @Override
     public List<FsStoreAfterSalesQueryVO> selectFsStoreAfterSalesListQuery(FsStoreAfterSalesQueryParam storeAfterSalesParam) {
-        List<FsStoreAfterSalesQueryVO>  list=fsStoreAfterSalesMapper.selectFsStoreAfterSalesListQuery(storeAfterSalesParam);
+        List<FsStoreAfterSalesQueryVO>  list=fsStoreAfterSalesMapper.selectFsStoreAfterSalesListQueryNew(storeAfterSalesParam);
         for(FsStoreAfterSalesQueryVO vo:list){
             FsStoreAfterSalesItemScrm map=new FsStoreAfterSalesItemScrm();
              map.setStoreAfterSalesId(vo.getId());

+ 47 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -1070,7 +1070,6 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 return R.error("地址不能为空!");
             }
         }
-
         FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
         BeanUtils.copyProperties(param, computedParam);
         //计算金额
@@ -1304,7 +1303,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
             // 商城订单归属绑定
             if (hisStoreConfig != null && Boolean.TRUE.equals(hisStoreConfig.getEnableStoreOrderAttribution())
-                    && storeOrder.getOrderType() == null ) {
+                    && storeOrder.getOrderType() == null && storeOrder.getCompanyUserId() == null) {
                 try {
                     bindStoreOrderAttribution(storeOrder, userId);
                 } catch (Exception e) {
@@ -3719,21 +3718,27 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     public R syncExpress(FsStoreOrderExpressEditParam param) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(param.getOrderId());
         String lastFourNumber = "";
+        ExpressInfoDTO dto = null;
         if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
             if("恒春来".equals(cloudHostProper.getCompanyName())
                     && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
                 if (lastFourNumber.contains("-")) {
-                    lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                    String beforeDash = lastFourNumber.split("-")[0];
+                    lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
                 }else{
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                 }
+                dto=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                if(dto!=null && !dto.isSuccess()){
+                    lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
+                }
             }
             // 原逻辑
             else if ((lastFourNumber = order.getUserPhone()).length() == 11) {
                 lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
             }
         }
-        ExpressInfoDTO dto = expressService.getExpressInfo(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
+        dto = expressService.getExpressInfo(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
         if (dto == null || dto.getState() == null || dto.getStateEx() == null) {
             return null;
         }
@@ -7209,6 +7214,44 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         return R.error();
     }
 
+    @Override
+    public List<FsMyStoreOrderListQueryVO> selectFsMyStoreOrderListVONew(FsMyStoreOrderQueryParam param) {
+        List<FsMyStoreOrderListQueryVO> list = fsStoreOrderMapper.selectFsMyStoreOrderListVONew(param);
+        for (FsMyStoreOrderListQueryVO vo : list) {
+//          List<FsStoreOrderItemVO> items=fsStoreOrderItemMapper.selectMyFsStoreOrderItemListByOrderId(vo.getId());
+//          vo.setItems(items);
+            if (StringUtils.isNotEmpty(vo.getItemJson())) {
+                JSONArray jsonArray = JSONUtil.parseArray(vo.getItemJson());
+                List<FsStoreOrderItemVO> items = JSONUtil.toList(jsonArray, FsStoreOrderItemVO.class);
+                if (items.size() > 0) {
+                    vo.setItems(items);
+                }
+            }
+            //处理是否可以申请售后
+            vo.setIsAfterSales(0);
+            if (vo.getStatus().equals(OrderInfoEnum.STATUS_3.getValue())) {
+                //已完成订单
+                vo.setIsAfterSales(1);
+                if (vo.getFinishTime() != null) {
+                    String json = configService.selectConfigByKey("store.config");
+                    StoreConfig storeConfig = JSONUtil.toBean(json, StoreConfig.class);
+                    if (storeConfig.getStoreAfterSalesDay() != null && storeConfig.getStoreAfterSalesDay() > 0) {
+                        //判断完成时间是否超过指定时间
+                        Calendar calendar = new GregorianCalendar();
+                        calendar.setTime(vo.getFinishTime());
+                        calendar.add(calendar.DATE, storeConfig.getStoreAfterSalesDay()); //把日期往后增加一天,整数  往后推,负数往前移动
+                        if (calendar.getTime().getTime() < new Date().getTime()) {
+                            vo.setIsAfterSales(0);
+                        }
+                    }
+                }
+            } else if (vo.getStatus() == 1 || vo.getStatus() == 2) {
+                vo.setIsAfterSales(1);
+            }
+        }
+        return list;
+    }
+
 
     /**
      * 综合参数

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsMyStoreOrderListQueryVO.java

@@ -47,5 +47,7 @@ public class FsMyStoreOrderListQueryVO implements Serializable
 
     private List<FsStoreOrderItemVO> items;
 
+    private Long liveId;
+
 
 }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesQueryVO.java

@@ -87,6 +87,8 @@ public class FsStoreAfterSalesQueryVO implements Serializable
 
     private String packageJson;
 
+    private Long liveId;
+
 
     List<FsStoreAfterSalesItemScrm> items;
 

+ 7 - 0
fs-service/src/main/java/com/fs/live/domain/LiveOrder.java

@@ -367,4 +367,11 @@ public class LiveOrder extends BaseEntity {
     @Excel(name = "小程序AppId")
     private String appId;
 
+    //订单创建类型 1普通订单 2套餐订单 3制单
+    @TableField(exist = false)
+    private Integer orderCreateType;
+
+    @TableField(exist = false)
+    private Integer orderType; //订单类型(0商城,2直播,3点播,5直播中奖,6秒杀,7限时折扣)
+
 }

+ 147 - 99
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -2181,88 +2181,141 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         // 更改店铺库存
         fsStoreProduct.setStock(fsStoreProduct.getStock() - Integer.parseInt(liveOrder.getTotalNum()));
         fsStoreProduct.setSales(fsStoreProduct.getSales() + Integer.parseInt(liveOrder.getTotalNum()));
-        fsStoreProductScrmMapper.incStockDecSales(Long.valueOf("-" + liveOrder.getTotalNum()), fsStoreProduct.getProductId());
+        fsStoreProductScrmMapper.incStockDecSales(Long.valueOf(liveOrder.getTotalNum()), fsStoreProduct.getProductId());
         // 更新直播间库存
         goods.setStock(goods.getStock() - Integer.parseInt(liveOrder.getTotalNum()));
         goods.setSales(goods.getSales() + Integer.parseInt(liveOrder.getTotalNum()));
         liveGoodsMapper.updateLiveGoods(goods);
 
+        // 复制到商城订单
+        FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+        copyLiveToStore(liveOrder, storeOrder);
+        storeOrder.setOrderCreateType(liveOrder.getOrderCreateType());
+        storeOrder.setLiveId(liveOrder.getLiveId());
+        storeOrder.setCartId(liveOrder.getCartId());
+
         //判断是否是三种特定产品
+        String storeHouseCode;
         if (fsStoreProduct.getProductId() != null && (fsStoreProduct.getProductId().equals(3168L)
                 || fsStoreProduct.getProductId().equals(3184L)
                 || fsStoreProduct.getProductId().equals(3185L))) {
-            liveOrder.setStoreHouseCode("YDSP001");
+            storeHouseCode = "YDSP001";
         } else {
-            liveOrder.setStoreHouseCode("CQDS001");
+            storeHouseCode = "CQDS001";
         }
 
+        storeOrder.setStoreHouseCode(storeHouseCode);
+
         LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
-        liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
-        liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
-        liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        if (ObjectUtil.isNotEmpty(liveUserFirstEntry)) {
+            storeOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
+            storeOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
+            storeOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        }
 
         String orderSn = SnowflakeUtil.nextIdStr();
         log.info("订单生成:" + orderSn);
-        liveOrder.setOrderCode(orderSn);
+        storeOrder.setOrderCode(orderSn);
         BigDecimal totalPrice = fsStoreProduct.getPrice().multiply(new BigDecimal(liveOrder.getTotalNum()));
-        // 直播不需要服务费 0915 1735 左
-//        String config=configService.selectConfigByKey("store.config");
-//        StoreConfig storeConfig= JSONUtil.toBean(config,StoreConfig.class);
-//        BigDecimal serviceFee=new BigDecimal(0);
-//        if(storeConfig.getServiceFee()!=null){
-//            if(liveOrder.getCompanyUserId()==null||liveOrder.getCompanyUserId()==0){
-//                serviceFee=storeConfig.getServiceFee();
-//            }
-//        }
-//        payPrice = payPrice.add(serviceFee);
+
         // 生成
         BigDecimal deliveryMoney = handleDeliveryMoney(liveOrder);
         if (deliveryMoney.compareTo(BigDecimal.valueOf(-1)) == 0) {
             return R.error("偏远地区暂不可购买");
         }
         totalPrice = totalPrice.add(deliveryMoney);
-        liveOrder.setDiscountMoney(totalPrice);
+        BigDecimal discountMoney = BigDecimal.ZERO;
+
+        // 设置商城订单字段(按照 createOrder 的逻辑)
+        storeOrder.setUserId(Long.parseLong(liveOrder.getUserId()));
+        storeOrder.setTotalNum(Long.parseLong(liveOrder.getTotalNum()));
+        storeOrder.setTotalPrice(totalPrice);
+        storeOrder.setTotalPostage(deliveryMoney);
+        storeOrder.setPayPostage(deliveryMoney);
+        storeOrder.setPayDelivery(deliveryMoney);
+        storeOrder.setCouponPrice(discountMoney);
+        storeOrder.setDeductionPrice(BigDecimal.ZERO);
+        storeOrder.setPaid(0);
+        storeOrder.setPayType(StringUtils.isEmpty(liveOrder.getPayType()) ? "1" :liveOrder.getPayType());
+        storeOrder.setUseIntegral(BigDecimal.ZERO);
+        storeOrder.setBackIntegral(BigDecimal.ZERO);
+        storeOrder.setGainIntegral(BigDecimal.ZERO);
+        storeOrder.setCost(BigDecimal.ZERO);
+        storeOrder.setIsChannel(1);
+        storeOrder.setShippingType(1);
+        storeOrder.setCreateTime(new Date());
+        storeOrder.setIsPrescribe(0);
+        storeOrder.setOrderType(5);
+        storeOrder.setStatus(OrderInfoEnum.STATUS_0.getValue());
+        storeOrder.setLiveId(liveOrder.getLiveId());
+
+        // 获取配置
+        String json = configService.selectConfigByKey("store.config");
+        StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+        if (config != null && config.getServiceFee() != null) {
+            storeOrder.setServiceFee(config.getServiceFee());
+        }
 
-        fsStoreProduct.setCost(BigDecimal.ZERO);
-        liveOrder.setItemJson(JSON.toJSONString(fsStoreProduct));
-        liveOrder.setCreateTime(new Date());
-        liveOrder.setUpdateTime(new Date());
-        liveOrder.setPayDelivery(deliveryMoney);
-        liveOrder.setPayPostage(deliveryMoney);
-        liveOrder.setProductId(fsStoreProduct.getProductId());
-        liveOrder.setStatus(OrderInfoEnum.STATUS_0.getValue());
-        liveOrder.setPayType("1");
-        liveOrder.setTotalPrice(totalPrice);
-        liveOrder.setPayMoney(BigDecimal.ZERO);
         try {
-            if (baseMapper.insertLiveOrder(liveOrder) > 0) {
-                liveUserLotteryRecord.setOrderId(liveOrder.getOrderId());
-                liveUserLotteryRecord.setOrderStatus(liveOrder.getStatus());
-                liveUserLotteryRecordMapper.updateLiveUserLotteryRecord(liveUserLotteryRecord);
-                LiveOrderItemDTO dto = new LiveOrderItemDTO();
-                dto.setImage(fsStoreProduct.getImage());
-                dto.setSku(String.valueOf(fsStoreProduct.getStock()));
-                if (StringUtils.isEmpty(fsStoreProduct.getBarCode())) {
-                    FsStoreProductAttrValueScrm fsStoreProductAttrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(fsStoreProduct.getProductId()).stream().filter(attr -> StringUtils.isNotEmpty(attr.getBarCode())).findFirst().orElse(null);
-                    if (fsStoreProductAttrValue != null) {
-                        dto.setBarCode(fsStoreProductAttrValue.getBarCode());
-                    }
+            int i = fsStoreOrderScrmMapper.insertFsStoreOrder(storeOrder);
+            if (i > 0) {
+
+                // 保存订单明细
+                FsStoreCartDTO fsStoreCartDTO = new FsStoreCartDTO();
+                fsStoreCartDTO.setProductId(fsStoreProduct.getProductId());
+                fsStoreCartDTO.setPrice(attrValue != null ? attrValue.getPrice() : fsStoreProduct.getPrice());
+                fsStoreCartDTO.setSku(attrValue != null ? (attrValue.getSku() != null ? attrValue.getSku() : "") : "");
+                fsStoreCartDTO.setProductName(fsStoreProduct.getProductName());
+                fsStoreCartDTO.setNum(Integer.parseInt(liveOrder.getTotalNum()));
+                fsStoreCartDTO.setImage(fsStoreProduct.getImage());
+
+                if (attrValue != null) {
+                    fsStoreCartDTO.setBarCode(attrValue.getBarCode());
+                    fsStoreCartDTO.setGroupBarCode(attrValue.getGroupBarCode());
+                }
+
+                FsStoreOrderItemScrm orderItem = new FsStoreOrderItemScrm();
+                orderItem.setOrderId(storeOrder.getId());
+                orderItem.setOrderCode(orderSn);
+                orderItem.setProductId(fsStoreProduct.getProductId());
+                orderItem.setProductAttrValueId(attrValue != null ? attrValue.getId() : null);
+                orderItem.setJsonInfo(JSONUtil.toJsonStr(fsStoreCartDTO));
+                orderItem.setNum(Integer.parseInt(liveOrder.getTotalNum()));
+                orderItem.setIsAfterSales(0);
+                orderItem.setIsPrescribe(0);
+                fsStoreOrderItemScrmMapper.insertFsStoreOrderItem(orderItem);
+
+                // 更新订单的 itemJson
+                List<FsStoreOrderItemScrm> listOrderItem = new ArrayList<>();
+                listOrderItem.add(orderItem);
+                String itemJson = JSONUtil.toJsonStr(listOrderItem);
+                storeOrder.setItemJson(itemJson);
+                fsStoreOrderScrmMapper.updateFsStoreOrder(storeOrder);
+
+                // 添加订单日志
+                orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
+                        OrderLogEnum.CREATE_ORDER.getDesc());
+
+                // 设置直播订单的 orderId 为商城订单的 id
+
+                // 加入redis,24小时自动取消
+                String redisKey = StoreConstants.REDIS_ORDER_OUTTIME_UNPAY + storeOrder.getId();
+                if (config != null && config.getUnPayTime() != null && config.getUnPayTime() > 0) {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
                 } else {
-                    dto.setBarCode(fsStoreProduct.getBarCode());
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
                 }
-                dto.setPrice(fsStoreProduct.getPrice());
-                dto.setProductName(fsStoreProduct.getProductName());
-                dto.setNum(Long.valueOf(liveOrder.getTotalNum()));
 
-                LiveOrderItem liveOrderItem = new LiveOrderItem();
-                liveOrderItem.setOrderCode(liveOrder.getOrderCode());
-                liveOrderItem.setOrderId(liveOrder.getOrderId());
-                liveOrderItem.setProductId(liveOrder.getProductId());
-                liveOrderItem.setNum(Long.valueOf(liveOrder.getTotalNum()));
-                liveOrderItem.setJsonInfo(JSON.toJSONString(dto));
-                liveOrderItemMapper.insertLiveOrderItem(liveOrderItem);
                 redisCache.deleteObject("orderKey:" + liveOrder.getOrderKey());
-                return R.ok("下单成功").put("order", liveOrder);
+                //添加支付到期时间
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(storeOrder.getCreateTime());
+                if (config != null && config.getUnPayTime() != null) {
+                    calendar.add(Calendar.MINUTE, config.getUnPayTime());
+                }
+                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                String payLimitTime = format.format(calendar.getTime());
+                return R.ok("下单成功").put("order", storeOrder).put("payLimitTime", payLimitTime);
             } else {
                 return R.error("订单创建失败");
             }
@@ -3168,8 +3221,10 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         String appId = liveOrder.getAppId();
         Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
         try {
-            liveOrder = baseMapper.selectLiveOrderByOrderId(String.valueOf(orderId));
-            if (liveOrder == null || !liveOrder.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())) {
+            // 查询商城订单
+            FsStoreOrderScrm storeOrder = fsStoreOrderScrmService.selectFsStoreOrderById(orderId);
+
+            if (storeOrder == null || !storeOrder.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())) {
                 throw new CustomException("当前订单未找到或者订单状态不为待支付! orderId:" + orderId);
             }
             // 设置appId
@@ -3178,9 +3233,6 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             }
             FsUserScrm user = userMapper.selectFsUserById(Long.valueOf(liveOrder.getUserId()));
             if (user == null) return R.error("用户不存在");
-//            String json = configService.selectConfigByKey("store.pay");
-//            String json = configService.selectConfigByKey("his.pay");
-//            FsPayConfig fsPayConfig = JSON.parseObject(json, FsPayConfig.class);
             FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(appId);
             if (fsCoursePlaySourceConfig == null) {
                 throw new CustomException("未找到appId对应的小程序配置: " + appId);
@@ -3196,56 +3248,49 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             if (StringUtils.isEmpty(payCode)) {
                 return R.error("订单生成失败,请重试");
             }
-            LiveOrderPayment storePayment = liveOrderPaymentMapper.selectByBuissnessId(liveOrder.getOrderId());
-            storePayment.setAppId(liveOrder.getAppId());
-            if (storePayment != null) {
-                storePayment.setStatus(1);
-                liveOrderPaymentMapper.updateLiveOrderPayment(storePayment);
-            } else {
-                storePayment = new LiveOrderPayment();
-                storePayment.setStatus(1);
+
+            // 查询商城支付记录
+            List<FsStorePaymentScrm> paymentList = fsStorePaymentScrmMapper.selectFsStorePaymentByOrderId(orderId);
+            if (paymentList != null && !paymentList.isEmpty()) {
+                // 检查是否有已支付的支付记录
+                for (FsStorePaymentScrm storePayment : paymentList) {
+                    if (storePayment != null) {
+                        storePayment.setStatus(1);
+                        storePayment.setAppId(liveOrder.getAppId());
+                        storePayment.setPaymentId(storePayment.getPaymentId());
+                        storePayment.setPayTime(new Date());
+                        fsStorePaymentScrmMapper.updateFsStorePayment(storePayment);
+                    }
+                }
+            }else{
+                FsStorePaymentScrm storePayment = new FsStorePaymentScrm();
+                storePayment.setCompanyId(storeOrder.getCompanyId());
+                storePayment.setCompanyUserId(storeOrder.getCompanyUserId());
                 storePayment.setPayMode(merchantAppConfig.getMerchantType());
-                storePayment.setBusinessCode(liveOrder.getOrderCode());
+                storePayment.setStatus(1);
                 storePayment.setPayCode(payCode);
-                storePayment.setPayMoney(liveOrder.getPayMoney());
-//                storePayment.setPayMoney(new BigDecimal("0.01"));
+                storePayment.setPayMoney(storeOrder.getPayMoney());
                 storePayment.setCreateTime(new Date());
                 storePayment.setPayTypeCode("weixin");
-                storePayment.setBusinessType(5);
-                storePayment.setRemark("直播商品订单支付");
-                storePayment.setOpenId(user.getMaOpenId());
+                storePayment.setBusinessType(9);
+                storePayment.setRemark("直播订单支付");
+                storePayment.setOpenId(user.getRealName());
                 storePayment.setUserId(user.getUserId());
-                storePayment.setBusinessId(liveOrder.getOrderId().toString());
-                storePayment.setAppId(appId);
+                storePayment.setBusinessOrderId(storeOrder.getId().toString());
+                storePayment.setOrderId(storeOrder.getId());
+                storePayment.setAppId(fsCoursePlaySourceConfig.getAppid() == null ? "" : fsCoursePlaySourceConfig.getAppid());
                 storePayment.setMerConfigId(merchantAppConfig.getId());
-                liveOrderPaymentMapper.insertLiveOrderPayment(storePayment);
-            }
-
-            if (storePayment != null) {
-                if (storePayment.getStatus().equals(0)) {
-                    LiveOrderPayment paymentMap = new LiveOrderPayment();
-                    paymentMap.setPaymentId(storePayment.getPaymentId());
-                    paymentMap.setStatus(1);
-                    paymentMap.setPayTime(new Date());
-                    liveOrderPaymentMapper.updateLiveOrderPayment(paymentMap);
-                }
-            } else {
-                log.info("支付单号不存在:" + payCode);
-                throw new CustomException("当前支付记录未找到!");
-            }
-            if (liveOrder.getStatus() != 1) {
-                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                return R.error("当前订单未找到或者订单状态不为待支付!");
+                storePayment.setBusinessCode(storeOrder.getOrderCode());
+                fsStorePaymentScrmMapper.insertFsStorePayment(storePayment);
             }
             //增加用户购买次数
-            userMapper.incPayCount(Long.valueOf(liveOrder.getUserId()));
+            userMapper.incPayCount(storeOrder.getUserId());
             //更新用户下单次数和累计成交总额
-            userMapper.updateUserOrderCountAndAmount(Long.valueOf(liveOrder.getUserId()), liveOrder.getPayMoney());
-
-            liveOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
-            liveOrder.setPayTime(LocalDateTime.now());
-            liveUserLotteryRecordMapper.updateOrderStatusByOrderId(liveOrder.getOrderId(), 2);
-            baseMapper.updateLiveOrder(liveOrder);
+            userMapper.updateUserOrderCountAndAmount(storeOrder.getUserId(), storeOrder.getPayMoney());
+            storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+            storeOrder.setPayTime(new Date());
+            liveUserLotteryRecordMapper.updateOrderStatusByOrderId(storeOrder.getId(), 2);
+            fsStoreOrderScrmService.updateFsStoreOrder(storeOrder);
             return R.ok("支付成功");
         } catch (Exception e) {
             log.info("抽奖订单支付错误:" + e.getMessage());
@@ -4053,6 +4098,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
         copyLiveToStore(liveOrder, storeOrder);
 
+        storeOrder.setOrderCreateType(liveOrder.getOrderCreateType());
+
         //判断是否是三种特定产品
         String storeHouseCode;
         if (fsStoreProduct.getProductId() != null && (fsStoreProduct.getProductId().equals(3168L)
@@ -4131,6 +4178,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         storeOrder.setCreateTime(new Date());
         storeOrder.setIsPrescribe(0);
         storeOrder.setOrderType(2);
+        storeOrder.setLiveId(liveOrder.getLiveId());
 
         // 获取配置
         String json = configService.selectConfigByKey("store.config");

+ 33 - 0
fs-service/src/main/java/com/fs/qw/bo/SendMsgLogBo.java

@@ -0,0 +1,33 @@
+package com.fs.qw.bo;
+
+import lombok.Data;
+
+
+@Data
+public class SendMsgLogBo {
+
+    //短信发送记录id
+    private Long companySmsLogsId;
+
+    //获客链接主键
+    private Long qwAcquisitionId;
+
+    //公司id
+    private Long companyId;
+
+    //客户id
+    private Long customerId;
+
+    //公司用户id
+    private Long companyUserId;
+
+    //外部联系人id
+    private Long externalId;
+
+    // 课程id
+    private Long courseId;
+
+    // 视频id
+    private Long videoId;
+
+}

+ 77 - 0
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionLinkInfo.java

@@ -0,0 +1,77 @@
+package com.fs.qw.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+
+/**
+ * 获客链接-号码链接生成记录对象 qw_acquisition_link_info
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+@Data
+public class QwAcquisitionLinkInfo {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 获客链接管理主键ID
+     */
+    private Long qwAcquisitionAssistantId;
+
+    /**
+     * 完整链接
+     */
+    private String link;
+
+    /**
+     * 客户电话
+     */
+    private String phone;
+
+    /**
+     * 随机字符串
+     */
+    private String randomStr;
+
+    /**
+     * 创建人
+     */
+    private Long createBy;
+
+    /**
+     * 创建时间
+     * */
+    private String createTime;
+
+    /**
+     * 更新人
+     */
+    private Long updateBy;
+
+    /**
+     * 更新时间
+     * */
+    private String updateTime;
+
+    /**
+     * 备注
+     * */
+    private String remark;
+
+    /**
+     * 链接名称
+     * */
+    @TableField(exist = false)
+    private String linkName;
+
+    /**
+     * 创建人名称
+     * */
+    @TableField(exist = false)
+    private String createName;
+}

+ 55 - 0
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionSendMsgLog.java

@@ -0,0 +1,55 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 企微-获客链接短信发送记录日志对象 qw_acquisition_send_msg_log
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@Data
+public class QwAcquisitionSendMsgLog extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 短信发送记录主键id */
+    @Excel(name = "短信发送记录主键id")
+    private Long companySmsLogsId;
+
+    /** 获客链接管理ID */
+    @Excel(name = "获客链接管理ID")
+    private Long qwAcquisitionId;
+
+    /** 客户电话 */
+    @Excel(name = "客户电话")
+    private String phone;
+
+    /** 短信数量,超过67个字符为2条短信 */
+    @Excel(name = "短信数量")
+    private Integer number;
+
+    /** 短信模板id */
+    @Excel(name = "短信模板id")
+    private Long tempId;
+
+    /** 短信服务商类型 */
+    @Excel(name = "短信服务商类型")
+    private String type;
+
+    /** 短信内容 */
+    @Excel(name = "短信内容")
+    private String content;
+
+    /** 发送结果 */
+    @Excel(name = "发送结果")
+    private String result;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+}

+ 64 - 0
fs-service/src/main/java/com/fs/qw/domain/QwCourseLinkSendMsgLog.java

@@ -0,0 +1,64 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 企微-发送看课链接短信记录日志对象 qw_course_link_send_msg_log
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@Data
+public class QwCourseLinkSendMsgLog extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 短信发送记录主键id */
+    @Excel(name = "短信发送记录主键id")
+    private Long companySmsLogsId;
+
+    /** 课程id */
+    @Excel(name = "课程id")
+    private Long courseId;
+
+    /** 视频id */
+    @Excel(name = "视频id")
+    private Long videoId;
+
+    /** 短信数量,超过67个字符为2条短信 */
+    @Excel(name = "短信数量")
+    private Integer number;
+
+    /** 短信模板id */
+    @Excel(name = "短信模板id")
+    private Long tempId;
+
+    /** 外部联系人ID */
+    @Excel(name = "外部联系人ID")
+    private Long externalContactId;
+
+    /** 短信服务商类型 */
+    @Excel(name = "短信服务商类型")
+    private String type;
+
+    /** 客户电话 */
+    @Excel(name = "客户电话")
+    private String phone;
+
+    /** 短信内容 */
+    @Excel(name = "短信内容")
+    private String content;
+
+    /** 发送结果 */
+    @Excel(name = "发送结果")
+    private String result;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+}

+ 32 - 0
fs-service/src/main/java/com/fs/qw/dto/BatchAddAcquisitionLinkDTO.java

@@ -0,0 +1,32 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class BatchAddAcquisitionLinkDTO {
+
+    /** 公司id */
+    private Long companyId;
+
+    /**
+     * 创建人
+     */
+    private Long createBy;
+
+    /**
+     * 获客链接管理主键ID
+     */
+    private Long qwAcquisitionAssistantId;
+
+    /**
+     * 获客链接管理url
+     */
+    private String qwAcquisitionAssistantUrl;
+
+    /**
+     * 客户电话列表
+     */
+    private List<String> phoneList;
+}

+ 45 - 0
fs-service/src/main/java/com/fs/qw/dto/IpadBlindAddDto.java

@@ -0,0 +1,45 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+/**
+ * iPad盲加请求参数DTO
+ */
+@Data
+public class IpadBlindAddDto {
+
+    /**
+     * 链接ID(企微返回的linkId)
+     */
+    @NotBlank(message = "链接ID不能为空")
+    private String linkId;
+
+    /**
+     * 所在主体id
+     */
+    @NotBlank(message = "企业主体ID不能为空")
+    private String corpId;
+
+    /**
+     * 获客链接主键ID
+     */
+    @NotNull(message = "获客链接ID不能为空")
+    private Long qwAcquisitionAssistantId;
+
+    /**
+     * 选中的成员ID(本地数据库ID)
+     */
+    @NotNull(message = "成员ID不能为空")
+    private Long userId;
+
+    /**
+     * 手机号码
+     */
+    @NotBlank(message = "手机号码不能为空")
+    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码格式不正确")
+    private String phone;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/qw/enums/SmsLogType.java

@@ -0,0 +1,33 @@
+package com.fs.qw.enums;
+
+/**
+ * 短信发送日志类型枚举
+ * 用于在通用发送接口中区分不同的日志记录行为
+ */
+public enum SmsLogType {
+    /**
+     * 获客链接短信
+     */
+    ACQUISITION_LINK("ACQUISITION_LINK", "获客链接短信"),
+
+    /**
+     * 看课链接短信
+     */
+    COURSE_LINK("COURSE_LINK", "看课链接短信");
+
+    private final String code;
+    private final String info;
+
+    SmsLogType(String code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getInfo() {
+        return info;
+    }
+}

+ 91 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionLinkInfoMapper.java

@@ -0,0 +1,91 @@
+package com.fs.qw.mapper;
+
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 获客链接-号码链接生成记录Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+public interface QwAcquisitionLinkInfoMapper
+{
+    /**
+     * 查询获客链接-号码链接生成记录
+     *
+     * @param id 获客链接-号码链接生成记录主键
+     * @return 获客链接-号码链接生成记录
+     */
+    public QwAcquisitionLinkInfo selectQwAcquisitionLinkInfoById(Long id);
+
+    /**
+     * 查询获客链接-号码链接生成记录列表
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 获客链接-号码链接生成记录集合
+     */
+    public List<QwAcquisitionLinkInfo> selectQwAcquisitionLinkInfoList(QwAcquisitionLinkInfo qwAcquisitionLinkInfo);
+
+    /**
+     * 新增获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 结果
+     */
+    public int insertQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo);
+
+    /**
+     * 修改获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 结果
+     */
+    public int updateQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo);
+
+    /**
+     * 删除获客链接-号码链接生成记录
+     *
+     * @param id 获客链接-号码链接生成记录主键
+     * @return 结果
+     */
+    public int deleteQwAcquisitionLinkInfoById(Long id);
+
+    /**
+     * 批量删除获客链接-号码链接生成记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionLinkInfoByIds(Long[] ids);
+
+    /**
+     * 批量删除获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionAssistantIds 需要删除的获客链接管理主键ID集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(@Param("qwAcquisitionAssistantIds") Long[] qwAcquisitionAssistantIds);
+
+    /**
+     * 查询已存在的随机字符串
+     * */
+    List<String> selectAllRandomStr();
+
+    /**
+     * 根据随机字符串查询获客链接
+     * */
+    String selectQwAcquisitionUrlByRandomStr(String randomStr);
+
+    /**
+     *  根据主键id列表批量查询获客链接记录列表
+     * */
+    List<QwAcquisitionLinkInfo> selectAcquisitionLinkInfoListByIds(@Param("ids")Long[] ids);
+
+    /**
+     *  根据获客链接管理主键id列表批量查询获客链接记录列表
+     * */
+    List<QwAcquisitionLinkInfo> selectAcquisitionLinkInfoListByAcquisitionAssistantIds(@Param("qwAcquisitionAssistantIds")Long[] qwAcquisitionAssistantIds);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionSendMsgLogMapper.java

@@ -0,0 +1,62 @@
+package com.fs.qw.mapper;
+
+import com.fs.qw.domain.QwAcquisitionSendMsgLog;
+
+import java.util.List;
+
+/**
+ * 企微-获客链接短信发送记录日志Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+public interface QwAcquisitionSendMsgLogMapper
+{
+    /**
+     * 查询企微-获客链接短信发送记录日志
+     *
+     * @param id 企微-获客链接短信发送记录日志主键
+     * @return 企微-获客链接短信发送记录日志
+     */
+    public QwAcquisitionSendMsgLog selectQwAcquisitionSendMsgLogById(Long id);
+
+    /**
+     * 查询企微-获客链接短信发送记录日志列表
+     *
+     * @param qwAcquisitionSendMsgLog 企微-获客链接短信发送记录日志
+     * @return 企微-获客链接短信发送记录日志集合
+     */
+    public List<QwAcquisitionSendMsgLog> selectQwAcquisitionSendMsgLogList(QwAcquisitionSendMsgLog qwAcquisitionSendMsgLog);
+
+    /**
+     * 新增企微-获客链接短信发送记录日志
+     *
+     * @param qwAcquisitionSendMsgLog 企微-获客链接短信发送记录日志
+     * @return 结果
+     */
+    public int insertQwAcquisitionSendMsgLog(QwAcquisitionSendMsgLog qwAcquisitionSendMsgLog);
+
+    /**
+     * 修改企微-获客链接短信发送记录日志
+     *
+     * @param qwAcquisitionSendMsgLog 企微-获客链接短信发送记录日志
+     * @return 结果
+     */
+    public int updateQwAcquisitionSendMsgLog(QwAcquisitionSendMsgLog qwAcquisitionSendMsgLog);
+
+    /**
+     * 删除企微-获客链接短信发送记录日志
+     *
+     * @param id 企微-获客链接短信发送记录日志主键
+     * @return 结果
+     */
+    public int deleteQwAcquisitionSendMsgLogById(Long id);
+
+    /**
+     * 批量删除企微-获客链接短信发送记录日志
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionSendMsgLogByIds(Long[] ids);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwCourseLinkSendMsgLogMapper.java

@@ -0,0 +1,62 @@
+package com.fs.qw.mapper;
+
+import com.fs.qw.domain.QwCourseLinkSendMsgLog;
+
+import java.util.List;
+
+/**
+ * 企微-发送看课链接短信记录日志Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+public interface QwCourseLinkSendMsgLogMapper
+{
+    /**
+     * 查询企微-发送看课链接短信记录日志
+     *
+     * @param id 企微-发送看课链接短信记录日志主键
+     * @return 企微-发送看课链接短信记录日志
+     */
+    public QwCourseLinkSendMsgLog selectQwCourseLinkSendMsgLogById(Long id);
+
+    /**
+     * 查询企微-发送看课链接短信记录日志列表
+     *
+     * @param qwCourseLinkSendMsgLog 企微-发送看课链接短信记录日志
+     * @return 企微-发送看课链接短信记录日志集合
+     */
+    public List<QwCourseLinkSendMsgLog> selectQwCourseLinkSendMsgLogList(QwCourseLinkSendMsgLog qwCourseLinkSendMsgLog);
+
+    /**
+     * 新增企微-发送看课链接短信记录日志
+     *
+     * @param qwCourseLinkSendMsgLog 企微-发送看课链接短信记录日志
+     * @return 结果
+     */
+    public int insertQwCourseLinkSendMsgLog(QwCourseLinkSendMsgLog qwCourseLinkSendMsgLog);
+
+    /**
+     * 修改企微-发送看课链接短信记录日志
+     *
+     * @param qwCourseLinkSendMsgLog 企微-发送看课链接短信记录日志
+     * @return 结果
+     */
+    public int updateQwCourseLinkSendMsgLog(QwCourseLinkSendMsgLog qwCourseLinkSendMsgLog);
+
+    /**
+     * 删除企微-发送看课链接短信记录日志
+     *
+     * @param id 企微-发送看课链接短信记录日志主键
+     * @return 结果
+     */
+    public int deleteQwCourseLinkSendMsgLogById(Long id);
+
+    /**
+     * 批量删除企微-发送看课链接短信记录日志
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteQwCourseLinkSendMsgLogByIds(Long[] ids);
+}

+ 1 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -247,6 +247,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
             "            <if test=\"wayId != null  and wayId != ''\"> and  SUBSTRING_INDEX(ec.state, ':', -1) = #{wayId} </if>\n" +
             "            <if test=\"name != null  and name != ''\"> and ec.name like concat( #{name}, '%')</if>\n" +
             "            <if test=\"type != null \"> and ec.type = #{type}</if>\n" +
+            "            <if test=\"isReply != null \"> and ec.is_reply = #{isReply}</if>\n" +
             "            <if test=\"gender != null \"> and ec.gender = #{gender}</if>\n" +
             "            <if test=\"description != null  and description != ''\"> and ec.description = #{description}</if>\n" +
 

+ 8 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -67,6 +67,14 @@ public interface QwUserMapper extends BaseMapper<QwUser>
      */
     public List<QwUser> selectQwUserList(QwUser qwUser);
 
+    /**
+     * 查询未删除的企微用户列表
+     *
+     * @param qwUser 企微用户
+     * @return 企微用户集合
+     */
+    public List<QwUser> selectNotDelQwUserList(QwUser qwUser);
+
     @Select("select qw_user_id,qw_user_name,login_code_url from qw_user" +
             " where company_id=#{companyId} " +
             "and company_user_id =#{userId} " +

+ 26 - 0
fs-service/src/main/java/com/fs/qw/service/ICorporateWeChatSpaceService.java

@@ -0,0 +1,26 @@
+package com.fs.qw.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.qw.vo.QwSessionConfigVo;
+
+public interface ICorporateWeChatSpaceService {
+    /**
+     * 通过专区中转获取会话记录
+     */
+    JSONObject fetchConversations(long seq, long limit, long proxy, long timeout, String customerId,String staffUserId);
+
+    /**
+     * 获取 agentConfig 签名(供前端 JS-SDK 使用)
+     */
+    JSONObject getAgentConfigSignature(String url);
+
+    /**
+     * Web 登录(用 code 换取 userId)
+     */
+    JSONObject login(String code);
+
+    /**
+     * 获取企业微信专区会话配置
+     * */
+    QwSessionConfigVo getQwSessionConfig();
+}

+ 106 - 0
fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionLinkInfoService.java

@@ -0,0 +1,106 @@
+package com.fs.qw.service;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
+import com.fs.qw.dto.IpadBlindAddDto;
+
+/**
+ * 获客链接-号码链接生成记录Service接口
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+public interface IQwAcquisitionLinkInfoService
+{
+    /**
+     * 查询获客链接-号码链接生成记录
+     *
+     * @param id 获客链接-号码链接生成记录主键
+     * @return 获客链接-号码链接生成记录
+     */
+    public QwAcquisitionLinkInfo selectQwAcquisitionLinkInfoById(Long id);
+
+    /**
+     * 查询获客链接-号码链接生成记录列表
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 获客链接-号码链接生成记录集合
+     */
+    public List<QwAcquisitionLinkInfo> selectQwAcquisitionLinkInfoList(QwAcquisitionLinkInfo qwAcquisitionLinkInfo);
+
+    /**
+     * 新增获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 结果
+     */
+    public int insertQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo);
+
+    /**
+     * 修改获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 结果
+     */
+    public int updateQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo);
+
+    /**
+     * 批量删除获客链接-号码链接生成记录
+     *
+     * @param ids 需要删除的获客链接-号码链接生成记录主键集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionLinkInfoByIds(Long[] ids);
+
+    /**
+     * 批量删除获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionAssistantIds 需要删除的获客链接管理主键ID集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(Long[] qwAcquisitionAssistantIds);
+
+    /**
+     * 删除获客链接-号码链接生成记录信息
+     *
+     * @param id 获客链接-号码链接生成记录主键
+     * @return 结果
+     */
+    public int deleteQwAcquisitionLinkInfoById(Long id);
+
+    /**
+     * 发送获客链接短信
+     *
+     * @param phone 待发送短信的手机号
+     * @param qwAcquisitionId 获客链接Id
+     * @return 结果
+     */
+    public SendResultDetailDTO sendMessageLink(String phone, Long qwAcquisitionId, SendMsgLogBo sendMsgLogBo);
+
+    /**
+     * 根据页面路径参数查询完整获客链接url
+     * */
+    public String selectQwAcquisitionUrlByRandomStr(String randomStr);
+
+
+    /**
+     * 批量生成获客链接短链
+     * */
+    public int batchCreateMessageLink(BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO);
+
+    /**
+     * 根据手机号生成单个获客链接
+     * */
+    String extractLink(Long qwAcquisitionAssistantId, String originalPhone, String originalLink,Long createBy);
+    R extractLinkNol(Long qwAcquisitionAssistantId, String originalLink, Long createBy);
+
+    /**
+     * iPad盲加好友
+     * */
+    void ipadBlindAdd(IpadBlindAddDto dto,SendMsgLogBo sendMsgLogBo);
+}

+ 8 - 0
fs-service/src/main/java/com/fs/qw/service/IQwUserService.java

@@ -55,6 +55,14 @@ public interface IQwUserService
      */
     public List<QwUser> selectQwUserList(QwUser qwUser);
 
+    /**
+     * 查询未删除企微用户列表
+     *
+     * @param qwUser 企微用户
+     * @return 企微用户集合
+     */
+    public List<QwUser> selectNotDelQwUserList(QwUser qwUser);
+
     /**
      * 新增企微用户
      *

+ 158 - 0
fs-service/src/main/java/com/fs/qw/service/impl/ICorporateWeChatSpaceServiceImpl.java

@@ -0,0 +1,158 @@
+package com.fs.qw.service.impl;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.exception.CustomException;
+import com.fs.qw.service.ICorporateWeChatSpaceService;
+import com.fs.qw.utils.WeChatTokenUtil;
+import com.fs.qw.utils.WeComSignatureUtil;
+import com.fs.qw.vo.QwSessionConfigVo;
+import com.fs.system.service.ISysConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ICorporateWeChatSpaceServiceImpl implements ICorporateWeChatSpaceService {
+
+    @Autowired
+    private ISysConfigService sysConfigService;
+
+    private final static String CONFIG_KEY = "qw.sessionConfig";
+
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final java.util.concurrent.ConcurrentHashMap<String, String> consumedCodes = new java.util.concurrent.ConcurrentHashMap<>();
+
+    // =============== 核心:通过专区中转拉取会话 ===============
+    @Override
+    public JSONObject fetchConversations(long seq, long limit, long proxy, long timeout,
+                                         String customerId, String staffUserId) {
+        JSONObject result = new JSONObject();
+        try {
+            // 1. 获取企业微信配置(避免重复调用 getQwSessionConfig())
+            QwSessionConfigVo qwConfig = getQwSessionConfig();
+            String corpid = qwConfig.getCorpid();
+            String agentSecret = qwConfig.getAgentSecret();
+            // 2. 获取 access_token
+            String accessToken = WeChatTokenUtil.getAccessToken(corpid, agentSecret);
+
+            // 3. 构建 request_data(与专区程序约定一致)
+            JSONObject requestData = new JSONObject();
+            requestData.put("action", qwConfig.getAbilityAction());  // 对应能力 action
+            requestData.put("seq", seq);
+            requestData.put("limit", limit);
+            requestData.put("proxy", proxy);
+            requestData.put("timeout", timeout);
+            requestData.put("customerId", customerId);
+            requestData.put("staffUserId", staffUserId);
+
+            // 4. 能力ID
+            String abilityId = qwConfig.getFetchConversationAbilityId();
+            if (abilityId==null){
+                throw new CustomException("专区能力ID未配置");
+            }
+            // 5. 调用 sync_call_program 接口
+            String url = "https://qyapi.weixin.qq.com/cgi-bin/chatdata/sync_call_program?access_token=" + accessToken;
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("ability_id", abilityId);
+            requestBody.put("request_data", JSON.toJSONString(requestData));
+            requestBody.put("program_id", qwConfig.getProgramId());
+            log.info("调用专区接口: ability_id={}, request_data={}", abilityId, requestData);
+            JSONObject response = restTemplate.postForObject(url, requestBody, JSONObject.class);
+            log.info("专区响应: {}", response);
+
+            // 6. 处理返回结果
+            if (response != null && response.getInteger("errcode") == 0) {
+                String responseDataStr = response.getString("response_data");
+                if (responseDataStr != null) {
+                    JSONObject responseData = JSON.parseObject(responseDataStr);
+                    if (responseData.getInteger("errcode") == 0) {
+                        result.put("errcode", 0);
+                        result.put("errmsg", "ok");
+                        result.put("msgList", responseData.get("data"));
+                    } else {
+                        result.put("errcode", responseData.getInteger("errcode"));
+                        result.put("errmsg", responseData.getString("errmsg"));
+                    }
+                } else {
+                    result.put("errcode", -1);
+                    result.put("errmsg", "专区返回数据格式错误");
+                }
+
+            } else {
+                result.put("errcode", response != null ? response.getInteger("errcode") : -1);
+                result.put("errmsg", response != null ? response.getString("errmsg") : "专区调用失败");
+            }
+        } catch (Exception e) {
+            log.error("专区中转调用异常", e);
+            result.put("errcode", -1);
+            result.put("errmsg", "内部错误:" + e.getMessage());
+        }
+        return result;
+    }
+
+
+    @Override
+    public JSONObject getAgentConfigSignature(String url) {
+        QwSessionConfigVo qwSessionConfig = getQwSessionConfig();
+        return WeComSignatureUtil.generateAgentConfigSignature(qwSessionConfig.getCorpid(), qwSessionConfig.getAgentSecret(), qwSessionConfig.getAgentid(), url);
+    }
+
+    @Override
+    public JSONObject login(String code) {
+        JSONObject result = new JSONObject();
+        if (code == null || code.isEmpty()) {
+            result.put("errcode", -1);
+            result.put("errmsg", "missing code");
+            return result;
+        }
+        if (consumedCodes.containsKey(code)) {
+            result.put("errcode", 40029);
+            result.put("errmsg", "code already used");
+            return result;
+        }
+        QwSessionConfigVo qwSessionConfig = getQwSessionConfig();
+        try {
+            String accessToken = WeChatTokenUtil.getAccessToken(qwSessionConfig.getCorpid(), qwSessionConfig.getAgentSecret());
+            String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token="
+                    + accessToken + "&code=" + code;
+            JSONObject resp = restTemplate.getForObject(url, JSONObject.class);
+            if (resp != null && resp.getIntValue("errcode") == 0) {
+                consumedCodes.put(code, "used");
+                if (consumedCodes.size() > 1000) consumedCodes.clear();
+
+                JSONObject userInfo = new JSONObject();
+                userInfo.put("userid", resp.getString("UserId"));
+                result.put("errcode", 0);
+                result.put("errmsg", "ok");
+                result.put("data", userInfo);
+            } else {
+                result.put("errcode", resp != null ? resp.getIntValue("errcode") : -1);
+                result.put("errmsg", resp != null ? resp.getString("errmsg") : "fail");
+            }
+        } catch (Exception e) {
+            log.error("登录异常", e);
+            result.put("errcode", -1);
+            result.put("errmsg", e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取企业微信专区会话配置
+     */
+    @Override
+    public QwSessionConfigVo getQwSessionConfig() {
+        QwSessionConfigVo qwSessionConfig = sysConfigService.getConfig(CONFIG_KEY, QwSessionConfigVo.class);
+        if (qwSessionConfig == null){
+            log.error("未找到企微专区配置,key:{}",CONFIG_KEY);
+            throw new CustomException("未找到企微专区配置");
+        }
+        return qwSessionConfig;
+    }
+}

+ 823 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionLinkInfoServiceImpl.java

@@ -0,0 +1,823 @@
+package com.fs.qw.service.impl;
+
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanySms;
+import com.fs.company.domain.CompanySmsTemp;
+import com.fs.company.service.ICompanySmsService;
+import com.fs.company.service.ICompanySmsTempService;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
+import com.fs.qw.dto.IpadBlindAddDto;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.mapper.QwAcquisitionAssistantMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.QwUserListParam;
+import com.fs.qw.utils.UniqueStringUtil;
+import com.fs.qw.vo.QwUserVO;
+import com.fs.wxwork.dto.WxAddSearchDTO;
+import com.fs.wxwork.dto.WxSearchContactDTO;
+import com.fs.wxwork.dto.WxSearchContactResp;
+import com.fs.wxwork.dto.WxWorkResponseDTO;
+import com.fs.wxwork.service.WxWorkService;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+import com.fs.qw.mapper.QwAcquisitionLinkInfoMapper;
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
+import com.fs.qw.service.IQwAcquisitionLinkInfoService;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.PostConstruct;
+
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
+/**
+ * 获客链接-号码链接生成记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+@Slf4j
+@Service
+public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoService
+{
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @Autowired
+    private ISmsService smsService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ICompanySmsTempService smsTempService;
+
+    @Autowired
+    private ICompanySmsService companySmsService;
+
+    @Autowired
+    private QwAcquisitionAssistantMapper acquisitionAssistantMapper;
+
+    @Autowired
+    private QwAcquisitionLinkInfoMapper qwAcquisitionLinkInfoMapper;
+
+    @Autowired
+    private QwUserMapper qwUserMapper;
+
+    @Autowired
+    private WxWorkService wxWorkService;
+
+    @PostConstruct
+    public void init() {
+        // 如果自动注入失败,手动获取
+        if (wxWorkService == null) {
+            wxWorkService = applicationContext.getBean(WxWorkService.class);
+        }
+    }
+    //拼接电话号码的链接后缀(这个后面拼接加密后的手机字符串)
+    private static final String  LINK_SUFFIX = "?customer_channel=up:";
+    private static final String  LINK_SUFFIX_NOL = "?customer_channel=link:";
+
+    // 企微加好友链接-url的key
+    private static final String QW_FRIEND_LINK_URL_KEY = "qw_friend_link_url:";
+
+    //获客链接短信模板code
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
+
+    //访问链接域名
+    private static final String  LINK_DOMAIN = "https://c.ysyd.top/";
+    /**
+     * 查询获客链接-号码链接生成记录
+     *
+     * @param id 获客链接-号码链接生成记录主键
+     * @return 获客链接-号码链接生成记录
+     */
+    @Override
+    public QwAcquisitionLinkInfo selectQwAcquisitionLinkInfoById(Long id)
+    {
+        return qwAcquisitionLinkInfoMapper.selectQwAcquisitionLinkInfoById(id);
+    }
+
+    /**
+     * 查询获客链接-号码链接生成记录列表
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 获客链接-号码链接生成记录
+     */
+    @Override
+    public List<QwAcquisitionLinkInfo> selectQwAcquisitionLinkInfoList(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
+    {
+        List<QwAcquisitionLinkInfo> resultList = qwAcquisitionLinkInfoMapper.selectQwAcquisitionLinkInfoList(qwAcquisitionLinkInfo);
+        if (CollectionUtils.isEmpty(resultList)){
+            return Collections.emptyList();
+        }
+        for (QwAcquisitionLinkInfo item : resultList) {
+            if (item.getPhone() != null) {
+                item.setPhone(item.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+            }
+        }
+        return resultList;
+    }
+
+    /**
+     * 新增获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 结果
+     */
+    @Override
+    public int insertQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
+    {
+        return qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+    }
+
+    /**
+     * 修改获客链接-号码链接生成记录
+     *
+     * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
+     * @return 结果
+     */
+    @Override
+    public int updateQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
+    {
+        //TODO 如果放开编辑就需要更新缓存
+        return qwAcquisitionLinkInfoMapper.updateQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+    }
+
+    /**
+     * 批量删除获客链接-号码链接生成记录
+     *
+     * @param ids 需要删除的获客链接-号码链接生成记录主键集合
+     * @return 结果
+     */
+    @Override
+    public int deleteQwAcquisitionLinkInfoByIds(Long[] ids)
+    {
+        if (ids.length == 0){
+            return 0;
+        }
+        List<QwAcquisitionLinkInfo> toBeDeletedList = qwAcquisitionLinkInfoMapper.selectAcquisitionLinkInfoListByIds(ids);
+        //循环删除 Redis 缓存
+        batchDeleteLinkCatch(toBeDeletedList);
+        return qwAcquisitionLinkInfoMapper.deleteQwAcquisitionLinkInfoByIds(ids);
+    }
+
+    /**
+     * 删除获客链接-号码链接生成记录信息
+     *
+     * @param qwAcquisitionAssistantIds 需要删除的获客链接管理主键ID集合
+     * @return 结果
+     */
+    @Override
+    public int deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(Long[] qwAcquisitionAssistantIds){
+        if (qwAcquisitionAssistantIds.length == 0){
+            return 0;
+        }
+        List<QwAcquisitionLinkInfo> toBeDeletedList = qwAcquisitionLinkInfoMapper.selectAcquisitionLinkInfoListByAcquisitionAssistantIds(qwAcquisitionAssistantIds);
+        //循环删除 Redis 缓存
+        batchDeleteLinkCatch(toBeDeletedList);
+        return qwAcquisitionLinkInfoMapper.deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(qwAcquisitionAssistantIds);
+    }
+
+    /**
+     * 删除获客链接-号码链接生成记录信息
+     *
+     * @param id 获客链接-号码链接生成记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwAcquisitionLinkInfoById(Long id)
+    {
+        QwAcquisitionLinkInfo existAcquisitionLinkInfo = qwAcquisitionLinkInfoMapper.selectQwAcquisitionLinkInfoById(id);
+        if (existAcquisitionLinkInfo==null){
+            throw new CustomException("数据不存在");
+        }
+        // ========== 删除Redis缓存 ==========
+        try {
+            // 1. 删除pageParam对应的URL缓存
+            if (StringUtils.isNotEmpty(existAcquisitionLinkInfo.getRandomStr())) {
+                String urlCacheKey = QW_FRIEND_LINK_URL_KEY + existAcquisitionLinkInfo.getRandomStr();
+                redisCache.deleteObject(urlCacheKey);
+                log.info("删除获客链接URL缓存成功, pageParam: {}, key: {}",
+                        existAcquisitionLinkInfo.getRandomStr(), urlCacheKey);
+            }
+        } catch (Exception e) {
+            // 缓存删除失败不应该影响主流程,但需要记录日志
+            log.error("删除获客链接缓存失败, id: {}, qwAcquisitionAssistantId: {}, pageParam: {}",
+                    existAcquisitionLinkInfo.getId(), existAcquisitionLinkInfo.getQwAcquisitionAssistantId(),
+                    existAcquisitionLinkInfo.getRandomStr(), e);
+        }
+        return qwAcquisitionLinkInfoMapper.deleteQwAcquisitionLinkInfoById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public SendResultDetailDTO sendMessageLink(String phone, Long qwAcquisitionId, SendMsgLogBo sendMsgLogBo) {
+        log.info("发送获客链接短信(带随机参数),号码:{}", phone);
+
+        // 1. 获取短信模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_LINK_TEMPLATE_CODE);
+        if (temp == null) {
+            log.info("获客链接-未找到短信模板:{}", SMS_LINK_TEMPLATE_CODE);
+            throw new CustomException("获客链接-未找到短信模板");
+        }
+
+        // 2. 获取获客链接信息
+        String originalContent = temp.getContent();
+        QwAcquisitionAssistant acquisitionAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
+        if (acquisitionAssistant == null) {
+            log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
+            throw new CustomException("获客链接-未找到获客链接信息");
+        }
+
+        // 3. 生成随机参数并构建短信内容
+        String randomStr = generateUniqueRandomStr();
+        String replaceText = LINK_DOMAIN + randomStr;
+        String content = originalContent.replace("${sms.friendLink}", replaceText);
+
+        // 4. 计算需要的短信条数
+        Integer needCount = calculateSmsCount(content);
+
+        // 5. 【乐观锁扣减,支持重试】
+        int maxRetries = 3;
+
+        for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
+            Long balance = companySmsService.getBalance(2L);
+            if (balance == null || balance < needCount) {
+                log.warn("短信余额不足, companyId=2, balance={}, needCount={}", balance, needCount);
+                return new SendResultDetailDTO(false, "短信余额不足", null);
+            }
+
+            CompanySms latestSms = companySmsService.selectCompanySms(2L);
+            if (latestSms == null) {
+                throw new CustomException("公司短信配置不存在");
+            }
+
+            if (latestSms.getRemainSmsCount() < needCount) {
+                log.warn("短信余额不足, companyId=2, remainCount={}, needCount={}",
+                        latestSms.getRemainSmsCount(), needCount);
+                return new SendResultDetailDTO(false, "短信余额不足", null);
+            }
+
+            Long version = latestSms.getVersion() != null ? latestSms.getVersion() : 0L;
+            int updateCount = companySmsService.decrementRemainSmsCountWithVersion(2L, needCount, version);
+
+            if (updateCount > 0) {
+                log.info("乐观锁扣减成功, needCount={}, version={}", needCount, version);
+                break;
+            }
+
+            log.warn("乐观锁扣减失败,第{}次重试", retryCount + 1);
+
+            if (retryCount == maxRetries - 1) {
+                return new SendResultDetailDTO(false, "系统繁忙,请稍后重试", null);
+            }
+
+            try {
+                Thread.sleep(50L * (retryCount + 1));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return new SendResultDetailDTO(false, "操作被中断", null);
+            }
+        }
+
+        // 6. 更新缓存
+        companySmsService.updateCacheBalance(2L, -needCount);
+
+        try {
+            sendMsgLogBo.setQwAcquisitionId(acquisitionAssistant.getId());
+
+            R r = smsService.simpleSmsSend(phone, content, temp, SmsLogType.ACQUISITION_LINK, sendMsgLogBo);
+
+            if (r != null && "200".equals(String.valueOf(r.get("code")))) {
+                addAcquisitionLinkInfo(acquisitionAssistant.getId(), phone, acquisitionAssistant.getUrl(),
+                        randomStr, sendMsgLogBo.getCompanyUserId());
+                log.info("短信发送成功, phone={}, needCount={}", phone, needCount);
+                return new SendResultDetailDTO(true, null, null);
+            } else {
+                String msg = r != null && r.get("msg") != null ? r.get("msg").toString() : "未知错误";
+                log.warn("短信发送失败,退还余额, phone={}, needCount={}", phone, needCount);
+                rollbackBalance(needCount, maxRetries);
+                return new SendResultDetailDTO(false, msg, null);
+            }
+        } catch (Exception e) {
+            log.error("发送异常,退还余额, phone=" + phone, e);
+            rollbackBalance(needCount, maxRetries);
+            return new SendResultDetailDTO(false, e.getMessage(), null);
+        }
+    }
+
+    @Override
+    public String selectQwAcquisitionUrlByRandomStr(String randomStr) {
+        String key = QW_FRIEND_LINK_URL_KEY + randomStr;
+        String fullLink = null;
+
+        try {
+            Object cacheObj = redisCache.getCacheObject(key);
+            if (cacheObj instanceof String) {
+                fullLink = (String) cacheObj;
+                // 处理缓存空值的情况
+                if ("NULL".equals(fullLink)) {
+                    return null;
+                }
+                log.debug("从缓存获取完整获客链接url成功,randomStr:{}", randomStr);
+                return fullLink;
+            }
+        } catch (Exception e) {
+            log.warn("从缓存获取完整获客链接url异常, 将重新获取, randomStr:{}", randomStr);
+        }
+
+        // 缓存中没有,查询数据库
+        fullLink = qwAcquisitionLinkInfoMapper.selectQwAcquisitionUrlByRandomStr(randomStr);
+
+        // 缓存处理(包括空值缓存)
+        if (fullLink == null) {
+            int nullCacheExpire = 10; // 10秒
+            redisCache.setCacheObject(key, "NULL", nullCacheExpire, TimeUnit.SECONDS);
+            log.info("完整获客链接URL不存在,缓存空值10秒, randomStr:{}", randomStr);
+            return null;
+        } else {
+            // 正常值仍缓存2天
+            Integer cacheExpire = 2;
+            redisCache.setCacheObject(key, fullLink, cacheExpire, TimeUnit.DAYS);
+            log.info("完整获客链接URL缓存成功, randomStr:{}", randomStr);
+        }
+
+        return fullLink;
+    }
+
+    @Override
+    //TODO 多数据批量操作时,当前事务还未提交,在短时间内可能会导致第三方回调的时候查不到需要更新的数据,后续优化
+    //@Transactional(rollbackFor = Exception.class)
+    public int batchCreateMessageLink(BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO) {
+        Long qwAcquisitionId = batchAddAcquisitionLinkDTO.getQwAcquisitionAssistantId();
+        List<String> phoneList = batchAddAcquisitionLinkDTO.getPhoneList();
+
+        if (CollectionUtils.isEmpty(phoneList)) {
+            log.warn("批量发送短信,手机号列表为空");
+            return 0;
+        }
+
+        // 1. 获取短信模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_LINK_TEMPLATE_CODE);
+        if (temp == null) {
+            log.info("获客链接-未找到短信模板:{}", SMS_LINK_TEMPLATE_CODE);
+            throw new CustomException("获客链接-未找到短信模板");
+        }
+
+        // 2. 获取获客链接信息
+        QwAcquisitionAssistant acquisitionAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
+        if (acquisitionAssistant == null) {
+            log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
+            throw new CustomException("获客链接-未找到获客链接信息");
+        }
+
+        String originalContent = temp.getContent();
+        SendMsgLogBo baseSendMsgLogBo = new SendMsgLogBo();
+        baseSendMsgLogBo.setCompanyUserId(batchAddAcquisitionLinkDTO.getCreateBy());
+        baseSendMsgLogBo.setQwAcquisitionId(acquisitionAssistant.getId());
+
+        // 3. 为每个手机号预生成链接和短信内容
+        List<PhoneMessageInfo> phoneMessageList = new ArrayList<>();
+        for (String phone : phoneList) {
+            String randomStr = generateUniqueRandomStr();
+            String replaceText = LINK_DOMAIN + randomStr;
+            String content = originalContent.replace("${sms.friendLink}", replaceText);
+            Integer needCount = calculateSmsCount(content);
+
+            PhoneMessageInfo info = new PhoneMessageInfo();
+            info.setPhone(phone);
+            info.setContent(content);
+            info.setRandomStr(randomStr);
+            info.setNeedCount(needCount);
+            phoneMessageList.add(info);
+        }
+
+        // 4. 计算总需要条数
+        int totalNeedCount = phoneMessageList.stream().mapToInt(PhoneMessageInfo::getNeedCount).sum();
+
+        // 5. 快速校验总余额是否充足(使用缓存)
+        Long balance = companySmsService.getBalance(2L);
+        if (balance == null || balance < totalNeedCount) {
+            // 缓存可能不准,再查一次DB确认
+            CompanySms companySms = companySmsService.selectCompanySms(2L);
+            if (companySms == null) {
+                throw new CustomException("公司短信配置不存在");
+            }
+            if (companySms.getRemainSmsCount() < totalNeedCount) {
+                throw new CustomException("短信余额不足,总需要:" + totalNeedCount +
+                        ",当前余额:" + companySms.getRemainSmsCount());
+            }
+        }
+
+        // 6. 批量发送短信
+        List<SendResultInfo> sendResults = new ArrayList<>();
+        int successCount = 0;
+
+        for (PhoneMessageInfo info : phoneMessageList) {
+            try {
+                SendMsgLogBo sendMsgLogBo = new SendMsgLogBo();
+                BeanUtils.copyProperties(baseSendMsgLogBo, sendMsgLogBo);
+
+                R r = smsService.simpleSmsSend(info.getPhone(), info.getContent(), temp,
+                        SmsLogType.ACQUISITION_LINK, sendMsgLogBo);
+
+                if (r != null && "200".equals(String.valueOf(r.get("code")))) {
+                    addAcquisitionLinkInfo(acquisitionAssistant.getId(), info.getPhone(),
+                            acquisitionAssistant.getUrl(), info.getRandomStr(),
+                            baseSendMsgLogBo.getCompanyUserId());
+
+                    SendResultInfo successResult = new SendResultInfo();
+                    successResult.setSuccess(true);
+                    successResult.setNeedCount(info.getNeedCount());
+                    successResult.setPhone(info.getPhone());
+                    sendResults.add(successResult);
+                    successCount++;
+
+                    log.info("短信发送成功 phone={}, needCount={}", info.getPhone(), info.getNeedCount());
+                } else {
+                    String msg = r != null && r.get("msg") != null ? r.get("msg").toString() : "未知错误";
+                    log.warn("短信发送失败 phone={}, msg={}", info.getPhone(), msg);
+
+                    SendResultInfo failResult = new SendResultInfo();
+                    failResult.setSuccess(false);
+                    failResult.setNeedCount(0);
+                    failResult.setPhone(info.getPhone());
+                    failResult.setFailReason(msg);
+                    sendResults.add(failResult);
+                }
+            } catch (Exception e) {
+                log.error("短信发送异常 phone=" + info.getPhone(), e);
+
+                SendResultInfo failResult = new SendResultInfo();
+                failResult.setSuccess(false);
+                failResult.setNeedCount(0);
+                failResult.setPhone(info.getPhone());
+                failResult.setFailReason(e.getMessage());
+                sendResults.add(failResult);
+            }
+        }
+
+        // 7. 计算实际需要扣减的总条数
+        int actualNeedCount = sendResults.stream()
+                .filter(SendResultInfo::isSuccess)
+                .mapToInt(SendResultInfo::getNeedCount)
+                .sum();
+
+        if (actualNeedCount == 0) {
+            log.info("批量发送全部失败,不扣减余额");
+            return 0;
+        }
+
+        // 8. 乐观锁扣减,支持重试
+        int maxRetries = 3;
+
+        for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
+            // 快速检查(用缓存)
+            Long reBalance = companySmsService.getBalance(2L);
+            if (reBalance == null || reBalance < actualNeedCount) {
+                // 缓存可能不准,再查一次DB确认
+                CompanySms latestSms = companySmsService.selectCompanySms(2L);
+                if (latestSms == null) {
+                    throw new CustomException("公司短信配置不存在");
+                }
+                if (latestSms.getRemainSmsCount() < actualNeedCount) {
+                    throw new CustomException("短信余额不足");
+                }
+            }
+
+            // 获取最新数据(含version)
+            CompanySms latestSms = companySmsService.selectCompanySms(2L);
+            if (latestSms == null) {
+                throw new CustomException("公司短信配置不存在");
+            }
+
+            Long version = latestSms.getVersion() != null ? latestSms.getVersion() : 0L;
+            int updateCount = companySmsService.decrementRemainSmsCountWithVersion(2L, actualNeedCount, version);
+
+            if (updateCount > 0) {
+                log.info("乐观锁扣减余额成功, companyId=2, needCount={}, version={}", actualNeedCount, version);
+                // 扣减成功,更新缓存
+                companySmsService.updateCacheBalance(2L, -actualNeedCount);
+                log.info("批量创建获客链接完成,总计尝试 {}, 成功 {}, 实际扣减条数 {}",
+                        phoneList.size(), successCount, actualNeedCount);
+                return successCount;
+            }
+
+            log.warn("乐观锁扣减余额失败,第{}次重试, version={}", retryCount + 1, version);
+
+            if (retryCount == maxRetries - 1) {
+                throw new CustomException("扣减余额失败,系统繁忙,请稍后重试");
+            }
+
+            try {
+                Thread.sleep(100L * (retryCount + 1));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new CustomException("扣减余额被中断");
+            }
+        }
+
+        return successCount;
+    }
+
+    /**
+     * 退还余额(带重试)
+     */
+    private void rollbackBalance(int needCount, int maxRetries) {
+        for (int i = 0; i < maxRetries; i++) {
+            try {
+                CompanySms current = companySmsService.selectCompanySms(2L);
+                if (current == null) {
+                    log.error("退还短信余额失败,公司短信配置不存在");
+                    return;
+                }
+                Long currentVersion = current.getVersion() != null ? current.getVersion() : 0L;
+                int rollbackCount = companySmsService.incrementRemainSmsCountWithVersion(2L, needCount, currentVersion);
+                if (rollbackCount > 0) {
+                    companySmsService.updateCacheBalance(2L, needCount);
+                    log.info("退还余额成功, needCount={}", needCount);
+                    return;
+                }
+                Thread.sleep(50L);
+            } catch (Exception e) {
+                log.error("退还短信余额异常,第{}次重试", i + 1, e);
+            }
+        }
+        log.error("退还短信余额失败,需要人工处理, companyId=2, needCount={}", needCount);
+    }
+
+    @Override
+    public String extractLink(Long qwAcquisitionAssistantId, String originalPhone, String originalLink,Long createBy) {
+        String randomStr = generateUniqueRandomStr();
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setCreateBy(createBy);
+        qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
+        qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
+        //加密手机号
+        String phonePlus = encryptPhone(originalPhone);
+        String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        // ========== 缓存URL,便于后续通过randomStr访问 ==========
+        try {
+            String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
+            Integer cacheExpire = 2; // 默认缓存2天
+            redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
+        } catch (Exception e) {
+            // 缓存失败不影响主流程,但需要记录日志
+            log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
+        }
+        // 返回域名+随机字符串
+        return LINK_DOMAIN+randomStr;
+    }
+
+    @Override
+    public R extractLinkNol(Long qwAcquisitionAssistantId, String originalLink, Long createBy) {
+        String randomStr = generateUniqueRandomStr();
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setCreateBy(createBy);
+        qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
+        String linkPlus=originalLink+LINK_SUFFIX_NOL+ qwAcquisitionAssistantId;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        // ========== 缓存URL,便于后续通过randomStr访问 ==========
+        try {
+            String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
+            Integer cacheExpire = 2; // 默认缓存2天
+            redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
+        } catch (Exception e) {
+            // 缓存失败不影响主流程,但需要记录日志
+            log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
+        }
+
+        String shortLink = LINK_DOMAIN+randomStr;
+        return R.ok().put("shortLink",shortLink).put("linkPlus",linkPlus);
+        // 返回域名+随机字符串
+
+    }
+
+    @Override
+    public void ipadBlindAdd(IpadBlindAddDto dto,SendMsgLogBo sendMsgLogBo) {
+        //获取QwUser信息
+        QwUser qwUser=qwUserMapper.selectQwUserById(dto.getUserId());
+        if (qwUser==null){
+            log.error( "销售用户不存在,qwUserId:{}",dto.getUserId());
+            throw new CustomException("销售用户不存在");
+        }
+        //判断这个销售的ipad是否在线
+        QwUserListParam queryCondition=new QwUserListParam();
+        queryCondition.setCorpId(dto.getCorpId());
+        queryCondition.setQwUserId(qwUser.getQwUserId());
+        List<QwUserVO> qwUserVOS = qwUserMapper.selectQwUserListStaffVO(queryCondition);
+        if (qwUserVOS.isEmpty()){
+            throw new CustomException("此主体不存在该销售用户");
+        }
+        if (qwUserVOS.get(0).getIpadStatus()==0){
+            throw new CustomException("此销售用户ipad已离线,请登录");
+        }
+        // 每次使用时获取代理对象
+        QwAcquisitionLinkInfoServiceImpl self = applicationContext.getBean(QwAcquisitionLinkInfoServiceImpl.class);
+        try {
+            //调用ipad加好友接口
+            WxWorkResponseDTO<String> response = self.qwAddWxInvokeIpad(dto.getPhone(), qwUser.getUid(),
+                    qwUser.getServerId(),
+                    qwUser.getVid(),
+                    qwUser.getQwUserName());
+            if (response != null && response.getErrcode() == 0) {
+                log.info("ipad获客链接加微成功");
+            }else {
+                log.error("ipad获客链接加微失败,错误码:{},错误信息:{}",response.getErrcode(),response.getErrmsg());
+            }
+        }catch (Exception e){
+            //TODO 如果有异常,暂时只做记录
+            log.error("ipad加微异常",e);
+        }
+
+        //调用发送短信接口
+        self.sendMessageLink(dto.getPhone(),dto.getQwAcquisitionAssistantId(),sendMsgLogBo);
+    }
+
+    /**
+     * 企微加个微调用ipad端
+     * @param mobile  手机号
+     * @param qwUid   企微uid
+     * @param serverId   服务器id
+     * @return String 结果
+     */
+    public WxWorkResponseDTO<String> qwAddWxInvokeIpad(String mobile, String qwUid, Long serverId, String vid, String qwUserName) {
+        if (StringUtils.isBlank(mobile) || StringUtils.isBlank(qwUid) || serverId == null) {
+            log.warn("企微申请加好友任务参数校验失败: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId);
+            return null;
+        }
+
+        try {
+            WxAddSearchDTO wxAddSearchDTO = new WxAddSearchDTO();
+            wxAddSearchDTO.setUuid(qwUid);
+            wxAddSearchDTO.setVid(Long.valueOf(vid));
+            wxAddSearchDTO.setPhone(mobile);
+
+            WxSearchContactDTO contactDTO=new WxSearchContactDTO();
+            contactDTO.setUuid(qwUid);
+            contactDTO.setPhoneNumber(mobile);
+
+
+            WxWorkResponseDTO<WxSearchContactResp> respWxWorkResponseDTO = wxWorkService.searchContact(contactDTO, serverId);
+            WxSearchContactResp.UserList user = respWxWorkResponseDTO.getData().getUserList().stream()
+                    .filter(u -> u.getState().equals("2"))
+                    .findFirst()
+                    .orElse(null); // 或者 .orElseThrow(() -> new RuntimeException("未找到指定用户"))
+
+            wxAddSearchDTO.setOptionid(user.getOpenid());
+            wxAddSearchDTO.setTicket(user.getTicket());
+            wxAddSearchDTO.setContent("你好,我是你的专属助手:"+qwUserName+",有什么问题都可以问我哦~");
+
+            WxWorkResponseDTO<String> response = wxWorkService.addSearch(wxAddSearchDTO, serverId);
+            log.debug("企微加微接口调用结果: errcode={}, errmsg={}",
+                    response != null ? response.getErrcode() : "null",
+                    response != null ? response.getErrmsg() : "null");
+
+            return response;
+        } catch (Exception e) {
+            log.error("企微申请加好友任务请求接口异常: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId, e);
+            return null;
+        }
+    }
+
+    /**
+     * 添加链接生成记录
+     * */
+    public int addAcquisitionLinkInfo(Long qwAcquisitionAssistantId,String originalPhone,String originalLink,String randomStr,Long createBy){
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setCreateBy(createBy);
+        qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
+        qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
+        //加密手机号
+        String phonePlus = encryptPhone(originalPhone);
+        String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        // ========== 缓存URL,便于后续通过randomStr访问 ==========
+        try {
+            String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
+            Integer cacheExpire = 2; // 默认缓存2天
+            redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
+        } catch (Exception e) {
+            // 缓存失败不影响主流程,但需要记录日志
+            log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
+        }
+        return addResult;
+    }
+
+
+    /**
+     * 生成唯一的页面参数
+     */
+    private String generateUniqueRandomStr() {
+        // 获取所有已存在的pageParam(只取需要的字段)
+        List<String> existingParams = qwAcquisitionLinkInfoMapper.selectAllRandomStr();
+        //使用Set,提高查找效率 O(1)
+        Set<String> paramSet = new HashSet<>(existingParams);
+
+        int maxAttempts = 10; // 设置最大尝试次数
+        int attempt = 0;
+
+        while (attempt < maxAttempts) {
+            // 生成7位随机码
+            String candidate = UniqueStringUtil.generateTimeBasedUnique(7);
+
+            // 使用Set的contains方法,O(1)复杂度
+            if (!paramSet.contains(candidate)) {
+                log.debug("生成页面参数成功: {}, 尝试次数: {}", candidate, attempt + 1);
+                return candidate;
+            }
+
+            attempt++;
+            log.debug("页面参数 {} 已存在,重新生成,第{}次尝试", candidate, attempt);
+        }
+
+        // 如果多次尝试都失败,使用+1随机数方案
+        String finalParam = UniqueStringUtil.generateTimeBasedUnique(8);
+        log.warn("多次尝试后使用7位参数: {}", finalParam);
+        return finalParam;
+    }
+
+    /**
+     *  批量删除缓存
+     * */
+    private void batchDeleteLinkCatch(List<QwAcquisitionLinkInfo> toBeDeletedList) {
+        if (!toBeDeletedList.isEmpty()) {
+            for (QwAcquisitionLinkInfo record : toBeDeletedList) {
+                try {
+                    // 检查 randomStr 是否为空,避免处理无效的缓存 key
+                    if (StringUtils.isNotEmpty(record.getRandomStr())) {
+                        String urlCacheKey = QW_FRIEND_LINK_URL_KEY + record.getRandomStr();
+                        redisCache.deleteObject(urlCacheKey);
+                        log.info("批量删除获客链接URL缓存成功, randomStr: {}, key: {}",
+                                record.getRandomStr(), urlCacheKey);
+                    }
+                } catch (Exception e) {
+                    // 缓存删除失败不应影响主数据库删除流程,但需要记录日志
+                    log.error("批量删除获客链接缓存失败, id: {}, qwAcquisitionAssistantId: {}, randomStr: {}",
+                            record.getId(), record.getQwAcquisitionAssistantId(), record.getRandomStr(), e);
+                }
+            }
+        }
+    }
+
+    //根据短信文字内容计算短信条数
+    private int calculateSmsCount(String content) {
+        if (content == null) return 1;
+        int counts = content.length() / 67;
+        if (content.length() % 67 > 0) {
+            counts = counts + 1;
+        }
+        if (counts == 0) {
+            counts = 1;
+        }
+        return counts;
+    }
+
+    // 辅助类
+    @Data
+    private static class PhoneMessageInfo {
+        private String phone;
+        private String content;
+        private String randomStr;
+        private Integer needCount;
+    }
+
+    @Data
+    private static class SendResultInfo {
+        private boolean success;
+        private int needCount;
+        private String phone;
+        private String failReason;
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -848,6 +848,17 @@ public class QwUserServiceImpl implements IQwUserService
     {
         return qwUserMapper.selectQwUserList(qwUser);
     }
+    /**
+     * 查询未删除企微用户列表
+     *
+     * @param qwUser 企微用户
+     * @return 企微用户
+     */
+    @Override
+    public List<QwUser> selectNotDelQwUserList(QwUser qwUser)
+    {
+        return qwUserMapper.selectNotDelQwUserList(qwUser);
+    }
 
     /**
      * 新增企微用户

+ 19 - 0
fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategy.java

@@ -0,0 +1,19 @@
+package com.fs.qw.strategy;
+
+import com.fs.common.core.domain.R;
+/**
+ * 短信发送后记录特定业务日志的策略接口
+ */
+public interface SmsLogStrategy {
+    /**
+     * 执行日志记录操作
+     * @param result 发送结果
+     * @param content 短信内容
+     * @param phone 手机号
+     * @param tempId 模板ID
+     * @param type 服务商类型
+     * @param number 短信条数
+     * @param contextObject 特定业务的上下文对象(如qwAcquisitionId, externalContactId等)
+     */
+    void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject);
+}

+ 54 - 0
fs-service/src/main/java/com/fs/qw/strategy/SmsLogStrategyManager.java

@@ -0,0 +1,54 @@
+package com.fs.qw.strategy;
+
+import com.fs.common.core.domain.R;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.strategy.impl.AcquisitionLinkLogStrategyImpl;
+import com.fs.qw.strategy.impl.CourseLinkLogStrategyImpl;
+import com.fs.qw.strategy.impl.NoOpSmsLogStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.EnumMap;
+import java.util.Map;
+@Slf4j
+@Component
+public class SmsLogStrategyManager {
+
+    private final Map<SmsLogType, SmsLogStrategy> strategies = new EnumMap<>(SmsLogType.class);
+
+    @Autowired
+    private AcquisitionLinkLogStrategyImpl acquisitionLinkLogStrategy;
+
+    @Autowired
+    private CourseLinkLogStrategyImpl courseLinkLogStrategy;
+
+    //注入空操作策略
+    @Autowired
+    private NoOpSmsLogStrategy noOpSmsLogStrategy;
+
+    @PostConstruct
+    public void init() {
+        strategies.put(SmsLogType.ACQUISITION_LINK, acquisitionLinkLogStrategy);
+        strategies.put(SmsLogType.COURSE_LINK, courseLinkLogStrategy);
+    }
+
+    public void executeLogStrategy(SmsLogType logType, R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        // 如果 logType 为 null,则使用空操作策略
+        if (logType == null) {
+            noOpSmsLogStrategy.recordLog(result, content, phone, tempId, type, number, contextObject);
+            return;
+        }
+
+        // 尝试从map中获取策略
+        SmsLogStrategy strategy = strategies.get(logType);
+        if (strategy == null) {
+            // 使用空操作策略,以保证业务流程不中断
+            noOpSmsLogStrategy.recordLog(result, content, phone, tempId, type, number, contextObject);
+             log.warn("未找到处理类型 [{}] 的日志策略,将执行空操作。", logType.getInfo());
+            return;
+        }
+        strategy.recordLog(result, content, phone, tempId, type, number, contextObject);
+    }
+}

+ 35 - 0
fs-service/src/main/java/com/fs/qw/strategy/impl/AcquisitionLinkLogStrategyImpl.java

@@ -0,0 +1,35 @@
+package com.fs.qw.strategy.impl;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionSendMsgLog;
+import com.fs.qw.mapper.QwAcquisitionSendMsgLogMapper;
+import com.fs.qw.strategy.SmsLogStrategy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class AcquisitionLinkLogStrategyImpl implements SmsLogStrategy {
+
+    @Autowired
+    private QwAcquisitionSendMsgLogMapper acquisitionSendMsgLogMapper;
+
+    @Override
+    public void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        SendMsgLogBo sendMsgLogBo = (SendMsgLogBo) contextObject;
+
+        QwAcquisitionSendMsgLog log = new QwAcquisitionSendMsgLog();
+        log.setCompanySmsLogsId(sendMsgLogBo.getCompanySmsLogsId());
+        log.setQwAcquisitionId(sendMsgLogBo.getQwAcquisitionId());
+        log.setNumber(number);
+        log.setType(type);
+        log.setPhone(phone);
+        log.setTempId(tempId);
+        log.setContent(content);
+        log.setCreateTime(DateUtils.getNowDate());
+        log.setResult(result.get("msg").toString());
+
+        acquisitionSendMsgLogMapper.insertQwAcquisitionSendMsgLog(log);
+    }
+}

+ 38 - 0
fs-service/src/main/java/com/fs/qw/strategy/impl/CourseLinkLogStrategyImpl.java

@@ -0,0 +1,38 @@
+package com.fs.qw.strategy.impl;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwCourseLinkSendMsgLog;
+import com.fs.qw.mapper.QwCourseLinkSendMsgLogMapper;
+import com.fs.qw.strategy.SmsLogStrategy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CourseLinkLogStrategyImpl implements SmsLogStrategy {
+
+    @Autowired
+    private QwCourseLinkSendMsgLogMapper courseLinkSendMsgLogMapper;
+
+    @Override
+    public void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+
+        SendMsgLogBo sendMsgLogBo = (SendMsgLogBo) contextObject;
+
+        QwCourseLinkSendMsgLog log = new QwCourseLinkSendMsgLog();
+        log.setCompanySmsLogsId(sendMsgLogBo.getCompanySmsLogsId());
+        log.setExternalContactId(sendMsgLogBo.getExternalId());
+        log.setCourseId(sendMsgLogBo.getCourseId());
+        log.setVideoId(sendMsgLogBo.getVideoId());
+        log.setNumber(number);
+        log.setType(type);
+        log.setPhone(phone);
+        log.setTempId(tempId);
+        log.setContent(content);
+        log.setCreateTime(DateUtils.getNowDate());
+        log.setResult(result.get("msg").toString());
+
+        courseLinkSendMsgLogMapper.insertQwCourseLinkSendMsgLog(log);
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/qw/strategy/impl/NoOpSmsLogStrategy.java

@@ -0,0 +1,18 @@
+package com.fs.qw.strategy.impl;
+
+import com.fs.common.core.domain.R;
+import com.fs.qw.strategy.SmsLogStrategy;
+import org.springframework.stereotype.Component;
+
+/**
+ * 空操作日志策略,用于处理不需要记录特定业务日志的场景
+ */
+@Component
+public class NoOpSmsLogStrategy implements SmsLogStrategy {
+
+    @Override
+    public void recordLog(R result, String content, String phone, Long tempId, String type, Integer number, Object contextObject) {
+        // 什么都不做
+        // 这样可以保证当logType为null时,simpleSmsSend也能正常运行
+    }
+}

+ 61 - 0
fs-service/src/main/java/com/fs/qw/utils/WeChatTokenUtil.java

@@ -0,0 +1,61 @@
+package com.fs.qw.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 企业微信 token / ticket 工具类
+ */
+public class WeChatTokenUtil {
+
+    private static final RestTemplate restTemplate = new RestTemplate();
+    // 缓存 access_token,实际生产应使用 redis 或数据库
+    private static String accessTokenCache;
+    private static long accessTokenExpireTime = 0;
+
+    // 缓存 agent_ticket
+    private static String agentTicketCache;
+    private static long agentTicketExpireTime = 0;
+
+    /**
+     * 获取企业 access_token(带缓存)
+     */
+    public static String getAccessToken(String corpId, String corpSecret) {
+        long now = System.currentTimeMillis() / 1000;
+        if (accessTokenCache != null && now < accessTokenExpireTime) {
+            return accessTokenCache;
+        }
+        String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + corpSecret;
+        JSONObject resp = restTemplate.getForObject(url, JSONObject.class);
+        if (resp != null && resp.getIntValue("errcode") == 0) {
+            accessTokenCache = resp.getString("access_token");
+            accessTokenExpireTime = now + resp.getIntValue("expires_in") - 300; // 提前5分钟过期
+            return accessTokenCache;
+        }
+        throw new RuntimeException("获取 access_token 失败: " + (resp == null ? "null response" : resp.getString("errmsg")));
+    }
+
+    /**
+     * 获取 agent_ticket(必须用于 agentConfig 签名)
+     * @param corpId     企业ID
+     * @param corpSecret 应用 secret
+     * @param agentId    应用ID
+     */
+    public static String getAgentTicket(String corpId, String corpSecret, String agentId) {
+        long now = System.currentTimeMillis() / 1000;
+        if (agentTicketCache != null && now < agentTicketExpireTime) {
+            return agentTicketCache;
+        }
+        String accessToken = getAccessToken(corpId, corpSecret);
+        String url = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=" + accessToken + "&type=agent_config";
+        JSONObject resp = restTemplate.getForObject(url, JSONObject.class);
+        if (resp != null && resp.getIntValue("errcode") == 0) {
+            agentTicketCache = resp.getString("ticket");
+            agentTicketExpireTime = now + resp.getIntValue("expires_in") - 300; // 提前5分钟刷新
+            return agentTicketCache;
+        }
+        throw new RuntimeException("获取 agent_ticket 失败: " + (resp == null ? "null response" : resp.getString("errmsg")));
+    }
+
+    // 如果需要普通 jsapi_ticket,也可类似实现,但本场景不需要
+}

+ 38 - 0
fs-service/src/main/java/com/fs/qw/utils/WeComSignatureUtil.java

@@ -0,0 +1,38 @@
+package com.fs.qw.utils;
+
+import com.alibaba.fastjson.JSONObject;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.util.UUID;
+
+public class WeComSignatureUtil {
+
+    /**
+     * 生成 agentConfig 签名(使用 agent_ticket)
+     * @param corpId     企业ID
+     * @param corpSecret 应用 secret
+     * @param agentId    应用ID
+     * @param url        当前页面完整URL(不含#)
+     */
+    public static JSONObject generateAgentConfigSignature(String corpId, String corpSecret, String agentId, String url) {
+        try {
+            // 1. 获取 agent_ticket
+            String ticket = WeChatTokenUtil.getAgentTicket(corpId, corpSecret, agentId);
+            // 2. 生成随机串和时间戳
+            String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
+            String timestamp = Long.toString(System.currentTimeMillis() / 1000);
+            // 3. 拼接签名字符串
+            String signStr = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url;
+            // 4. SHA1 签名
+            String signature = DigestUtils.sha1Hex(signStr);
+
+            JSONObject result = new JSONObject();
+            result.put("timestamp", timestamp);
+            result.put("nonceStr", nonceStr);
+            result.put("signature", signature);
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("生成 agentConfig 签名失败", e);
+        }
+    }
+}

+ 4 - 0
fs-service/src/main/java/com/fs/qw/vo/QwExternalContactVO.java

@@ -168,4 +168,8 @@ public class QwExternalContactVO {
      */
     private String intentionDegree;
 
+    private String qwUserId;
+
+    private Integer deptId;
+
 }

+ 27 - 0
fs-service/src/main/java/com/fs/qw/vo/QwSessionConfigVo.java

@@ -0,0 +1,27 @@
+package com.fs.qw.vo;
+
+import lombok.Data;
+/**
+ * 企业微信专区配置-会话
+ * */
+@Data
+public class QwSessionConfigVo {
+
+    private String corpid;
+
+    private String agentid;
+
+    private String agentSecret;
+
+    // 会话专区查询会话记录能力id
+    private String fetchConversationAbilityId;
+
+    //专区程序ID
+    private String programId;
+
+    //专区程序能力action
+    private String abilityAction;
+
+    //自建应用可信域名
+    private String domain;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/qwApi/domain/SpecProgramCallRequest.java

@@ -0,0 +1,22 @@
+package com.fs.qwApi.domain;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("专区程序调用请求")
+public class SpecProgramCallRequest {
+
+    @ApiModelProperty("程序能力ID,在企微管理后台配置专区程序时填写的")
+    private String abilityId;
+
+    @ApiModelProperty("输入协议数据,JSON格式,与专区程序约定的输入协议对应")
+    private String inputProtocol;
+
+    @ApiModelProperty("是否异步调用,true-异步 false-同步")
+    private Boolean async;
+
+    @ApiModelProperty("企业CorpID")
+    private String corpId;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/qwApi/domain/SpecProgramCallResponse.java

@@ -0,0 +1,27 @@
+package com.fs.qwApi.domain;
+
+import com.google.gson.annotations.SerializedName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("专区程序调用响应")
+public class SpecProgramCallResponse {
+
+    @ApiModelProperty("错误码")
+    @SerializedName("errcode")
+    private Integer errcode;
+
+    @ApiModelProperty("错误信息")
+    @SerializedName("errmsg")
+    private String errmsg;
+
+    @ApiModelProperty("任务ID,异步调用时返回,用于后续查询结果")
+    @SerializedName("task_id")
+    private String taskId;
+
+    @ApiModelProperty("输出协议数据,同步调用时直接返回专区程序的处理结果")
+    @SerializedName("output_protocol")
+    private String outputProtocol;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/qwApi/domain/SpecProgramResultResponse.java

@@ -0,0 +1,22 @@
+package com.fs.qwApi.domain;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("专区程序异步任务查询响应")
+public class SpecProgramResultResponse {
+
+    @ApiModelProperty("错误码")
+    private Integer errcode;
+
+    @ApiModelProperty("错误信息")
+    private String errmsg;
+
+    @ApiModelProperty("任务状态:1-处理中,2-处理完成,3-处理失败")
+    private Integer status;
+
+    @ApiModelProperty("专区程序返回的输出协议数据")
+    private String outputProtocol;
+}

+ 5 - 0
fs-service/src/main/java/com/fs/system/service/ISysConfigService.java

@@ -89,4 +89,9 @@ public interface ISysConfigService
     public String checkConfigKeyUnique(SysConfig config);
 
     SysConfig selectConfigByConfigKey(String configKey);
+
+    /**
+     * 根据key获取配置
+     * */
+    public <T> T getConfig(String configKey, Class<T> clazz);
 }

+ 19 - 0
fs-service/src/main/java/com/fs/system/service/impl/SysConfigServiceImpl.java

@@ -1,5 +1,7 @@
 package com.fs.system.service.impl;
 
+
+import com.alibaba.fastjson.JSON;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.constant.Constants;
 import com.fs.common.constant.UserConstants;
@@ -11,6 +13,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import javax.annotation.PostConstruct;
@@ -24,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
  *
 
  */
+@Slf4j
 @Service
 @ThreadSafe
 public class SysConfigServiceImpl implements ISysConfigService
@@ -238,6 +242,21 @@ public class SysConfigServiceImpl implements ISysConfigService
         return configMapper.selectConfigByConfigKey(configKey);
     }
 
+    @Override
+    public <T> T getConfig(String configKey, Class<T> clazz) {
+        String json = selectConfigByKey(configKey);
+        if (StringUtils.isBlank(json)) {
+            log.warn("系统配置为空, configKey={}", configKey);
+            return null;
+        }
+        try {
+            return JSON.parseObject(json, clazz);
+        } catch (Exception e) {
+            log.error("解析系统配置失败, configKey={}, json={}", configKey, json, e);
+            return null;
+        }
+    }
+
     /**
      * 设置cache key
      *

+ 11 - 0
fs-service/src/main/resources/application-config-dev.yml

@@ -122,6 +122,8 @@ ipad:
   url:
   ipadUrl: http://ipad.cdwjyyh.com
   wxIpadUrl: http://ipad.cdwjyyh.com
+#  aiApi: http://152.136.202.157:3000/api
+  aiApi: http://1.95.196.10:3000/api
   voiceApi:
   commonApi:
 wx_miniapp_temp:
@@ -135,3 +137,12 @@ jst:
 #  app_secret: dfce1f8dc8a64ddc91212fc3fcdd9349 #聚水潭2025-07-25
   authorization_code: 666666
   shop_code: "18461733"
+weizou:
+  baseUrl: http://113.105.104.56:9620
+#  appId:
+#  appSecret:
+#  createName:
+#crm:
+#  customer:
+#    ai:
+#      key:

+ 1 - 1
fs-service/src/main/resources/application-config-druid-sxsm.yml

@@ -43,7 +43,7 @@ wx:
       timeout: 2000
     configs:
       - appId: wx036be2b18aa52235 # 第一个公众号的appid  //公众号名称:成都九州在线互联网医院
-        secret: 5da9ae6d354c130dc81cde1a6d097480 # 公众号的appsecret
+        secret: 6cea6a9c9e885005e5f7a26e773bd4f2 # 公众号的appsecret
 #      - appId: wx8e9574e8932f9e1d # 第一个公众号的appid  //公众号名称:挑宝连锁
 #        secret: 4504d4faa16d75a6551ad67a011d95cc # 公众号的appsecret
         token: PPKOdAlCoMO # 接口配置里的Token值

+ 51 - 6
fs-service/src/main/resources/mapper/company/CompanySmsMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.company.mapper.CompanySmsMapper">
-    
+
     <resultMap type="CompanySms" id="CompanySmsResult">
         <result property="smsId"    column="sms_id"    />
         <result property="companyId"    column="company_id"    />
@@ -16,12 +16,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </sql>
 
 
-    
+
     <select id="selectCompanySmsById" parameterType="Long" resultMap="CompanySmsResult">
         <include refid="selectCompanySmsVo"/>
         where sms_id = #{smsId}
     </select>
-        
+
     <insert id="insertCompanySms" parameterType="CompanySms" useGeneratedKeys="true" keyProperty="smsId">
         insert into company_sms
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -51,10 +51,55 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteCompanySmsByIds" parameterType="String">
-        delete from company_sms where sms_id in 
+        delete from company_sms where sms_id in
         <foreach item="smsId" collection="array" open="(" separator="," close=")">
             #{smsId}
         </foreach>
     </delete>
-    
-</mapper>
+
+    <!-- 直接扣减 -->
+    <update id="decrementRemainSmsCount">
+        update company_sms
+        set remain_sms_count = remain_sms_count - #{number},
+            total_sms_count = total_sms_count - #{number},
+            version = version + 1,
+            update_time = NOW()
+        where company_id = #{companyId}
+          and remain_sms_count >= #{number}
+    </update>
+
+    <!-- 直接增加 -->
+    <update id="incrementRemainSmsCount">
+        update company_sms
+        set remain_sms_count = remain_sms_count + #{number},
+            total_sms_count = total_sms_count + #{number},
+            version = version + 1,
+            update_time = NOW()
+        where company_id = #{companyId}
+    </update>
+
+    <!-- 乐观锁扣减 -->
+    <update id="decrementRemainSmsCountWithVersion">
+        update company_sms
+        set remain_sms_count = remain_sms_count - #{number},
+            total_sms_count = total_sms_count - #{number},
+            version = version + 1,
+            update_time = NOW()
+        where company_id = #{companyId}
+          and remain_sms_count >= #{number}
+          and version = #{currentVersion}
+    </update>
+
+    <!-- 乐观锁增加 -->
+    <update id="incrementRemainSmsCountWithVersion">
+        update company_sms
+        set remain_sms_count = remain_sms_count + #{number},
+            total_sms_count = total_sms_count + #{number},
+            version = version + 1,
+            update_time = NOW()
+        where company_id = #{companyId}
+          and version = #{currentVersion}
+    </update>
+
+
+</mapper>

+ 1 - 1
fs-service/src/main/resources/mapper/company/CompanyWithdrawDetailMapper.xml

@@ -65,7 +65,7 @@
                 WHEN 8 THEN '总公司驳回提现'
                 WHEN 3 THEN '订单金额入账'
                 WHEN 6 THEN '订单金额扣减'
-                WHEN 4 THEN '订单金额扣减' END
+                WHEN 4 THEN '订单金额扣减'
                 WHEN 5 THEN CASE
                     WHEN IFNULL(l.remark, '') LIKE '%退款%' OR l.money &lt; 0 THEN '订单金额扣减'
                     ELSE '订单金额入账'

+ 359 - 0
fs-service/src/main/resources/mapper/his/FsDoctorMapper.xml

@@ -64,6 +64,64 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="gptRoleId"    column="gpt_role_id"    />
     </resultMap>
 
+    <resultMap type="com.fs.his.param.DoctorPrescriptionImgLogParam" id="DoctorPrescriptionImgLogResult">
+        <result property="id"    column="id"    />
+        <result property="doctorId"    column="doctor_id"    />
+        <result property="url"    column="url"    />
+        <result property="status"    column="status"    />
+        <result property="isDel"    column="is_del"    />
+        <result property="remark"    column="remark"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <association property="doctorInfo" javaType="com.fs.his.param.FsDoctorParam">
+            <result property="doctorId"    column="doctor_id"    />
+            <result property="doctorName"    column="doctor_name"    />
+            <result property="introduction"    column="introduction"    />
+            <result property="speciality"    column="speciality"    />
+            <result property="certificateCode"    column="certificate_code"    />
+            <result property="certificateImages"    column="certificate_images"    />
+            <result property="practiseCode"    column="practise_code"    />
+            <result property="practiseImages"    column="practise_images"    />
+            <result property="avatar"    column="avatar"    />
+            <result property="hospitalId"    column="hospital_id"    />
+            <result property="deptId"    column="dept_id"    />
+            <result property="position"    column="position"    />
+            <result property="tags"    column="tags"    />
+            <result property="status"    column="doctor_status"    />
+            <result property="pingStar"    column="ping_star"    />
+            <result property="orderNumber"    column="order_number"    />
+            <result property="mobile"    column="mobile"    />
+            <result property="doctorType"    column="doctor_type"    />
+            <result property="sex"    column="sex"    />
+            <result property="workStatus"    column="work_status"    />
+            <result property="isAudit"    column="is_audit"    />
+        </association>
+
+        <association property="prescription" javaType="com.fs.his.param.DoctorPrescriptionParam">
+            <result property="id"    column="prescription_id"    />
+            <result property="isParse"    column="is_parse"    />
+            <result property="prescriptionCode"    column="prescription_code"    />
+            <result property="age"    column="age"    />
+            <result property="sex"    column="patient_sex"    />
+            <result property="prescriptionImg"    column="prescription_img"    />
+            <result property="prescriptionInfo"    column="prescription_info"    />
+            <result property="chiefComplaint"    column="chief_complaint"    />
+            <result property="historyOfPresentIllness"    column="history_of_present_illness"    />
+            <result property="pastMedicalHistory"    column="past_medical_history"    />
+            <result property="personalHistory"    column="personal_history"    />
+            <result property="obstetricHistory"    column="obstetric_history"    />
+            <result property="familyHistory"    column="family_history"    />
+            <result property="suggest"    column="suggest"    />
+            <result property="status"    column="prescription_status"    />
+            <result property="doctorId"    column="prescription_doctor_id"    />
+            <result property="logId"    column="prescription_log_id"    />
+            <result property="isDel"    column="prescription_is_del"    />
+            <result property="createTime"    column="prescription_create_time"    />
+            <result property="updateTime"    column="prescription_update_time"    />
+            <result property="remark"    column="prescription_remark"    />
+        </association>
+    </resultMap>
+
     <sql id="selectFsDoctorVo">
         select doctor_id,is_agreement_prescribe_doctor,is_follow,is_show,audit_type,store_ids,gpt_role_id, package_ids,sort,doctor_card_url,prescribe_doctor_id,is_prescribe_doctor,doctor_name,is_accept,is_self, introduction, speciality, certificate_code, certificate_images, practise_code, practise_images, avatar, user_id, hospital_id, dept_id, balance, position, tags, create_time, update_time, status, remark, ping_star, order_number, speed, mobile, doctor_type, sex, bitthday, id_card, id_card_front_url, id_card_back_url, city_ids, province, city, district, is_tui, is_expert, work_status, is_audit, audit_time, price_json, account, password,sign_url,jpush_id,extract_json from fs_doctor
     </sql>
@@ -181,6 +239,195 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{doctorId}
         </foreach>
     </select>
+    <select id="selectFsDoctorPrescriptionByCode" resultType="java.lang.Integer">
+        select count(1) from fs_doctor_prescription_new where prescription_code = #{code} and is_del = 0
+    </select>
+
+    <select id="selectFsDoctorPrescribeImgByDoctorId"
+            resultType="com.fs.his.param.DoctorPrescriptionImgLogParam">
+        select id,doctor_id doctorId,url,status,remark from fs_doctor_prescription_img_log
+        where is_del = 0
+          <if test="doctorId != null">and doctor_id = #{doctorId}</if>
+          <if test="status != null">and status != #{status}</if>
+        order by id desc
+    </select>
+    <select id="selectAllPrescriptionImgList" resultMap="DoctorPrescriptionImgLogResult">
+        SELECT
+        l.id,
+        l.doctor_id,
+        l.url,
+        l.status,
+        l.is_del,
+        l.remark,
+        l.create_time,
+        l.update_time,
+        l.remark,
+        p.id AS prescription_id,
+        p.is_parse AS prescription_is_parse,
+        p.prescription_code AS prescription_code,
+        p.age AS prescription_age,
+        p.sex AS prescription_sex,
+        p.prescription_img AS prescription_img,
+        p.prescription_info AS prescription_info,
+        p.chief_complaint AS prescription_chief_complaint,
+        p.history_of_present_illness AS prescription_history_of_present_illness,
+        p.past_medical_history AS prescription_past_medical_history,
+        p.personal_history AS prescription_personal_history,
+        p.obstetric_history AS prescription_obstetric_history,
+        p.family_history AS prescription_family_history,
+        p.suggest AS prescription_suggest,
+        p.status AS prescription_status,
+        p.doctor_id AS prescription_doctor_id,
+        p.remark AS prescription_remark
+        FROM fs_doctor_prescription_img_log l
+        LEFT JOIN fs_doctor_prescription_new p ON l.id = p.log_id
+        WHERE l.is_del = 0
+        <if test="doctorId != null and doctorId != ''">
+            AND l.doctor_id = #{doctorId}
+        </if>
+        <if test="param.status != null and param.status != ''">
+            AND p.status = #{param.status}
+        </if>
+        ORDER BY l.id DESC
+    </select>
+
+    <select id="selectPrescriptionImgInfo" resultType="com.fs.his.param.DoctorPrescriptionParam">
+        SELECT
+        p.id,
+        p.doctor_id doctorId,
+        p.prescription_code prescriptionCode,
+        p.age,
+        p.sex,
+        p.chief_complaint,
+        p.history_of_present_illness historyOfPresentIllness,
+        p.past_medical_history pastMedicalHistory,
+        p.personal_history personalHistory,
+        p.obstetric_history obstetricHistory,
+        p.family_history familyHistory,
+        p.is_parse isParse,
+        p.prescription_info prescriptionInfo,
+        p.prescription_img prescriptionImg,
+        p.suggest suggest,
+        p.status,
+        p.log_id logId,p.remark remark
+        from fs_doctor_prescription_new p
+        where id = #{id}
+    </select>
+    <select id="selectPrescriptionImgInfoByLogId" resultType="com.fs.his.param.DoctorPrescriptionParam">
+        SELECT
+            p.id,
+            p.doctor_id doctorId,
+            p.prescription_code prescriptionCode,
+            p.age,
+            p.sex,
+            p.chief_complaint,
+            p.history_of_present_illness historyOfPresentIllness,
+            p.past_medical_history pastMedicalHistory,
+            p.personal_history personalHistory,
+            p.obstetric_history obstetricHistory,
+            p.family_history familyHistory,
+            p.is_parse isParse,
+            p.prescription_info prescriptionInfo,
+            p.prescription_img prescriptionImg,
+            p.suggest suggest,
+            p.status,
+            p.log_id logId,
+            p.is_del isDel,
+            p.remark remark
+        from fs_doctor_prescription_new p
+        where log_id = #{logId}
+    </select>
+    <select id="selectFsDoctorPrescribeImgById" resultType="com.fs.his.param.DoctorPrescriptionImgLogParam">
+        SELECT
+        id,
+        doctor_id doctorId,
+        url,
+        status,
+        is_del,
+        remark,
+        create_time,
+        update_time,
+        remark
+        FROM fs_doctor_prescription_img_log
+        WHERE id = #{id}
+    </select>
+    <select id="selectPrescriptionLogList" resultMap="DoctorPrescriptionImgLogResult">
+        SELECT
+        d1.id,
+        d1.doctor_id,
+        d1.url,
+        d1.status,
+        d1.is_del,
+        d1.remark,
+        d1.create_time,
+        d1.update_time,
+        d2.doctor_id,
+        d2.doctor_name,
+        d2.introduction,
+        d2.speciality,
+        d2.certificate_code,
+        d2.certificate_images,
+        d2.practise_code,
+        d2.practise_images,
+        d2.avatar,
+        d2.hospital_id,
+        d2.dept_id,
+        d2.position,
+        d2.tags,
+        d2.status AS doctor_status,
+        d2.ping_star,
+        d2.order_number,
+        d2.mobile,
+        d2.doctor_type,
+        d2.sex,
+        d2.work_status,
+        d2.is_audit,
+        d3.id AS prescription_id,
+        d3.is_parse,
+        d3.prescription_code,
+        d3.age,
+        d3.sex AS patient_sex,
+        d3.prescription_img,
+        d3.prescription_info,
+        d3.chief_complaint,
+        d3.history_of_present_illness,
+        d3.past_medical_history,
+        d3.personal_history,
+        d3.obstetric_history,
+        d3.family_history,
+        d3.suggest,
+        d3.status AS prescription_status,
+        d3.doctor_id AS prescription_doctor_id,
+        d3.log_id AS prescription_log_id,
+        d3.is_del AS prescription_is_del,
+        d3.create_time AS prescription_create_time,
+        d3.update_time AS prescription_update_time,
+        d3.remark AS prescription_remark
+        FROM fs_doctor_prescription_img_log d1
+        LEFT JOIN fs_doctor d2 ON d1.doctor_id = d2.doctor_id
+        LEFT JOIN fs_doctor_prescription_new d3 ON d1.id = d3.log_id
+        WHERE d1.is_del = 0 and d3.status > 2
+        <if test="param.doctorId != null">
+            AND d1.doctor_id = #{doctorId}
+        </if>
+        <if test="param.beginTime != null and param.beginTime != ''">
+            AND DATE(d1.create_time) &gt;= DATE(#{param.beginTime})
+        </if>
+        <if test="param.endTime != null and param.endTime != ''">
+            AND DATE(d1.create_time) &lt;= DATE(#{param.endTime})
+        </if>
+        <if test="param.doctorName != null and param.doctorName != null">
+            AND d2.doctor_name LIKE CONCAT('%', #{param.doctorName}, '%')
+        </if>
+        <if test="param.prescription != null and param.prescription.status != null and param.prescription.status != ''">
+            AND d3.status LIKE CONCAT('%', #{param.prescription.status}, '%')
+        </if>
+        <if test="param.prescription != null and param.prescription.prescriptionCode != null and param.prescription.prescriptionCode != ''">
+            AND d3.prescription_code LIKE CONCAT('%', #{param.prescription.prescriptionCode}, '%')
+        </if>
+        ORDER BY d1.id DESC
+    </select>
+
 
     <insert id="insertFsDoctor" parameterType="FsDoctor" useGeneratedKeys="true" keyProperty="doctorId">
         insert into fs_doctor
@@ -301,6 +548,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="gptRoleId != null">#{gptRoleId},</if>
          </trim>
     </insert>
+    <insert id="insertFsDoctorPrescription" parameterType="com.fs.his.param.DoctorPrescriptionParam" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_doctor_prescription_new
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="isParse != null">is_parse,</if>
+            <if test="prescriptionCode != null">prescription_code,</if>
+            <if test="age != null">age,</if>
+            <if test="sex != null">sex,</if>
+            <if test="prescriptionImg != null">prescription_img,</if>
+            <if test="prescriptionInfo != null">prescription_info,</if>
+            <if test="chiefComplaint != null">chief_complaint,</if>
+            <if test="historyOfPresentIllness != null">history_of_present_illness,</if>
+            <if test="pastMedicalHistory != null">past_medical_history,</if>
+            <if test="personalHistory != null">personal_history,</if>
+            <if test="obstetricHistory != null">obstetric_history,</if>
+            <if test="familyHistory != null">family_history,</if>
+            <if test="suggest != null">suggest,</if>
+            <if test="status != null">status,</if>
+            <if test="doctorId != null">doctor_id,</if>
+            <if test="logId != null">log_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="isParse != null">#{isParse},</if>
+            <if test="prescriptionCode != null">#{prescriptionCode},</if>
+            <if test="age != null">#{age},</if>
+            <if test="sex != null">#{sex},</if>
+            <if test="prescriptionImg != null">#{prescriptionImg},</if>
+            <if test="prescriptionInfo != null">#{prescriptionInfo},</if>
+            <if test="chiefComplaint != null">#{chiefComplaint},</if>
+            <if test="historyOfPresentIllness != null">#{historyOfPresentIllness},</if>
+            <if test="pastMedicalHistory != null">#{pastMedicalHistory},</if>
+            <if test="personalHistory != null">#{personalHistory},</if>
+            <if test="obstetricHistory != null">#{obstetricHistory},</if>
+            <if test="familyHistory != null">#{familyHistory},</if>
+            <if test="suggest != null">#{suggest},</if>
+            <if test="status != null">#{status},</if>
+            <if test="doctorId != null">#{doctorId},</if>
+            <if test="logId != null">#{logId},</if>
+        </trim>
+    </insert>
+    <insert id="insertFsDoctorPrescriptionImgLog" parameterType="com.fs.his.param.DoctorPrescriptionImgLogParam" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_doctor_prescription_img_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="doctorId != null">doctor_id,</if>
+            <if test="url != null">url,</if>
+            <if test="status != null">status,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="doctorId != null">#{doctorId},</if>
+            <if test="url != null">#{url},</if>
+            <if test="status != null">#{status},</if>
+        </trim>
+    </insert>
+
 
     <update id="updateFsDoctor" parameterType="FsDoctor">
         update fs_doctor
@@ -364,6 +664,65 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </trim>
         where doctor_id = #{doctorId}
     </update>
+    <update id="updateFsDoctorPrescriptionImgLog" parameterType="com.fs.his.param.DoctorPrescriptionImgLogParam">
+        update fs_doctor_prescription_img_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="doctorId != null">doctor_id = #{doctorId},</if>
+            <if test="url != null">url = #{url},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="updateFsDoctorPrescription" parameterType="com.fs.his.param.DoctorPrescriptionParam">
+        update fs_doctor_prescription_new
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="isParse != null">is_parse = #{isParse},</if>
+            <if test="prescriptionCode != null">prescription_code = #{prescriptionCode},</if>
+            <if test="age != null">age = #{age},</if>
+            <if test="sex != null">sex = #{sex},</if>
+            <if test="prescriptionImg != null">prescription_img = #{prescriptionImg},</if>
+            <if test="prescriptionInfo != null ">prescription_info = #{prescriptionInfo},</if>
+            <if test="chiefComplaint != null">chief_complaint = #{chiefComplaint},</if>
+            <if test="historyOfPresentIllness != null">history_of_present_illness = #{historyOfPresentIllness},</if>
+            <if test="pastMedicalHistory != null">past_medical_history = #{pastMedicalHistory},</if>
+            <if test="personalHistory != null">personal_history = #{personalHistory},</if>
+            <if test="obstetricHistory != null">obstetric_history = #{obstetricHistory},</if>
+            <if test="familyHistory != null">family_history = #{familyHistory},</if>
+            <if test="suggest != null">suggest = #{suggest},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="doctorId != null">doctor_id = #{doctorId},</if>
+            <if test="logId != null">log_id = #{logId},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+    <update id="updateFsDoctorPrescriptionByLogId">
+        update fs_doctor_prescription_new
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="isParse != null">is_parse = #{isParse},</if>
+            <if test="prescriptionCode != null">prescription_code = #{prescriptionCode},</if>
+            <if test="age != null">age = #{age},</if>
+            <if test="sex != null">sex = #{sex},</if>
+            <if test="prescriptionImg != null">prescription_img = #{prescriptionImg},</if>
+            <if test="prescriptionInfo != null ">prescription_info = #{prescriptionInfo},</if>
+            <if test="chiefComplaint != null">chief_complaint = #{chiefComplaint},</if>
+            <if test="historyOfPresentIllness != null">history_of_present_illness = #{historyOfPresentIllness},</if>
+            <if test="pastMedicalHistory != null">past_medical_history = #{pastMedicalHistory},</if>
+            <if test="personalHistory != null">personal_history = #{personalHistory},</if>
+            <if test="obstetricHistory != null">obstetric_history = #{obstetricHistory},</if>
+            <if test="familyHistory != null">family_history = #{familyHistory},</if>
+            <if test="suggest != null">suggest = #{suggest},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="doctorId != null">doctor_id = #{doctorId},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
 
     <delete id="deleteFsDoctorByDoctorId" parameterType="Long">
         delete from fs_doctor where doctor_id = #{doctorId}

+ 2 - 0
fs-service/src/main/resources/mapper/his/FsIntegralCountMapper.xml

@@ -24,6 +24,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="integralConsume != null  and integralConsume != ''"> and integral_consume = #{integralConsume}</if>
             <if test="integralRefund != null  and integralRefund != ''"> and integral_refund = #{integralRefund}</if>
             <if test="logType != null  and logType != ''"> and log_type = #{logType}</if>
+            <if test="beginDate != null  and beginDate != ''"> and consumption_date >= #{beginDate}</if>
+            <if test="endDate != null  and endDate != ''"> and consumption_date &lt;= #{endDate}</if>
         </where>
     </select>
 

+ 52 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreAfterSalesScrmMapper.xml

@@ -69,6 +69,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectFsStoreAfterSalesVo"/>
         where id = #{id}
     </select>
+    <select id="selectFsStoreAfterSalesListQueryNew" resultType="com.fs.hisStore.vo.FsStoreAfterSalesQueryVO">
+        select a1.id,
+        a1.order_code,
+        a1.refund_amount,
+        a1.service_type,
+        a1.reasons,
+        a1.explains,
+        a1.explain_img,
+        a1.shipper_code,
+        a1.delivery_sn,
+        a1.delivery_name,
+        a1.status,
+        a1.sales_status,
+        a1.order_status,
+        a1.create_time,
+        a1.is_del,
+        a1.user_id,
+        a1.consignee,
+        a1.phone_number,
+        a1.address,
+        a1.company_id,
+        a1.company_user_id,
+        a1.is_package,
+        a1.package_json,
+        a1.reason_id1,
+        a1.reason_id2,
+        a1.reason_level1_text,
+        a1.reason_level2_text,
+        a1.audit_remark,
+        a1.audit_reason_name,
+        a2.live_id as liveId
+        from fs_store_after_sales_scrm a1
+        left join fs_store_order_scrm a2
+        on a1.order_code = a2.order_code
+        where 1=1
+        <if test="maps.status != null and maps.status == 1">
+            and a1.sales_status = 0
+        </if>
+        <if test="maps.status != null and maps.status == 2">
+            and a1.sales_status = 3
+        </if>
+        <if test="maps.liveId == null">
+            and a2.live_id is null
+        </if>
+        <if test="maps.liveId == 0">
+            and a2.live_id is not null
+        </if>
+        <if test="maps.userId != null">
+            and a1.user_id = #{maps.userId}
+        </if>
+        order by a1.create_time desc
+    </select>
 
     <insert id="insertFsStoreAfterSales" parameterType="FsStoreAfterSalesScrm" useGeneratedKeys="true" keyProperty="id">
         insert into fs_store_after_sales_scrm

+ 6 - 3
fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -94,10 +94,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="virtualPhone"    column="virtual_phone"    />
         <result property="groupBuyId"    column="group_buy_id"    />
         <result property="shoppingPointsClaimed"    column="shopping_points_claimed"    />
+        <result property="liveId"    column="live_id"    />
     </resultMap>
 
     <sql id="selectFsStoreOrderVo">
-        select id, order_code,outer_oi_id,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,backend_edit_product_type,video_id,course_id,project_id,period_id,virtual_phone,group_buy_id from fs_store_order_scrm
+        select id, order_code,outer_oi_id,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,backend_edit_product_type,video_id,course_id,project_id,period_id,virtual_phone,group_buy_id,live_id from fs_store_order_scrm
     </sql>
 
     <select id="selectFsStoreOrderList" parameterType="FsStoreOrderScrm" resultMap="FsStoreOrderResult">
@@ -290,6 +291,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="projectId != null" >project_id,</if>
             <if test="periodId != null" >period_id,</if>
             <if test="groupBuyId != null" >group_buy_id,</if>
+            <if test="liveId != null" >live_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="orderCode != null and orderCode != ''">#{orderCode},</if>
@@ -382,6 +384,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="projectId != null" >#{projectId},</if>
             <if test="periodId != null" >#{periodId},</if>
             <if test="groupBuyId != null" >#{groupBuyId},</if>
+            <if test="liveId != null" >#{liveId},</if>
          </trim>
     </insert>
 
@@ -965,10 +968,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
         <!-- 时间条件 -->
         <if test="param.startDate != null">
-            AND DATE_FORMAT(fso.pay_time, '%Y-%m') >= DATE_FORMAT(#{param.startDate}, '%Y-%m')
+            AND DATE_FORMAT(fso.pay_time, '%Y-%m-%d') >= DATE_FORMAT(#{param.startDate}, '%Y-%m-%d')
         </if>
         <if test="param.endDate != null">
-            AND DATE_FORMAT(fso.pay_time, '%Y-%m') &lt;= DATE_FORMAT(#{param.endDate}, '%Y-%m')
+            AND DATE_FORMAT(fso.pay_time, '%Y-%m-%d') &lt;= DATE_FORMAT(#{param.endDate}, '%Y-%m-%d')
         </if>
 
         <!-- 商品和标签筛选(使用EXISTS避免重复计算) -->

+ 1 - 1
fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml

@@ -212,7 +212,7 @@
     </select>
 
     <select id="selectLiveWatchAndRegisterUser" resultType="com.fs.live.domain.LiveWatchUser">
-        select a.*,fu.nickname as nick_name from (
+        select a.*,fu.nick_name as nickname from (
             select lws.* from live_watch_user lws 
             where live_id=#{params.liveId} and online = 0
             <if test="params.liveFlag != null "> and live_flag = #{params.liveFlag}</if>

+ 213 - 0
fs-service/src/main/resources/mapper/qw/QwAcquisitionLinkInfoMapper.xml

@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwAcquisitionLinkInfoMapper">
+
+    <resultMap type="com.fs.qw.domain.QwAcquisitionLinkInfo" id="QwAcquisitionLinkInfoResult">
+        <result property="id" column="id"/>
+        <result property="qwAcquisitionAssistantId" column="qw_acquisition_assistant_id"/>
+        <result property="link" column="link"/>
+        <result property="phone" column="phone"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+        <result property="randomStr" column="random_str"/>
+        <result property="linkName" column="link_name"/>
+        <result property="createName" column="nick_name"/>
+    </resultMap>
+
+    <sql id="selectQwAcquisitionLinkInfoVo">
+        select id,
+               qw_acquisition_assistant_id,
+               link,
+               phone,
+               create_by,
+               create_time,
+               update_by,
+               update_time,
+               remark,
+               random_str
+        from qw_acquisition_link_info
+    </sql>
+
+    <select id="selectQwAcquisitionLinkInfoList" parameterType="com.fs.qw.domain.QwAcquisitionLinkInfo"
+            resultMap="QwAcquisitionLinkInfoResult">
+        select acinfo.id, acinfo.qw_acquisition_assistant_id, acinfo.link, acinfo.phone, acinfo.create_by,
+        acinfo.create_time, acinfo.update_by,
+        acinfo.update_time,acinfo.remark,acinfo.random_str, acas.link_name,cu.nick_name
+        from qw_acquisition_link_info acinfo
+        left join qw_acquisition_assistant acas on acinfo.qw_acquisition_assistant_id = acas.id
+        left join company_user cu on cu.user_id = acinfo.create_by
+        <where>
+            <if test="qwAcquisitionAssistantId != null and qwAcquisitionAssistantId != ''">
+                and acinfo.qw_acquisition_assistant_id = #{qwAcquisitionAssistantId}
+            </if>
+            <if test="link != null and link != ''">
+                and acinfo.link like concat('%', #{link}, '%')
+            </if>
+            <if test="phone != null and phone != ''">
+                and acinfo.phone like concat('%', #{phone}, '%')
+            </if>
+            <if test="linkName != null and linkName != ''">
+                and acas.link_name like concat('%', #{linkName}, '%')
+            </if>
+            <if test="createName != null and createName != ''">
+                and cu.nick_name like concat('%', #{createName}, '%')
+            </if>
+            <if test="remark != null and remark != ''">
+                and acinfo.remark like concat('%', #{remark}, '%')
+            </if>
+            <if test="randomStr != null and randomStr != ''">
+                and acinfo.random_str like concat('%', #{randomStr}, '%')
+            </if>
+        </where>
+        order by acinfo.create_time desc
+    </select>
+
+    <select id="selectQwAcquisitionLinkInfoById" parameterType="Long" resultMap="QwAcquisitionLinkInfoResult">
+        <include refid="selectQwAcquisitionLinkInfoVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectAllRandomStr" resultType="java.lang.String">
+        select random_str
+        from qw_acquisition_link_info
+    </select>
+    <select id="selectQwAcquisitionUrlByRandomStr" resultType="java.lang.String">
+        select link
+        from qw_acquisition_link_info
+        where random_str = #{randomStr}
+    </select>
+
+    <select id="selectAcquisitionLinkInfoListByIds" resultMap="QwAcquisitionLinkInfoResult">
+        select id, qw_acquisition_assistant_id, link, phone, create_by, create_time, update_by, update_time,
+        remark,random_str from qw_acquisition_link_info where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <select id="selectAcquisitionLinkInfoListByAcquisitionAssistantIds" resultMap="QwAcquisitionLinkInfoResult">
+        select id, qw_acquisition_assistant_id, link, phone, create_by, create_time, update_by, update_time,
+        remark,random_str
+        from qw_acquisition_link_info
+        where qw_acquisition_assistant_id in
+        <foreach item="id" collection="qwAcquisitionAssistantIds" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <insert id="insertQwAcquisitionLinkInfo" parameterType="com.fs.qw.domain.QwAcquisitionLinkInfo"
+            useGeneratedKeys="true" keyProperty="id">
+        insert into qw_acquisition_link_info
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="qwAcquisitionAssistantId != null">
+                qw_acquisition_assistant_id,
+            </if>
+            <if test="link != null and link != ''">
+                link,
+            </if>
+            <if test="phone != null and phone != ''">
+                phone,
+            </if>
+            <if test="createBy != null and createBy != ''">
+                create_by,
+            </if>
+            <if test="createTime != null">
+                create_time,
+            </if>
+            <if test="updateBy != null and updateBy != ''">
+                update_by,
+            </if>
+            <if test="updateTime != null">
+                update_time,
+            </if>
+            <if test="remark != null and remark != ''">
+                remark,
+            </if>
+            <if test="randomStr != null and randomStr != ''">
+                random_str,
+            </if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="qwAcquisitionAssistantId != null">
+                #{qwAcquisitionAssistantId},
+            </if>
+            <if test="link != null and link != ''">
+                #{link},
+            </if>
+            <if test="phone != null and phone != ''">
+                #{phone},
+            </if>
+            <if test="createBy != null and createBy != ''">
+                #{createBy},
+            </if>
+            <if test="createTime != null">
+                #{createTime},
+            </if>
+            <if test="updateBy != null and updateBy != ''">
+                #{updateBy},
+            </if>
+            <if test="updateTime != null">
+                #{updateTime},
+            </if>
+            <if test="remark != null and remark != ''">
+                #{remark},
+            </if>
+            <if test="randomStr != null and randomStr != ''">
+                #{randomStr},
+            </if>
+        </trim>
+    </insert>
+
+    <update id="updateQwAcquisitionLinkInfo" parameterType="com.fs.qw.domain.QwAcquisitionLinkInfo">
+        update qw_acquisition_link_info
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="qwAcquisitionAssistantId != null">
+                qw_acquisition_assistant_id = #{qwAcquisitionAssistantId},
+            </if>
+            <if test="link != null and link != ''">
+                link = #{link},
+            </if>
+            <if test="phone != null and phone != ''">
+                phone = #{phone},
+            </if>
+            <if test="updateBy != null and updateBy != ''">
+                update_by = #{updateBy},
+            </if>
+            <if test="updateTime != null">
+                update_time = #{updateTime},
+            </if>
+            <if test="remark != null and remark != ''">
+                remark = #{remark},
+            </if>
+            <if test="randomStr != null and randomStr != ''">
+                random_str = #{randomStr},
+            </if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwAcquisitionLinkInfoById" parameterType="Long">
+        delete
+        from qw_acquisition_link_info
+        where id = #{id}
+    </delete>
+
+    <delete id="deleteQwAcquisitionLinkInfoByIds" parameterType="String">
+        delete from qw_acquisition_link_info where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds" parameterType="String">
+        delete from qw_acquisition_link_info where qw_acquisition_assistant_id in
+        <foreach item="id" collection="qwAcquisitionAssistantIds" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 105 - 0
fs-service/src/main/resources/mapper/qw/QwAcquisitionSendMsgLogMapper.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwAcquisitionSendMsgLogMapper">
+
+    <resultMap type="com.fs.qw.domain.QwAcquisitionSendMsgLog" id="QwAcquisitionSendMsgLogResult">
+        <result property="id"    column="id"    />
+        <result property="companySmsLogsId"    column="company_sms_logs_id"    />
+        <result property="qwAcquisitionId"    column="qw_acquisition_id"    />
+        <result property="phone"    column="phone"    />
+        <result property="number"    column="number"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="type"    column="type"    />
+        <result property="content"    column="content"    />
+        <result property="result"    column="result"    />
+        <result property="remark"    column="remark"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectQwAcquisitionSendMsgLogVo">
+        select id, company_sms_logs_id,qw_acquisition_id, phone, number, temp_id, type, content, result, remark, create_by, create_time from qw_acquisition_send_msg_log
+    </sql>
+
+    <select id="selectQwAcquisitionSendMsgLogList" parameterType="com.fs.qw.domain.QwAcquisitionSendMsgLog" resultMap="QwAcquisitionSendMsgLogResult">
+        <include refid="selectQwAcquisitionSendMsgLogVo"/>
+        <where>
+            <if test="companySmsLogsId != null  and companySmsLogsId != ''"> and company_sms_logs_id = #{companySmsLogsId}</if>
+            <if test="qwAcquisitionId != null  and qwAcquisitionId != ''"> and qw_acquisition_id = #{qwAcquisitionId}</if>
+            <if test="phone != null  and phone != ''"> and phone like concat('%', #{phone}, '%')</if>
+            <if test="number != null "> and number = #{number}</if>
+            <if test="tempId != null "> and temp_id = #{tempId}</if>
+            <if test="type != null  and type != ''"> and type like concat('%', #{type}, '%')</if>
+            <if test="content != null  and content != ''"> and content like concat('%', #{content}, '%')</if>
+            <if test="result != null  and result != ''"> and result like concat('%', #{result}, '%')</if>
+            <if test="remark != null  and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectQwAcquisitionSendMsgLogById" parameterType="Long" resultMap="QwAcquisitionSendMsgLogResult">
+        <include refid="selectQwAcquisitionSendMsgLogVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwAcquisitionSendMsgLog" parameterType="com.fs.qw.domain.QwAcquisitionSendMsgLog" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_acquisition_send_msg_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companySmsLogsId != null and companySmsLogsId != ''">company_sms_logs_id,</if>
+            <if test="qwAcquisitionId != null and qwAcquisitionId != ''">qw_acquisition_id,</if>
+            <if test="phone != null and phone != ''">phone,</if>
+            <if test="number != null">number,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="type != null and type != ''">type,</if>
+            <if test="content != null and content != ''">content,</if>
+            <if test="result != null and result != ''">result,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            create_time,
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companySmsLogsId != null and companySmsLogsId != ''">#{companySmsLogsId},</if>
+            <if test="qwAcquisitionId != null and qwAcquisitionId != ''">#{qwAcquisitionId},</if>
+            <if test="phone != null and phone != ''">#{phone},</if>
+            <if test="number != null">#{number},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="type != null and type != ''">#{type},</if>
+            <if test="content != null and content != ''">#{content},</if>
+            <if test="result != null and result != ''">#{result},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            sysdate(),
+        </trim>
+    </insert>
+
+    <update id="updateQwAcquisitionSendMsgLog" parameterType="com.fs.qw.domain.QwAcquisitionSendMsgLog">
+        update qw_acquisition_send_msg_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companySmsLogsId != null and companySmsLogsId != ''">company_sms_logs_id = #{companySmsLogsId},</if>
+            <if test="qwAcquisitionId != null and qwAcquisitionId != ''">qw_acquisition_id = #{qwAcquisitionId},</if>
+            <if test="phone != null and phone != ''">phone = #{phone},</if>
+            <if test="number != null">number = #{number},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="type != null and type != ''">type = #{type},</if>
+            <if test="content != null and content != ''">content = #{content},</if>
+            <if test="result != null and result != ''">result = #{result},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate(),
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwAcquisitionSendMsgLogById" parameterType="Long">
+        delete from qw_acquisition_send_msg_log where id = #{id}
+    </delete>
+
+    <delete id="deleteQwAcquisitionSendMsgLogByIds" parameterType="String">
+        delete from qw_acquisition_send_msg_log where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 115 - 0
fs-service/src/main/resources/mapper/qw/QwCourseLinkSendMsgLogMapper.xml

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwCourseLinkSendMsgLogMapper">
+
+    <resultMap type="com.fs.qw.domain.QwCourseLinkSendMsgLog" id="QwCourseLinkSendMsgLogResult">
+        <result property="id"    column="id"    />
+        <result property="companySmsLogsId"    column="company_sms_logs_id"    />
+        <result property="courseId"    column="course_id"    />
+        <result property="videoId"    column="video_id"    />
+        <result property="number"    column="number"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="externalContactId"    column="external_contact_id"    />
+        <result property="type"    column="type"    />
+        <result property="phone"    column="phone"    />
+        <result property="content"    column="content"    />
+        <result property="result"    column="result"    />
+        <result property="remark"    column="remark"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectQwCourseLinkSendMsgLogVo">
+        select id,company_sms_logs_id,course_id,video_id, number, temp_id, external_contact_id, type, phone, content, result, remark, create_by, create_time from qw_course_link_send_msg_log
+    </sql>
+
+    <select id="selectQwCourseLinkSendMsgLogList" parameterType="com.fs.qw.domain.QwCourseLinkSendMsgLog" resultMap="QwCourseLinkSendMsgLogResult">
+        <include refid="selectQwCourseLinkSendMsgLogVo"/>
+        <where>
+            <if test="companySmsLogsId != null  and companySmsLogsId != ''"> and company_sms_logs_id = #{companySmsLogsId}</if>
+            <if test="courseId != null  and courseId != ''"> and course_id = #{courseId}</if>
+            <if test="videoId != null  and videoId != ''"> and video_id = #{videoId}</if>
+            <if test="number != null "> and number = #{number}</if>
+            <if test="tempId != null "> and temp_id = #{tempId}</if>
+            <if test="externalContactId != null "> and external_contact_id = #{externalContactId}</if>
+            <if test="type != null  and type != ''"> and type like concat('%', #{type}, '%')</if>
+            <if test="phone != null  and phone != ''"> and phone like concat('%', #{phone}, '%')</if>
+            <if test="content != null  and content != ''"> and content like concat('%', #{content}, '%')</if>
+            <if test="result != null  and result != ''"> and result like concat('%', #{result}, '%')</if>
+            <if test="remark != null  and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectQwCourseLinkSendMsgLogById" parameterType="Long" resultMap="QwCourseLinkSendMsgLogResult">
+        <include refid="selectQwCourseLinkSendMsgLogVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwCourseLinkSendMsgLog" parameterType="com.fs.qw.domain.QwCourseLinkSendMsgLog" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_course_link_send_msg_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companySmsLogsId != null and companySmsLogsId != ''">company_sms_logs_id,</if>
+            <if test="courseId != null and courseId != ''">course_id,</if>
+            <if test="videoId != null and videoId != ''">video_id,</if>
+            <if test="number != null">number,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="externalContactId != null">external_contact_id,</if>
+            <if test="type != null and type != ''">type,</if>
+            <if test="phone != null and phone != ''">phone,</if>
+            <if test="content != null and content != ''">content,</if>
+            <if test="result != null and result != ''">result,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            create_time,
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companySmsLogsId != null and companySmsLogsId != ''">#{companySmsLogsId},</if>
+            <if test="courseId != null and courseId != ''">#{courseId},</if>
+            <if test="videoId != null and videoId != ''">#{videoId},</if>
+            <if test="number != null">#{number},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="externalContactId != null">#{externalContactId},</if>
+            <if test="type != null and type != ''">#{type},</if>
+            <if test="phone != null and phone != ''">#{phone},</if>
+            <if test="content != null and content != ''">#{content},</if>
+            <if test="result != null and result != ''">#{result},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            sysdate(),
+        </trim>
+    </insert>
+
+    <update id="updateQwCourseLinkSendMsgLog" parameterType="com.fs.qw.domain.QwCourseLinkSendMsgLog">
+        update qw_course_link_send_msg_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companySmsLogsId != null and companySmsLogsId != ''">company_sms_logs_id = #{companySmsLogsId},</if>
+            <if test="courseId != null and courseId != ''">course_id = #{courseId},</if>
+            <if test="videoId != null and videoId != ''">video_id = #{videoId},</if>
+            <if test="number != null">number = #{number},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="externalContactId != null">external_contact_id = #{externalContactId},</if>
+            <if test="type != null and type != ''">type = #{type},</if>
+            <if test="phone != null and phone != ''">phone = #{phone},</if>
+            <if test="content != null and content != ''">content = #{content},</if>
+            <if test="result != null and result != ''">result = #{result},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate(),
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwCourseLinkSendMsgLogById" parameterType="Long">
+        delete from qw_course_link_send_msg_log where id = #{id}
+    </delete>
+
+    <delete id="deleteQwCourseLinkSendMsgLogByIds" parameterType="String">
+        delete from qw_course_link_send_msg_log where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 1 - 1
fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

@@ -663,7 +663,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where fs_user_id = #{userId} and company_user_id = #{companyUserId}
     </select>
     <select id="selectQwExternalContactListVONewSys" resultType="com.fs.qw.vo.QwExternalContactVO">
-            select ec.*, qu.qw_user_name, qd.dept_name as departmentName
+        select ec.*, qu.qw_user_id as qwUserId,qu.qw_user_name, qd.dept_name as departmentName,qd.dept_id as deptId
             from qw_external_contact ec
             left join qw_user qu on ec.user_id = qu.qw_user_id and qu.corp_id = ec.corp_id
             left join qw_dept qd on qd.dept_id = qu.department and qd.corp_id = qu.corp_id

+ 5 - 0
fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

@@ -71,6 +71,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
     </select>
 
+    <select id="selectNotDelQwUserList" parameterType="QwUser" resultMap="QwUserResult">
+    <include refid="selectQwUserVo"/>
+    where is_del = 0
+    </select>
+
     <select id="selectQwUserById" parameterType="Long" resultMap="QwUserResult">
         <include refid="selectQwUserVo"/>
         where id = #{id}

+ 12 - 0
fs-spec-zone/Dockerfile

@@ -0,0 +1,12 @@
+FROM alpine:3.18
+
+RUN apk add --no-cache openjdk8-jre openssl3
+
+WORKDIR /app
+COPY target/fs-spec-zone-1.0.0.jar app.jar
+COPY libWeWorkSpecSDK.so /usr/lib/
+
+ENV JAVA_OPTS="-Xms256m -Xmx512m"
+
+EXPOSE 8080
+ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.library.path=/usr/lib -jar app.jar"]

BIN
fs-spec-zone/libWeWorkSpecSDK.so


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä