Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/master'

lk 1 mēnesi atpakaļ
vecāks
revīzija
6bdfca2050
85 mainītis faili ar 4414 papildinājumiem un 341 dzēšanām
  1. 48 7
      fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java
  2. 8 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  3. 8 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java
  4. 15 0
      fs-admin/src/main/java/com/fs/his/controller/FsStorePaymentController.java
  5. 31 11
      fs-admin/src/main/java/com/fs/his/task/Task.java
  6. 0 4
      fs-admin/src/main/java/com/fs/qw/controller/QwExternalContactController.java
  7. 18 2
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  8. 11 0
      fs-admin/src/main/java/com/fs/qw/qwTask/qwTask.java
  9. 186 0
      fs-admin/src/main/java/com/fs/user/controller/FsUserIntegralController.java
  10. 1 0
      fs-common/src/main/java/com/fs/common/enums/BizResponseEnum.java
  11. 34 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisQwUserController.java
  12. 0 7
      fs-qwhook-sop/src/main/java/com/fs/app/controller/QwUserController.java
  13. 0 7
      fs-qwhook/src/main/java/com/fs/app/controller/ApisQwUserController.java
  14. 0 6
      fs-qwhook/src/main/java/com/fs/app/controller/QwUserController.java
  15. 69 0
      fs-service/src/main/java/com/fs/course/domain/FinishCourseStatistics.java
  16. 72 0
      fs-service/src/main/java/com/fs/course/mapper/FinishCourseStatisticsSyncMapper.java
  17. 15 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  18. 16 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java
  19. 17 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  20. 36 0
      fs-service/src/main/java/com/fs/course/param/CourseStatisticsQueryParam.java
  21. 14 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogStatisticsListParam.java
  22. 33 0
      fs-service/src/main/java/com/fs/course/service/IFinishCourseStatisticsSyncService.java
  23. 7 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  24. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java
  25. 8 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  26. 322 0
      fs-service/src/main/java/com/fs/course/service/impl/FinishCourseStatisticsSyncServiceImpl.java
  27. 214 210
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  28. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  29. 29 5
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  30. 59 0
      fs-service/src/main/java/com/fs/course/vo/FsCourseReportVO.java
  31. 26 0
      fs-service/src/main/java/com/fs/his/domain/HsyyValidationResult.java
  32. 80 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/AdministrationRoute.java
  33. 54 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/AntibioticLevel.java
  34. 58 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/CardType.java
  35. 57 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/DecoctionRequirement.java
  36. 127 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/DrugFormulation.java
  37. 55 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/EducationLevel.java
  38. 59 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/ExaminationType.java
  39. 55 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/Gender.java
  40. 56 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/IdCardType.java
  41. 57 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/LaboratoryType.java
  42. 57 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/MedicationRoute.java
  43. 58 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/PaymentType.java
  44. 64 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/PoliticalStatus.java
  45. 84 0
      fs-service/src/main/java/com/fs/his/enums/hsyy/ProfessionalTitle.java
  46. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreSubOrderMapper.java
  47. 5 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java
  48. 5 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  49. 432 0
      fs-service/src/main/java/com/fs/his/param/FsSubOrderHsyyParam.java
  50. 4 0
      fs-service/src/main/java/com/fs/his/service/IFsStoreSubOrderService.java
  51. 40 22
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  52. 421 4
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreSubOrderServiceImpl.java
  53. 46 0
      fs-service/src/main/java/com/fs/his/utils/HsyyPushApiClientUtil.java
  54. 45 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductPurchaseLimitScrm.java
  55. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  56. 72 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductPurchaseLimitScrmMapper.java
  57. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  58. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java
  59. 91 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductPurchaseLimitScrmService.java
  60. 95 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java
  61. 72 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  62. 163 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductPurchaseLimitScrmServiceImpl.java
  63. 6 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  64. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductQueryVO.java
  65. 105 9
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  66. 4 4
      fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java
  67. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  68. 2 1
      fs-service/src/main/java/com/fs/qw/param/QwMaterialParam.java
  69. 2 0
      fs-service/src/main/java/com/fs/qw/param/sidebar/TagGroupListParam.java
  70. 49 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  71. 1 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwTagGroupServiceImpl.java
  72. 2 2
      fs-service/src/main/resources/application-config-druid-bjzm-test.yml
  73. 1 1
      fs-service/src/main/resources/application-druid-yxj.yml
  74. 35 7
      fs-service/src/main/resources/mapper/company/CompanyMoneyLogsMapper.xml
  75. 198 0
      fs-service/src/main/resources/mapper/course/FinishCourseStatisticsSyncMapper.xml
  76. 72 1
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  77. 8 0
      fs-service/src/main/resources/mapper/course/FsUserCourseMapper.xml
  78. 8 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  79. 9 0
      fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml
  80. 15 0
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  81. 81 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductPurchaseLimitScrmMapper.xml
  82. 19 15
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  83. 4 1
      fs-service/src/main/resources/mapper/qw/QwTagGroupMapper.xml
  84. 28 9
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveGoodsController.java
  85. 32 1
      fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

+ 48 - 7
fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java

@@ -1,9 +1,12 @@
 package com.fs.company.controller;
 
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.Excel;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.TimeUtils;
 import com.fs.common.utils.poi.ExcelUtil;
@@ -14,6 +17,10 @@ import com.fs.company.service.ICompanySmsLogsService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyVoiceLogsService;
 import com.fs.company.vo.*;
+import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.service.IFinishCourseStatisticsSyncService;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.vo.FsCourseReportVO;
 import com.fs.crm.param.CrmCustomerStatisticsParam;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerVisitService;
@@ -29,15 +36,11 @@ import com.fs.his.vo.FsStoreOrderAmountStatsVo;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
+import java.lang.reflect.Field;
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -76,6 +79,12 @@ public class CompanyStatisticsController extends BaseController
     //app商城订单接口Service
     @Autowired
     private IFsStoreOrderScrmService fsStoreOrderScrmService;
+
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+
+    @Autowired
+    private IFinishCourseStatisticsSyncService finishCourseStatisticsSyncService;
     @GetMapping("/storeOrder")
     public R storeOrder(FsStoreStatisticsParam param)
     {
@@ -751,4 +760,36 @@ public class CompanyStatisticsController extends BaseController
         return AjaxResult.success(scrmStatsVo);
     }
 
+    /**
+     * 木易华康特殊处理 课程完课统计数据
+     */
+    @GetMapping("/courseReport")
+    public TableDataInfo selectFsCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
+        startPage();
+        List<FsCourseReportVO> fsCourseReportVOS = fsCourseWatchLogService.selectFsCourseReportVO(param);
+        return getDataTable(fsCourseReportVOS);
+    }
+
+    @GetMapping("/exportFsCourseReportVO")
+    public AjaxResult exportFsCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
+        List<FsCourseReportVO> list = fsCourseWatchLogService.selectFsCourseReportVO(param);
+        List<String> allFields = Arrays.stream(FsCourseReportVO.class.getDeclaredFields())
+                .filter(field -> field.isAnnotationPresent(Excel.class))
+                .map(Field::getName)
+                .collect(Collectors.toList());
+        ExcelUtil<FsCourseReportVO> util = new ExcelUtil<FsCourseReportVO>(FsCourseReportVO.class);
+        return util.exportExcelSelectedColumns(list, "完课统计报表", allFields);
+    }
+
+    @PostMapping("/syncYesterday")
+    public AjaxResult syncYesterday() {
+        try {
+            Date startDate = DateUtils.parseDate("2026-01-07");
+            Date endDate = DateUtils.parseDate("2026-01-07");
+            finishCourseStatisticsSyncService.syncDailyStatistics(startDate, endDate);
+            return AjaxResult.success("同步指定范围数据成功");
+        } catch (Exception e) {
+            return AjaxResult.error("同步失败:" + e.getMessage());
+        }
+    }
 }

+ 8 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -346,4 +346,12 @@ public class FsUserCourseController extends BaseController {
         sopTempService.syncTemplate(courseId);
         return toAjax(1);
     }
+
+    /**
+     * 课程下拉列表
+     */
+    @GetMapping("/selectCourseOptionsList")
+    public R getCourseList() {
+        return R.ok().put("data",fsUserCourseService.selectCourseOptionsList());
+    }
 }

+ 8 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java

@@ -280,4 +280,12 @@ public class FsUserCourseVideoController extends BaseController
     public AjaxResult batchEditCover(@Validated @RequestBody BatchEditCoverParam param) {
         return toAjax(fsUserCourseVideoService.batchEditCover(param));
     }
+
+    /**
+     * 获取课程视频选项列表
+     */
+    @GetMapping("/getCourseVideoOptions")
+    public R getCourseVideoOptions(Long courseId) {
+        return R.ok().put("data", fsUserCourseVideoService.selectVideoOptionsByCourseId(courseId));
+    }
 }

+ 15 - 0
fs-admin/src/main/java/com/fs/his/controller/FsStorePaymentController.java

@@ -133,6 +133,21 @@ public class FsStorePaymentController extends BaseController
 
         return AjaxResult.success( fsStorePaymentService.updateFsStorePaymentByDecryptForm(paymentId));
     }
+
+    /**
+     * 批量互医同步支付明细(临时)
+     */
+    @PostMapping(value = "/batchUpdate")
+    public AjaxResult batchUpdate(@RequestBody List<Long> paymentIds)
+    {
+        if(!paymentIds.isEmpty()){
+            for (Long paymentId : paymentIds) {
+                fsStorePaymentService.updateFsStorePaymentByDecryptForm(paymentId);
+            }
+        }
+        return AjaxResult.success("成功");
+    }
+
     @PreAuthorize("@ss.hasPermi('his:storePayment:refund')")
     @GetMapping(value = "refund/{paymentId}")
     public R refund(@PathVariable("paymentId") Long paymentId)

+ 31 - 11
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -1,12 +1,11 @@
 package com.fs.his.task;
 
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
-import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.service.impl.SmsServiceImpl;
@@ -23,16 +22,11 @@ import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.course.dto.BatchSendCourseAllDTO;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
 import com.fs.course.service.IFsCourseWatchLogService;
-import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.ITencentCloudCosService;
 import com.fs.erp.domain.ErpDeliverys;
-import com.fs.erp.domain.ErpOrder;
 import com.fs.erp.domain.ErpOrderQuery;
-import com.fs.erp.domain.FsErpFinishPush;
 import com.fs.erp.dto.ErpOrderQueryRequert;
 import com.fs.erp.dto.ErpOrderQueryResponse;
-import com.fs.erp.dto.ErpOrderResponse;
-import com.fs.erp.mapper.FsErpFinishPushMapper;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.fastGpt.domain.*;
 import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
@@ -41,7 +35,6 @@ import com.fs.fastGpt.service.AiHookService;
 import com.fs.fastGpt.service.IFastgptEventLogTotalService;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
-import com.fs.gtPush.mapper.PushLogMapper;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.config.StoreConfig;
 import com.fs.his.domain.*;
@@ -52,13 +45,11 @@ import com.fs.his.mapper.*;
 import com.fs.his.param.FsInquiryOrderFinishParam;
 import com.fs.his.param.FsPackageOrderCancelParam;
 import com.fs.his.service.*;
-import com.fs.his.service.impl.FsPackageOrderServiceImpl;
 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.FsStorePaymentScrmMapper;
-import com.fs.hisStore.service.IFsStorePaymentScrmService;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
 import com.fs.huifuPay.service.HuiFuService;
@@ -90,8 +81,8 @@ import org.springframework.stereotype.Component;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
@@ -907,6 +898,35 @@ public class Task {
         }
     }
 
+    /**
+     * 推送河山医院
+     */
+    public void puSubStoreOrderHsyy() {
+        // 获取时间并格式化为字符串
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+        String todayStartStr = LocalDateTime.now()
+                .withHour(0)
+                .withMinute(0)
+                .withSecond(0)
+                .withNano(0)
+                .format(formatter);
+
+        String yesterdayStartStr = LocalDateTime.now()
+                .minusDays(1)
+                .withHour(0)
+                .withMinute(0)
+                .withSecond(0)
+                .withNano(0)
+                .format(formatter);
+
+        List<FsStoreSubOrder> list = fsStoreSubOrderService.selectFsStoreSubOrderListByCreateTime(yesterdayStartStr, todayStartStr);
+        log.info("------>>>>>>"+todayStartStr+"推送" + yesterdayStartStr+"的子订单数据共:" + list.size() + "条");
+        if(CollectionUtil.isNotEmpty(list)){
+            fsStoreSubOrderService.pushHsyy(list);
+        }
+    }
+
     public void refundOp() {
         List<FsStoreAfterSales> list = fsStoreAfterSalesService.selectFsStoreAfterSalesByDoAudit();
         if (list != null) {

+ 0 - 4
fs-admin/src/main/java/com/fs/qw/controller/QwExternalContactController.java

@@ -93,10 +93,6 @@ public class QwExternalContactController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(QwExternalContactParam qwExternalContact)
     {
-        if (qwExternalContact.getCompanyId() == null) {
-            return AjaxResult.success();
-        }
-
         List<QwExternalContactVO> list = qwExternalContactService.selectQwExternalContactListVO(qwExternalContact);
         list.forEach(item->{
 

+ 18 - 2
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -126,8 +126,8 @@ public class QwUserController extends BaseController {
      * 查询企微员工列表
      */
     @PreAuthorize("@ss.hasPermi('qw:user:staffListPost')")
-    @PostMapping("/staffListPost")
-    public TableDataInfo staffListPost(@RequestBody QwUserListParam qwUser) {
+    @GetMapping("/staffListPost")
+    public TableDataInfo staffListPost(QwUserListParam qwUser) {
         // 添加企微部门查询条件
         Long deptId = qwUser.getDeptId();
         if(deptId!=null && qwUser.getCorpId()!=null){
@@ -588,6 +588,22 @@ public class QwUserController extends BaseController {
         List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOByCompanyId(companyId);
         return  R.ok().put("data",list);
     }
+
+    @GetMapping("/getMyQwCompanyListAll")
+    public R getMyQwCompanyListAll()
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOAll();
+        return  R.ok().put("data",list);
+    }
+
+
+    @GetMapping("/getQwCompanyList")
+    public R getQwCompanyList()
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOAll();
+        return  R.ok().put("data",list);
+    }
+
     /**
      * 获取企微用户详细信息
      */

+ 11 - 0
fs-admin/src/main/java/com/fs/qw/qwTask/qwTask.java

@@ -1,5 +1,6 @@
 package com.fs.qw.qwTask;
 
+import com.fs.course.service.IFinishCourseStatisticsSyncService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.qw.domain.QwIpadServerLog;
 import com.fs.qw.domain.QwUser;
@@ -81,6 +82,9 @@ public class qwTask {
     @Autowired
     private IQwCompanyService iQwCompanyService;
 
+    @Autowired
+    private IFinishCourseStatisticsSyncService finishCourseStatisticsSyncService;
+
 
     //正在使用
     public void qwExternalContact()
@@ -369,4 +373,11 @@ public class qwTask {
 
         }
     }
+
+    /**
+     * 特殊处理(木易完课统计数据)
+     */
+    private  void  syncMultiDimensionStatistics(){
+        finishCourseStatisticsSyncService.syncMultiDimensionStatistics();
+    }
 }

+ 186 - 0
fs-admin/src/main/java/com/fs/user/controller/FsUserIntegralController.java

@@ -0,0 +1,186 @@
+package com.fs.user.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.his.service.IFsUserService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 添加积分参数
+ */
+class AddIntegralParam {
+    private Long userId;
+    private Integer logType;
+    private Long integral;
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public Integer getLogType() {
+        return logType;
+    }
+
+    public void setLogType(Integer logType) {
+        this.logType = logType;
+    }
+
+    public Long getIntegral() {
+        return integral;
+    }
+
+    public void setIntegral(Long integral) {
+        this.integral = integral;
+    }
+}
+
+/**
+ * 用户积分管理Controller 提供给卓美 完成直播积分发放
+ *
+ * @author fs
+ * @date 2025-01-01
+ */
+@Api("用户积分管理")
+@RestController
+@RequestMapping("/user/integral")
+public class FsUserIntegralController {
+
+    @Autowired
+    private IFsUserService userService;
+
+    @Autowired
+    private IFsUserIntegralLogsService integralLogsService;
+
+    @Autowired
+    private FsUserMapper userMapper;
+
+    @Autowired
+    private FsUserIntegralLogsMapper integralLogsMapper;
+
+    /**
+     * 查询用户列表(支持手机号和昵称筛选)
+     */
+    @ApiOperation("查询用户列表")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String phone,
+                              @RequestParam(required = false) String nickName,
+                              @RequestParam(defaultValue = "1") Integer pageNum,
+                              @RequestParam(defaultValue = "10") Integer pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        
+        // 构建查询条件
+        FsUser queryUser = new FsUser();
+        if (StringUtils.isNotEmpty(phone)) {
+            queryUser.setPhone(phone);
+        }
+        if (StringUtils.isNotEmpty(nickName)) {
+            queryUser.setNickName(nickName);
+            queryUser.setNickname(nickName);
+        }
+        
+        // 查询用户列表(只返回需要的字段)
+        List<FsUser> userList = userMapper.selectFsUserListForIntegral(queryUser);
+        
+        PageInfo<FsUser> pageInfo = new PageInfo<>(userList);
+        TableDataInfo dataTable = new TableDataInfo();
+        dataTable.setCode(200);
+        dataTable.setMsg("查询成功");
+        dataTable.setRows(pageInfo.getList());
+        dataTable.setTotal(pageInfo.getTotal());
+        return dataTable;
+    }
+
+    /**
+     * 查询用户积分明细
+     */
+    @ApiOperation("查询用户积分明细")
+    @GetMapping("/logs/{userId}")
+    public R getIntegralLogs(@PathVariable Long userId,
+                             @RequestParam(defaultValue = "1") Integer pageNum,
+                             @RequestParam(defaultValue = "10") Integer pageSize,
+                             @RequestParam(required = false) Integer logType) {
+        PageHelper.startPage(pageNum, pageSize);
+        FsUserIntegralLogs queryLogs = new FsUserIntegralLogs();
+        queryLogs.setUserId(userId);
+        if (logType != null) {
+            queryLogs.setLogType(logType);
+        }
+        // 按创建时间倒序查询(Mapper XML 中已添加排序)
+        List<FsUserIntegralLogs> logsList = integralLogsService.selectFsUserIntegralLogsList(queryLogs);
+        PageInfo<FsUserIntegralLogs> pageInfo = new PageInfo<>(logsList);
+        return R.ok().put("data", pageInfo.getList()).put("total", pageInfo.getTotal());
+    }
+
+    /**
+     * 添加积分
+     */
+    @ApiOperation("添加积分")
+    @PostMapping("/add")
+    public R addIntegral(@RequestBody AddIntegralParam param) {
+        Long userId = param.getUserId();
+        Integer logType = param.getLogType();
+        Long integral = param.getIntegral();
+        // 查询用户信息
+        FsUser user = userService.selectFsUserByUserId(userId);
+        if (user == null) {
+            return R.error("用户不存在");
+        }
+
+        // 查询用户最新的积分记录
+        FsUserIntegralLogs latestLog = integralLogsMapper.selectLatestIntegralLogByUserId(userId);
+        
+        // 计算新的积分余额
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        Long newIntegral = currentIntegral + integral;
+        
+        // 创建时间默认往后移动2个小时,使用整点的创建时间和更新时间
+        Date now = new Date();
+        LocalDateTime localDateTime = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault());
+        // 往后移动2小时
+        localDateTime = localDateTime.plusHours(2);
+        // 设置为整点(分钟和秒都设为0)
+        localDateTime = localDateTime.withMinute(0).withSecond(0).withNano(0);
+        Date createTime = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        Date updateTime = createTime;
+
+        // 更新用户积分
+        user.setIntegral(newIntegral);
+
+        userService.increaseIntegral(Collections.singletonList(user.getUserId()),integral);
+
+        // 创建积分记录
+        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+        integralLogs.setUserId(userId);
+        integralLogs.setLogType(logType);
+        integralLogs.setIntegral(integral);
+        integralLogs.setBalance(newIntegral);
+        integralLogs.setCreateTime(createTime);
+        integralLogs.setUpdateTime(updateTime);
+        integralLogsService.insertFsUserIntegralLogs(integralLogs);
+
+        return R.ok("添加积分成功");
+    }
+}

+ 1 - 0
fs-common/src/main/java/com/fs/common/enums/BizResponseEnum.java

@@ -9,6 +9,7 @@ public enum BizResponseEnum {
     FAIL(500, "操作失败"),
     PARAM_ERROR(400, "参数错误"),
     DATA_NOT_EXIST(1002, "数据不存在"),
+    WATCH_LATEST_COURSE(482, "请观看最新的课程项目"),
     WAIT_APPROVAL(505, "等待审核");
 
     private final Integer code;

+ 34 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisQwUserController.java

@@ -1,6 +1,7 @@
 package com.fs.app.controller;
 
 import com.fs.app.params.LoginBindCompanyParam;
+import com.fs.common.BeanCopyUtils;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
@@ -17,13 +18,18 @@ import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.course.param.FsCourseListBySidebarParam;
 import com.fs.qw.domain.QwExternalContactInfo;
+import com.fs.qw.domain.QwTagGroup;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.param.ExternalContactDetailsParam;
 import com.fs.qw.param.QwExtCourseSopWatchLog;
+import com.fs.qw.param.sidebar.TagGroupListParam;
+import com.fs.qw.param.sidebar.TagGroupUpdateParam;
 import com.fs.qw.service.IQwExternalContactInfoService;
 import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.IQwTagGroupService;
 import com.fs.qw.vo.ExternalContactDetailsVO;
 import com.fs.qw.vo.QwExternalListByHeavyVO;
+import com.fs.qw.vo.QwTagGroupListVO;
 import com.fs.sop.service.ISopUserLogsInfoService;
 import com.fs.sop.vo.ExtCourseSopWatchLogVO;
 import com.github.pagehelper.PageHelper;
@@ -67,6 +73,9 @@ public class ApisQwUserController extends BaseController {
     @Autowired
     ICompanyPostService postService;
 
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+
 
     @GetMapping("/details")
     @ApiOperation("会员看课详情")
@@ -188,4 +197,29 @@ public class ApisQwUserController extends BaseController {
         }
     }
 
+
+    @GetMapping("/tagGroupList")
+    @ApiOperation("获取所有标签组和其下标签")
+    public R getTagGroupList(TagGroupListParam param) {
+        QwTagGroup qwTagGroup = new QwTagGroup();
+        BeanCopyUtils.copy(param, qwTagGroup);
+        qwTagGroup.setName(param.getTagName());
+
+        PageHelper.startPage(qwTagGroup.getPageNum(), qwTagGroup.getPageSize());
+        List<QwTagGroupListVO> list = qwTagGroupService.selectQwGroupTagList(qwTagGroup);
+
+        PageInfo<QwTagGroupListVO> result = new PageInfo<>(list);
+        return R.ok().put("data", result);
+    }
+
+    @PutMapping("/externalContact/tag")
+    @ApiOperation("编辑标签")
+    public R updateExternalContactTag(@RequestBody TagGroupUpdateParam param) {
+        int i = qwExternalContactService.updateExternalContactTag(param);
+        if (i > 0) {
+            return R.ok();
+        }
+        return R.error();
+    }
+
 }

+ 0 - 7
fs-qwhook-sop/src/main/java/com/fs/app/controller/QwUserController.java

@@ -167,13 +167,6 @@ public class QwUserController extends BaseController {
         return R.ok().put("data", result);
     }
 
-//    @GetMapping("/searchTags")
-//    @ApiOperation("搜索标签-跟管理端保持一致")
-//    public R searchTagList(QwTagParam param) {
-//        List<QwTagGroupListVO> list = qwTagService.searchTags(param);
-//        return R.ok().put("data", list);
-//    }
-
     @PutMapping("/externalContact/tag")
     @ApiOperation("编辑标签")
     public R updateExternalContactTag(@RequestBody TagGroupUpdateParam param) {

+ 0 - 7
fs-qwhook/src/main/java/com/fs/app/controller/ApisQwUserController.java

@@ -193,13 +193,6 @@ public class ApisQwUserController extends BaseController {
         return R.ok().put("data", result);
     }
 
-//    @GetMapping("/searchTags")
-//    @ApiOperation("搜索标签-跟管理端保持一致")
-//    public R searchTagList(QwTagParam param) {
-//        List<QwTagGroupListVO> list = qwTagService.searchTags(param);
-//        return R.ok().put("data", list);
-//    }
-
     @PutMapping("/externalContact/tag")
     @ApiOperation("编辑标签")
     public R updateExternalContactTag(@RequestBody TagGroupUpdateParam param) {

+ 0 - 6
fs-qwhook/src/main/java/com/fs/app/controller/QwUserController.java

@@ -169,12 +169,6 @@ public class QwUserController extends BaseController {
         return R.ok().put("data", result);
     }
 
-//    @GetMapping("/searchTags")
-//    @ApiOperation("搜索标签-跟管理端保持一致")
-//    public R searchTagList(QwTagParam param) {
-//        List<QwTagGroupListVO> list = qwTagService.searchTags(param);
-//        return R.ok().put("data", list);
-//    }
 
     @PutMapping("/externalContact/tag")
     @ApiOperation("编辑标签")

+ 69 - 0
fs-service/src/main/java/com/fs/course/domain/FinishCourseStatistics.java

@@ -0,0 +1,69 @@
+package com.fs.course.domain;
+
+import com.baidu.dev2.thirdparty.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class FinishCourseStatistics {
+
+    /** 主键ID */
+    private Long id;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 课程ID */
+    @Excel(name = "课程ID")
+    private Long courseId;
+
+    /** 小节ID */
+    @Excel(name = "小节ID")
+    private Long videoId;
+
+    /** 维度类型 */
+    @Excel(name = "维度类型")
+    private String dimensionType;
+
+    /** 统计日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "统计日期", dateFormat = "yyyy-MM-dd")
+    private Date statDate;
+
+    /** 完成人数 */
+    @Excel(name = "完成人数")
+    private Integer finishedCount;
+
+    /** 完播数 */
+    @Excel(name = "完播数")
+    private Integer courseCompleteTimes;
+
+    /** 访问人数 */
+    @Excel(name = "访问人数")
+    private Integer accessCount;
+
+    /** 完成率 */
+    @Excel(name = "完成率")
+    private BigDecimal finishRate;
+
+    /** 同步类型 */
+    @Excel(name = "同步类型")
+    private String syncType;
+
+    /** 同步时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "同步时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date syncTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 72 - 0
fs-service/src/main/java/com/fs/course/mapper/FinishCourseStatisticsSyncMapper.java

@@ -0,0 +1,72 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FinishCourseStatistics;
+import com.fs.course.param.CourseStatisticsQueryParam;
+import com.fs.course.vo.FsCourseReportVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public interface FinishCourseStatisticsSyncMapper extends BaseMapper<FinishCourseStatistics> {
+
+    /**
+     * 按公司维度查询统计
+     */
+    List<Map<String, Object>> selectCompanyStatistics(@Param("startTime") Date startTime,
+                                                      @Param("endTime") Date endTime);
+
+    /**
+     * 按课程维度查询统计
+     */
+    List<Map<String, Object>> selectCourseStatistics(@Param("startTime") Date startTime,
+                                                     @Param("endTime") Date endTime);
+
+    /**
+     * 按小节维度查询统计
+     */
+    List<Map<String, Object>> selectVideoStatistics(@Param("startTime") Date startTime,
+                                                    @Param("endTime") Date endTime);
+
+    /**
+     * 分维度查询统计结果
+     * @param courseStatisticsQueryParam
+     * @return
+     */
+    List<FsCourseReportVO> querySimpleStatistics(CourseStatisticsQueryParam courseStatisticsQueryParam);
+    /**
+     * 批量插入统计结果
+     */
+    int batchInsertStatistics(List<FinishCourseStatistics> list);
+
+    /**
+     * 删除指定维度的同步数据
+     */
+    int deleteByDimension(@Param("statDate") Date statDate,
+                          @Param("dimensionType") String dimensionType);
+
+    /**
+     * 检查指定维度数据是否已同步
+     */
+    int checkDimensionExists(@Param("statDate") Date statDate,
+                             @Param("dimensionType") String dimensionType);
+
+    /**
+     * 清理旧数据(保留最近N天)
+     */
+    int cleanOldData(@Param("keepDays") int keepDays);
+
+    /**
+     * 获取数据日期范围
+     */
+    Map<String, Date> selectDateRange();
+
+    /**
+     * 查询未同步的日期列表
+     */
+    List<Date> selectUnsyncedDates(@Param("startDate") Date startDate,
+                                   @Param("endDate") Date endDate);
+
+}

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

@@ -719,4 +719,19 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             "</script>"
     })
     List<Long> getExContactIdsIdsByWatchLogIds(@Param("watchLogIds")List<Long> watchLogIds);
+
+    /**
+     *  公司基本信息
+     * @param param
+     * @return
+     */
+    List<FsCourseReportVO> selectCompanyBaseInfo(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * 看课信息
+     * @param param
+     * @return
+     */
+    List<FsCourseReportVO> selectWatchStatistics(FsCourseWatchLogStatisticsListParam param);
+
 }

+ 16 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java

@@ -2,6 +2,7 @@ package com.fs.course.mapper;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
@@ -142,6 +143,9 @@ public interface FsUserCourseMapper
             "<if test = ' maps.subCateId !=null '> " +
             "and c.sub_cate_id =#{maps.subCateId}" +
             "</if>" +
+            "<if test = ' maps.courseId !=null '> " +
+            "and c.course_id =#{maps.courseId}" +
+            "</if>" +
             "<if test = ' maps.userId !=null '> " +
             "and c.user_id =#{maps.userId}" +
             "</if>" +
@@ -335,4 +339,16 @@ public interface FsUserCourseMapper
      */
     @Update("update fs_user_course set config_json = #{configJson} where course_id = #{id}")
     void editConfig(@Param("id") Long id, @Param("configJson") String configJson);
+    /**
+     * 根据课程id查询课程名称
+     */
+    List<Map<String, Object>> selectCourseNamesByIds(@Param("courseIds") Set<Long> courseIds);
+
+    @Select(" SELECT course_id as dictValue, \n" +
+            "           course_name as dictLabel \n" +
+            "    FROM fs_user_course \n" +
+            "    WHERE is_del = 0 \n" +
+            "    GROUP BY course_id, course_name")
+    List<OptionsVO> selectCourseOptionsList();
+
 }

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

@@ -17,6 +17,7 @@ import org.apache.ibatis.annotations.Update;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * 课堂视频Mapper接口
@@ -92,6 +93,9 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
             "<if test = ' maps.userId!=null and maps.userId != \"\" '> " +
             "and v.user_id = #{maps.userId} " +
             "</if>" +
+            "<if test = ' maps.videoId!=null and maps.videoId != \"\" '> " +
+            "and v.video_id = #{maps.videoId} " +
+            "</if>" +
             " order by v.course_sort  " +
             "</script>"})
     List<FsUserCourseVideo> selectFsUserCourseVideoListByCourseId(@Param("maps") FsUserCourseVideo fsUserCourseVideo);
@@ -289,4 +293,17 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
      * 批量修改视频封面
      */
     int batchEditCover(BatchEditCoverParam param);
+
+    /**
+     * 批量修改视频名称
+     */
+    List<Map<String, Object>> selectVideoNamesByIds(@Param("videoIds") Set<Long> videoIds);
+
+    @Select("SELECT video_id as dictValue, \n" +
+            "           title as dictLabel \n" +
+            "    FROM fs_user_course_video \n" +
+            "    WHERE is_del = 0 \n" +
+            "      AND course_id = #{courseId}")
+    List<OptionsVO> selectVideoOptionsByCourseId(@Param("courseId") Long courseId);
+
 }

+ 36 - 0
fs-service/src/main/java/com/fs/course/param/CourseStatisticsQueryParam.java

@@ -0,0 +1,36 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 特殊处理(木易完课统计查询参数)
+ */
+@Data
+public class CourseStatisticsQueryParam {
+
+    /**
+     * 维度类型
+     * 可选值: company(公司), course(课程), video(视频)
+     */
+    private String dimensionType;
+
+    /**
+     * 开始日期
+     */
+    private String startDate;
+
+    /**
+     * 结束日期
+     */
+    private String endDate;
+
+    private  Long companyId;
+
+    private  Long courseId;
+
+    private  Long videoId;
+
+
+}

+ 14 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogStatisticsListParam.java

@@ -36,10 +36,24 @@ public class FsCourseWatchLogStatisticsListParam {
         return DateUtils.getEndOfDayString(eTime);
     }
 
+    private String startTime;
+
+    private String endTime;
+
     private Long project;
 
     private Long pageNum;
     private Long pageSize;
 
     private Integer sendType; //归属发送方式:1 个微  2 企微
+
+    /**
+     * 公司ids
+     */
+    private  List<Long> companyIds;
+
+    /**
+     * 标识('course','company')
+     */
+    private  String dimension ;
 }

+ 33 - 0
fs-service/src/main/java/com/fs/course/service/IFinishCourseStatisticsSyncService.java

@@ -0,0 +1,33 @@
+package com.fs.course.service;
+
+import java.util.Date;
+
+public interface IFinishCourseStatisticsSyncService {
+
+    /**
+     * 每日定时同步(同步前一天的数据)
+     */
+    void syncMultiDimensionStatistics();
+
+    /**
+     * 按指定日期范围同步多维度统计数据
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     */
+    void syncDailyStatistics(Date startDate, Date endDate);
+
+    /**
+     * 同步历史数据
+     */
+    void syncHistoryStatistics();
+
+    /**
+     * 增量同步(同步未同步的日期)
+     */
+    void syncIncrementalStatistics();
+
+    /**
+     * 重新同步指定日期范围
+     */
+    void resyncStatistics(Date startDate, Date endDate);
+}

+ 7 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -154,4 +154,11 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
      * @return
      */
     List<Long> getExContactIdsIdsByWatchLogIds(List<Long> watchLogIds);
+
+    /**
+     * 看课统计报表
+     * @param param
+     * @return
+     */
+    List<FsCourseReportVO> selectFsCourseReportVO(FsCourseWatchLogStatisticsListParam param);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java

@@ -137,4 +137,9 @@ public interface IFsUserCourseService {
      * 修改课堂配置
      */
     void editConfig(Long id, String configJson);
+
+    /**
+     * 获取课程选项列表
+     */
+    List<OptionsVO> selectCourseOptionsList();
 }

+ 8 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -254,4 +254,12 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      * 批量修改视频封面
      */
     int batchEditCover(BatchEditCoverParam param);
+
+    /**
+     * 获取课程视频选项列表
+     *
+     * @param courseId 课程ID
+     * @return list
+     */
+    List<OptionsVO> selectVideoOptionsByCourseId(Long courseId);
 }

+ 322 - 0
fs-service/src/main/java/com/fs/course/service/impl/FinishCourseStatisticsSyncServiceImpl.java

@@ -0,0 +1,322 @@
+package com.fs.course.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.FinishCourseStatistics;
+import com.fs.course.mapper.FinishCourseStatisticsSyncMapper;
+import com.fs.course.service.IFinishCourseStatisticsSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class FinishCourseStatisticsSyncServiceImpl  implements IFinishCourseStatisticsSyncService {
+
+    private final FinishCourseStatisticsSyncMapper syncMapper;
+
+    public FinishCourseStatisticsSyncServiceImpl(FinishCourseStatisticsSyncMapper syncMapper) {
+        this.syncMapper = syncMapper;
+    }
+
+
+    // 维度定义
+    private static final List<String> DIMENSIONS = Arrays.asList(
+            "company", "course", "video"
+    );
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncMultiDimensionStatistics() {
+        try {
+            // 获取昨天的日期
+            Date yesterday = DateUtils.addDays(new Date(), -1);
+            String dateStr = DateUtils.parseDateToStr("yyyy-MM-dd", yesterday);
+
+            log.info("开始同步{}的统计数据", dateStr);
+
+            // 按维度分别检查和处理
+            for (String dimensionType : DIMENSIONS) {
+                try {
+                    // 检查该维度是否已经同步
+                    int exists = syncMapper.checkDimensionExists(yesterday, dimensionType);
+                    if (exists > 0) {
+                        log.info("维度 {} 的 {} 数据已同步,跳过",
+                                dimensionType, dateStr);
+                        continue; // 跳过该维度,继续处理其他维度
+                    }
+
+                    // 同步该维度数据
+                    log.info("开始同步维度 {} 的数据", dimensionType);
+                    long dimStartTime = System.currentTimeMillis();
+
+                    // 获取时间范围
+                    Date startTime = getStartOfDay(yesterday);
+
+                    Date endTime = getEndOfDay(yesterday);
+
+                    // 同步单个维度
+                    syncSingleDimension(dimensionType, yesterday, startTime, endTime);
+
+                    long dimEndTime = System.currentTimeMillis();
+                    log.info("维度 {} 同步完成,耗时:{}ms",
+                            dimensionType, dimEndTime - dimStartTime);
+
+                } catch (Exception e) {
+                    log.error("维度 {} 同步失败:{}", dimensionType, e.getMessage(), e);
+                    // 继续处理其他维度
+                }
+            }
+
+            // 清理旧数据(保留90天)
+            syncMapper.cleanOldData(90);
+
+            log.info("每日同步完成");
+
+        } catch (Exception e) {
+            log.error("每日同步任务执行失败:{}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 同步单个维度数据
+     */
+    private void syncSingleDimension(String dimensionType, Date statDate,
+                                     Date startTime, Date endTime) {
+        // 1. 先删除已存在的该维度数据
+        int deleted = syncMapper.deleteByDimension(statDate, dimensionType);
+        if (deleted > 0) {
+            log.debug("删除维度 {} 的{}条旧数据", dimensionType, deleted);
+        }
+
+        // 2. 查询该维度的统计数据
+        List<Map<String, Object>> statsData = queryDimensionStatistics(
+                dimensionType, startTime, endTime);
+
+        if (statsData.isEmpty()) {
+            log.debug("维度 {} 没有数据", dimensionType);
+            return;
+        }
+
+        // 3. 为每条数据添加日期和同步信息
+        // 3. 转换为实体
+        List<FinishCourseStatistics> statisticsList = convertToStatistics(
+                statsData, statDate, dimensionType, "DAILY");
+
+        // 4. 批量插入
+        int inserted = syncMapper.batchInsertStatistics(statisticsList);
+        log.debug("维度 {} 插入{}条数据", dimensionType, inserted);
+    }
+
+    /**
+     * 查询指定维度的统计数据
+     */
+    private List<Map<String, Object>> queryDimensionStatistics(String dimensionType,Date startTime, Date endTime) {
+        switch (dimensionType) {
+            case "company":
+                return syncMapper.selectCompanyStatistics(startTime,endTime);
+            case "course":
+                return syncMapper.selectCourseStatistics(startTime,endTime);
+            case "video":
+                return syncMapper.selectVideoStatistics(startTime,endTime);
+            default:
+                throw new IllegalArgumentException("不支持的维度类型:" + dimensionType);
+        }
+    }
+
+    /**
+     * 转换统计数据为实体
+     */
+    private List<FinishCourseStatistics> convertToStatistics(List<Map<String, Object>> statisticsData,
+                                                             Date statDate, String dimensionType, String syncType) {
+        return statisticsData.stream()
+                .map(map -> {
+                    FinishCourseStatistics stats = new FinishCourseStatistics();
+
+                    // 设置维度ID
+                    stats.setCompanyId(getLongValue(map.get("company_id")));
+                    stats.setCourseId(getLongValue(map.get("course_id")));
+                    stats.setVideoId(getLongValue(map.get("video_id")));
+                    stats.setDimensionType(dimensionType);
+                    stats.setStatDate(statDate);
+
+                    // 设置统计指标
+                    stats.setFinishedCount(getIntValue(map.get("finished_count")));
+                    stats.setCourseCompleteTimes(getIntValue(map.get("course_complete_times")));
+                    stats.setAccessCount(getIntValue(map.get("access_count")));
+                    stats.setFinishRate(getBigDecimalValue(map.get("finish_rate")));
+
+                    // 设置同步信息
+                    stats.setSyncType(syncType);
+                    stats.setSyncTime(new Date());
+                    stats.setCreateTime(new Date());
+                    stats.setUpdateTime(new Date());
+
+                    return stats;
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncDailyStatistics(Date startDate, Date endDate) {
+        try {
+            String startDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", startDate);
+            String endDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", endDate);
+            log.info("开始同步{}到{}的统计数据", startDateStr, endDateStr);
+
+            // 计算日期范围内所有日期
+            List<Date> dateRange = getDateRange(startDate, endDate);
+
+            // 按维度分别检查和处理
+            for (String dimensionType : DIMENSIONS) {
+                log.info("开始同步维度 {} 的数据", dimensionType);
+                long dimStartTime = System.currentTimeMillis();
+
+                // 对日期范围内的每一天进行处理
+                for (Date statDate : dateRange) {
+                    try {
+                        // 检查该维度当天是否已经同步
+                        int exists = syncMapper.checkDimensionExists(statDate, dimensionType);
+                        if (exists > 0) {
+                            log.info("维度 {} 的 {} 数据已同步,跳过",
+                                    dimensionType, DateUtils.parseDateToStr("yyyy-MM-dd", statDate));
+                            continue; // 跳过该日期,继续处理下一天
+                        }
+
+                        // 获取当天的时间范围
+                        Date startTime = getStartOfDay(statDate);
+                        Date endTime = getEndOfDay(statDate);
+
+                        // 同步单个维度当天数据
+                        syncSingleDimension(dimensionType, statDate, startTime, endTime);
+
+                    } catch (Exception e) {
+                        log.error("维度 {} 在日期 {} 同步失败:{}",
+                                dimensionType, DateUtils.parseDateToStr("yyyy-MM-dd", statDate), e.getMessage(), e);
+                        // 继续处理下一天
+                    }
+                }
+
+                long dimEndTime = System.currentTimeMillis();
+                log.info("维度 {} 同步完成,耗时:{}ms", dimensionType, dimEndTime - dimStartTime);
+            }
+
+            // 清理旧数据(保留90天)
+            syncMapper.cleanOldData(90);
+
+            log.info("指定日期范围同步完成");
+
+        } catch (Exception e) {
+            log.error("指定日期范围同步任务执行失败:{}", e.getMessage(), e);
+            throw e;
+        }
+
+    }
+
+
+    /**
+     * 获取日期范围内的所有日期列表
+     */
+    private List<Date> getDateRange(Date startDate, Date endDate) {
+        List<Date> dateList = new ArrayList<>();
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(startDate);
+
+        Calendar endCalendar = Calendar.getInstance();
+        endCalendar.setTime(endDate);
+
+        while (!calendar.getTime().after(endCalendar.getTime())) {
+            dateList.add(calendar.getTime());
+            calendar.add(Calendar.DAY_OF_MONTH, 1);
+        }
+
+        return dateList;
+    }
+
+
+    /**
+     * 获取一天的开始时间(00:00:00.000)
+     */
+    private Date getStartOfDay(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar.getTime();
+    }
+
+    private Date getEndOfDay(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.HOUR_OF_DAY, 23);
+        calendar.set(Calendar.MINUTE, 59);
+        calendar.set(Calendar.SECOND, 59);
+        calendar.set(Calendar.MILLISECOND, 999);
+        return calendar.getTime();
+    }
+
+    @Override
+    public void syncHistoryStatistics() {
+
+    }
+
+    @Override
+    public void syncIncrementalStatistics() {
+
+    }
+
+    @Override
+    public void resyncStatistics(Date startDate, Date endDate) {
+
+    }
+
+
+    // 辅助方法
+    private Long getLongValue(Object value) {
+        if (value == null) return null;
+        if (value instanceof Long) return (Long) value;
+        if (value instanceof Integer) return ((Integer) value).longValue();
+        if (value instanceof String) {
+            try {
+                return Long.parseLong((String) value);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    private Integer getIntValue(Object value) {
+        if (value == null) return 0;
+        if (value instanceof Integer) return (Integer) value;
+        if (value instanceof Long) return ((Long) value).intValue();
+        if (value instanceof String) {
+            try {
+                return Integer.parseInt((String) value);
+            } catch (NumberFormatException e) {
+                return 0;
+            }
+        }
+        return 0;
+    }
+
+    private BigDecimal getBigDecimalValue(Object value) {
+        if (value == null) return BigDecimal.ZERO;
+        if (value instanceof BigDecimal) return (BigDecimal) value;
+        if (value instanceof Double) return BigDecimal.valueOf((Double) value);
+        if (value instanceof Float) return BigDecimal.valueOf((Float) value);
+        if (value instanceof String) {
+            try {
+                return new BigDecimal((String) value);
+            } catch (NumberFormatException e) {
+                return BigDecimal.ZERO;
+            }
+        }
+        return BigDecimal.ZERO;
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 214 - 210
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java


+ 5 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -806,6 +806,11 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         fsUserCourseMapper.editConfig(id, configJson);
     }
 
+    @Override
+    public List<OptionsVO> selectCourseOptionsList() {
+        return fsUserCourseMapper.selectCourseOptionsList();
+    }
+
 
     private Graphics2D initializeGraphics(BufferedImage combined) {
         Graphics2D graphics = combined.createGraphics();

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

@@ -2626,8 +2626,15 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 //        FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
         FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId(), param.getPeriodId());
 
-        if (!isUserCoursePeriodValid(param)) {
-            return ResponseResult.fail(ExceptionCodeEnum.WATCH_LATEST_COURSE.getCode(), ExceptionCodeEnum.WATCH_LATEST_COURSE.getDescription());
+        Map<String, Object> userCoursePeriodValid = isUserCoursePeriodValid(param);
+        if (CloudHostUtils.hasCloudHostName("叮当国医") ) {
+            if (!((boolean)userCoursePeriodValid.get("isWithinRangeSafe"))) {
+                return ResponseResult.fail(BizResponseEnum.WATCH_LATEST_COURSE, userCoursePeriodValid);
+            }
+        } else {
+            if (!((boolean)userCoursePeriodValid.get("isWithinRangeSafe"))) {
+                return ResponseResult.fail(ExceptionCodeEnum.WATCH_LATEST_COURSE.getCode(), ExceptionCodeEnum.WATCH_LATEST_COURSE.getDescription());
+            }
         }
         // 项目看课数限制
         if (!EXCLUDE_PROJECTS.contains(signProjectName) && !CloudHostUtils.hasCloudHostName("弘德堂")) {
@@ -2702,7 +2709,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
      * @param param 查询参数
      * @return 如果当前时间在有效范围内且状态为1,返回true,否则返回false
      */
-    public boolean isUserCoursePeriodValid(FsUserCourseAddCompanyUserParam param) {
+    public Map<String, Object> isUserCoursePeriodValid(FsUserCourseAddCompanyUserParam param) {
+        Map<String, Object> map = new HashMap<>();
         // 查询课程周期信息
         FsUserCoursePeriodDays periodDays = getPeriodDaysInfo(param);
 
@@ -2720,9 +2728,20 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         log.error("传入参数:开始时间:{},结束时间:{},periodDays={}", effectiveStartTime, effectiveEndTime, periodDays);
         if (ObjectUtils.isEmpty(effectiveStartTime) || ObjectUtils.isEmpty(effectiveEndTime)) {
             log.error("请检查营期时间!");
-            return false;
+            map.put("isWithinRangeSafe", false);
+            return map;
+        }
+        boolean isWithinRangeSafe = DateUtil.isWithinRangeSafe(LocalDateTime.now(), effectiveStartTime, effectiveEndTime) && periodDays.getStatus() == 1;
+        map.put("isWithinRangeSafe", isWithinRangeSafe);
+        // 计算差值,如果是未到开课时间的课程,计算出时间差值
+        if(periodDays.getStatus() != 2 && LocalDateTime.now().isBefore(effectiveStartTime)){
+            long millis = Duration.between(LocalDateTime.now(), effectiveStartTime).toMillis();
+            map.put("millis", millis);
+            map.put("countdown", true);
+        } else {
+            map.put("countdown", false);
         }
-        return DateUtil.isWithinRangeSafe(LocalDateTime.now(), effectiveStartTime, effectiveEndTime) && periodDays.getStatus() == 1;
+        return map;
     }
 
     // 其他辅助方法保持不变
@@ -4475,5 +4494,10 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     public int batchEditCover(BatchEditCoverParam param) {
         return baseMapper.batchEditCover(param);
     }
+
+    @Override
+    public List<OptionsVO> selectVideoOptionsByCourseId(Long courseId) {
+        return fsUserCourseVideoMapper.selectVideoOptionsByCourseId(courseId);
+    }
 }
 

+ 59 - 0
fs-service/src/main/java/com/fs/course/vo/FsCourseReportVO.java

@@ -0,0 +1,59 @@
+package com.fs.course.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class FsCourseReportVO {
+
+    /** 公司id */
+    private  Long companyId;
+
+    /** 公司名称 */
+    @Excel(name = "销售公司")
+    private String companyName;
+
+    /** 课程id */
+    private  Long courseId;
+
+
+    /** 课程名称 */
+    @Excel(name = "课程名称")
+    private  String courseName;
+
+
+    /**
+     * 进线人数
+     */
+    @Excel(name = "进线人数")
+    private  Integer accessCount;
+
+
+    /**
+     * 完课人数
+     */
+    @Excel(name = "完课人数")
+    private  Integer finishedCount;
+
+    /**
+     * 完播数
+     */
+    @Excel(name = "完播数")
+    private  Integer courseCompleteTimes;
+
+    /**
+     * 完课率
+     */
+    @Excel(name = "完课率")
+    private   BigDecimal finishRate;
+
+
+    /** 视频id */
+    private  Long videoId;
+
+    /** 视频名称 */
+    @Excel(name = "视频名称")
+    private  String videoName;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/his/domain/HsyyValidationResult.java

@@ -0,0 +1,26 @@
+package com.fs.his.domain;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author:peicj
+ * @Description: 验证订单数据
+ * @Date:2026/1/7 14:58
+ */
+@Data
+public class HsyyValidationResult {
+
+    private boolean isValid = true;
+    private String message = "";
+    private FsPatient patient;
+    private FsDoctor doctor;
+    private FsDoctor drugDoctor;
+    private FsDepartment department;
+    private FsInquiryOrder inquiryOrder;
+    private FsPrescribe prescribe;
+    private FsHospital hospital;
+    private FsStore store;
+    private List<FsPrescribeDrug> prescribeDrugs;
+}

+ 80 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/AdministrationRoute.java

@@ -0,0 +1,80 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 给药途径
+ * @Date:2026/1/5 11:23
+ */
+public enum AdministrationRoute {
+
+    // 基础给药途径
+    ORAL(1, "口服"),
+    RECTAL(2, "直肠给药"),
+    SUBLINGUAL(3, "舌下给药"),
+    INJECTION(4, "注射给药"),
+    INHALATION(5, "吸入给药"),
+    LOCAL_APPLICATION(6, "局部用药"),
+    OTHER_ROUTE(9, "其他给药途径"),
+
+    // 注射给药子类型
+    SUBCUTANEOUS_INJECTION(401, "皮下注射"),
+    INTRADERMAL_INJECTION(402, "皮内注射"),
+    INTRAMUSCULAR_INJECTION(403, "肌肉注射"),
+    INTRAVENOUS_INJECTION(404, "静脉注射或静脉滴注"),
+
+    // 局部用药子类型
+    INTRATHECAL_USE(601, "椎管内用药"),
+    INTRA_ARTICULAR_USE(602, "关节腔内用药"),
+    PLEURAL_CAVITY_USE(603, "胸膜腔用药"),
+    PERITONEAL_CAVITY_USE(604, "腹腔用药"),
+    VAGINAL_USE(605, "阴道用药"),
+    TRACHEAL_USE(606, "气管内用药"),
+    EYE_DROPS(607, "滴眼"),
+    NASAL_DROPS(608, "滴鼻"),
+    THROAT_SPRAY(609, "喷喉"),
+    SUBLINGUAL_USE(610, "含化"),
+    WOUND_DRESSING(611, "敷伤口"),
+    SKIN_APPLICATION(612, "擦皮肤"),
+    OTHER_LOCAL_ROUTE(699, "其他局部给药途径");
+
+    private final int code;
+    private final String description;
+
+    AdministrationRoute(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static AdministrationRoute fromCode(int code) {
+        for (AdministrationRoute route : values()) {
+            if (route.code == code) {
+                return route;
+            }
+        }
+        return OTHER_ROUTE; // 默认返回其他给药途径
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (AdministrationRoute route : values()) {
+            if (route.description.equals(description)) {
+                return route.code;
+            }
+        }
+        return OTHER_ROUTE.code;
+    }
+}

+ 54 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/AntibioticLevel.java

@@ -0,0 +1,54 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 抗生素级别代码
+ * @Date:2026/1/5 11:30
+ */
+public enum AntibioticLevel {
+
+    SPECIAL_LEVEL(1, "特殊级"),
+    RESTRICTED_LEVEL(2, "限制级"),
+    NON_RESTRICTED_LEVEL(3, "非限制级"),
+    OTHER(99, "其他");
+
+    private final int code;
+    private final String description;
+
+    AntibioticLevel(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static AntibioticLevel fromCode(int code) {
+        for (AntibioticLevel level : values()) {
+            if (level.code == code) {
+                return level;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (AntibioticLevel level : values()) {
+            if (level.description.equals(description)) {
+                return level.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 58 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/CardType.java

@@ -0,0 +1,58 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 卡类型
+ * @Date:2026/1/5 11:13
+ */
+public enum CardType {
+
+    HOSPITAL_TREATMENT_CARD(1, "院内诊疗卡"),
+    VIRTUAL_CARD(2, "虚拟卡"),
+    HEALTH_CARD(3, "健康卡"),
+    SOCIAL_SECURITY_CARD(4, "社保卡"),
+    MEDICAL_INSURANCE_CARD(5, "医保卡"),
+    CITIZEN_CARD(6, "市民卡"),
+    OTHER(9, "其它");
+
+    private final int code;
+    private final String description;
+
+    CardType(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static CardType fromCode(int code) {
+        for (CardType cardType : values()) {
+            if (cardType.code == code) {
+                return cardType;
+            }
+        }
+        return OTHER; // 默认返回其它
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (CardType cardType : values()) {
+            if (cardType.description.equals(description)) {
+                return cardType.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/DecoctionRequirement.java

@@ -0,0 +1,57 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 煎煮要求
+ * @Date:2026/1/5 11:26
+ */
+public enum DecoctionRequirement {
+
+    PRE_DECOCION(1, "先煎"),
+    POST_ADDITION(2, "后下"),
+    WRAPPED_DECOCION(3, "包煎"),
+    SEPARATE_DECOCION(4, "另煎"),
+    MELTING(5, "烊化"),
+    FLUSHING(6, "冲服"),
+    OTHER(99, "其他");
+
+    private final int code;
+    private final String description;
+
+    DecoctionRequirement(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static DecoctionRequirement fromCode(int code) {
+        for (DecoctionRequirement requirement : values()) {
+            if (requirement.code == code) {
+                return requirement;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (DecoctionRequirement requirement : values()) {
+            if (requirement.description.equals(description)) {
+                return requirement.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 127 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/DrugFormulation.java

@@ -0,0 +1,127 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 药物剂型代码
+ * @Date:2026/1/5 11:27
+ */
+public enum DrugFormulation {
+
+    RAW_MATERIAL(0, "原料"),
+    UNCOATED_TABLETS(1, "片剂(素片,压制片),浸膏片,非包衣片"),
+    COATED_TABLETS(2, "糖衣片,包衣片,薄膜衣片"),
+    CHEWING_TABLETS(3, "咀嚼片,糖片,异型片,糖胶片"),
+    ENTERIC_COATED_TABLETS(4, "肠溶片(肠衣片)"),
+    MODIFIED_RELEASE_TABLETS(5, "调释片,缓释片,控释片,长效片"),
+    EFFERVESCENT_TABLETS(6, "泡腾片"),
+    SUBLINGUAL_TABLETS(7, "舌下片"),
+    LOZENGES(8, "含片,嗽口片(含嗽片),喉症片(喉片),口腔粘附片"),
+    EXTERNAL_TABLETS(9, "外用片,外用膜,坐药片,环型片"),
+    VAGINAL_TABLETS(10, "阴道片,外用阴道膜,阴道用药,阴道栓片"),
+    WATER_SOLUBLE_TABLETS(11, "水溶片,眼药水片"),
+    DISPERSIBLE_TABLETS(12, "分散片(适应片)"),
+    PAPER_TABLETS(13, "纸片(纸型片),膜片(薄膜片)"),
+    PILLS(14, "丸剂,药丸,眼丸,耳丸,糖丸,糖衣丸,浓缩丸,调释丸,水丸"),
+    POWDER_INJECTION(15, "粉针剂(冻干粉针剂),冻干粉"),
+    INJECTION(16, "注射液(水针剂),油针剂,混悬针剂"),
+    INJECTION_SOLVENT(17, "注射溶媒(在16有冲突时,可代油针剂,混悬针剂)"),
+    INFUSION(18, "输液剂,血浆代用品"),
+    CAPSULES(19, "胶囊剂,硬胶囊"),
+    SOFT_CAPSULES(20, "软胶囊,滴丸,胶丸"),
+    ENTERIC_COATED_CAPSULES(21, "肠溶胶囊,肠溶胶丸"),
+    MODIFIED_RELEASE_CAPSULES(22, "调释胶囊,控释胶囊,缓释胶囊"),
+    SOLUTION(23, "溶液剂,含漱液,内服混悬液"),
+    MIXTURE(24, "合剂"),
+    EMULSION(25, "乳剂,乳胶"),
+    GEL(26, "凝胶剂,胶剂(胶体),胶冻,胶体微粒"),
+    MUCILAGE(27, "胶浆剂"),
+    AROMATIC_WATER(28, "芳香水剂(露剂)"),
+    DROPS(29, "滴剂"),
+    SYRUP(30, "糖浆剂(蜜浆剂)"),
+    ORAL_LIQUID(31, "口服液"),
+    EXTRACT(32, "浸膏剂"),
+    FLUID_EXTRACT(33, "流浸膏剂"),
+    TINCTURE(34, "酊剂"),
+    SPIRIT(35, "醑剂"),
+    JUICE(36, "酏剂"),
+    LINIMENT(37, "洗剂,阴道冲洗剂"),
+    RUBEFACIENT(38, "搽剂(涂剂,擦剂),外用混悬液剂"),
+    OIL(39, "油剂,甘油剂"),
+    COLLIDION(40, "棉胶剂(火棉胶剂)"),
+    FILM_FORMING_AGENT(41, "涂膜剂"),
+    APPLICATION(42, "涂布剂"),
+    EYE_DROPS(43, "滴眼剂,洗眼剂,粉剂眼花缭乱药"),
+    NASAL_DROPS(44, "滴鼻剂,洗鼻剂"),
+    EAR_DROPS(45, "滴耳剂,洗耳剂"),
+    ORAL_CAVITY_AGENT(46, "口腔药剂,口腔用药,牙科用药"),
+    ENEMA(47, "灌肠剂"),
+    OINTMENT(48, "软膏剂(油膏剂,水膏剂)"),
+    CREAM(49, "霜剂(乳膏剂)"),
+    PASTE(50, "糊剂"),
+    PLASTER(51, "硬膏剂,橡皮膏"),
+    EYE_OINTMENT(52, "眼膏剂"),
+    POWDER(53, "散剂(内服散剂,外用散剂,粉剂,撒布粉"),
+    GRANULES(54, "颗粒剂(冲剂),晶剂(结晶,晶体),干糖浆"),
+    EFFERVESCENT_GRANULES(55, "泡腾颗粒剂"),
+    MODIFIED_RELEASE_GRANULES(56, "调释颗粒剂,缓释颗粒剂"),
+    AEROSOL(57, "气雾剂,水雾剂, (加抛射剂)"),
+    SPRAY(58, "喷雾剂, (不加抛射剂)"),
+    SUSPENSION_AEROSOL(59, "混悬雾剂, (水,气,粉三相)"),
+    INHALATION_AGENT(60, "吸入药剂(鼻吸式),粉雾剂"),
+    FILM(61, "膜剂(口腔膜)"),
+    SPONGE(62, "海绵剂"),
+    SUPPOSITORY(63, "栓剂,痔疮栓,耳栓"),
+    IMPLANT_SUPPOSITORY(64, "植入栓"),
+    TRANSDERMAL_AGENT(65, "透皮剂,贴剂(贴膏,贴膜),贴片"),
+    CONTROLLED_RELEASE_TRANSDERMAL(66, "控释透皮剂,控释贴片,控释口颊片"),
+    SCARIFICATION_AGENT(67, "划痕剂"),
+    PEARL_CHAIN(68, "珠链(泥珠链)"),
+    LOZENGE(69, "锭剂,糖锭"),
+    MICROCAPSULE(70, "微囊胶囊(微丸胶囊)"),
+    DRY_SUSPENSION(71, "干混悬剂(干悬乳剂/口服乳干粉)"),
+    INHALANT(72, "吸放剂(气体)"),
+    KIT(90, "试剂盒(诊断用试剂),药盒"),
+    OTHER(99, "其它剂型(空心胶囊,绷带,纱布,胶布");
+
+    private final int code;
+    private final String description;
+
+    DrugFormulation(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static DrugFormulation fromCode(int code) {
+        for (DrugFormulation formulation : values()) {
+            if (formulation.code == code) {
+                return formulation;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (DrugFormulation formulation : values()) {
+            if (formulation.description.equals(description)) {
+                return formulation.code;
+            }
+        }
+        return OTHER.code;
+    }
+
+}

+ 55 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/EducationLevel.java

@@ -0,0 +1,55 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 学历代码
+ * @Date:2026/1/5 11:21
+ */
+public enum EducationLevel {
+
+    DOCTORAL_GRADUATE(11, "博士研究生"),
+    MASTER_GRADUATE(14, "硕士研究生"),
+    BACHELOR(21, "本科"),
+    ASSOCIATE(31, "专科"),
+    OTHER(90, "其他");
+
+    private final int code;
+    private final String description;
+
+    EducationLevel(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static EducationLevel fromCode(int code) {
+        for (EducationLevel level : values()) {
+            if (level.code == code) {
+                return level;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (EducationLevel level : values()) {
+            if (level.description.equals(description)) {
+                return level.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 59 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/ExaminationType.java

@@ -0,0 +1,59 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 检查类型
+ * @Date:2026/1/5 11:31
+ */
+public enum ExaminationType {
+
+    CT(1, "CT"),
+    RADIOLOGY(2, "放射线"),
+    MRI(3, "核磁"),
+    ULTRASOUND(4, "超声"),
+    ECG(5, "心电"),
+    EEG(6, "脑电"),
+    GASTROSCOPY(7, "胃镜"),
+    COLONOSCOPY(8, "肠镜"),
+    OTHER(9, "其他检查");
+
+    private final int code;
+    private final String description;
+
+    ExaminationType(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static ExaminationType fromCode(int code) {
+        for (ExaminationType type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return OTHER; // 默认返回其他检查
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (ExaminationType type : values()) {
+            if (type.description.equals(description)) {
+                return type.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 55 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/Gender.java

@@ -0,0 +1,55 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 性别
+ * @Date:2026/1/5 11:10
+ */
+public enum Gender {
+
+    UNKNOWN(0, "未知"),
+    MALE(1, "男"),
+    FEMALE(2, "女"),
+    OTHER(9, "其他");
+
+    private final int code;
+    private final String description;
+
+    Gender(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static Gender fromCode(int code) {
+        for (Gender gender : values()) {
+            if (gender.code == code) {
+                return gender;
+            }
+        }
+        return UNKNOWN; // 默认返回未知
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (Gender gender : values()) {
+            if (gender.description.equals(description)) {
+                return gender.code;
+            }
+        }
+        return UNKNOWN.code;
+    }
+}

+ 56 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/IdCardType.java

@@ -0,0 +1,56 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 证件类型
+ * @Date:2026/1/5 11:15
+ */
+public enum IdCardType {
+
+    ID_CARD(1, "身份证"),
+    HK_MACAU_PASS(2, "港澳居民来往内地通行证"),
+    TAIWAN_PASS(3, "台湾居民来往大陆通行证/台胞证"),
+    PASSPORT(4, "护照"),
+    OTHER(9, "其它");
+
+    private final int code;
+    private final String description;
+
+    IdCardType(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static IdCardType fromCode(int code) {
+        for (IdCardType idCardType : values()) {
+            if (idCardType.code == code) {
+                return idCardType;
+            }
+        }
+        return OTHER; // 默认返回其它
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (IdCardType idCardType : values()) {
+            if (idCardType.description.equals(description)) {
+                return idCardType.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/LaboratoryType.java

@@ -0,0 +1,57 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 检验类型
+ * @Date:2026/1/5 11:33
+ */
+public enum LaboratoryType {
+
+    IMMUNOLOGY(1, "免疫"),
+    MICROBIOLOGY(2, "微生物"),
+    BIOCHEMISTRY(3, "生化"),
+    BIOMOLECULAR(4, "生物分子"),
+    CLINICAL_LAB(5, "临检"),
+    EMERGENCY_LAB(6, "急诊检验"),
+    OTHER(9, "其他检验");
+
+    private final int code;
+    private final String description;
+
+    LaboratoryType(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static LaboratoryType fromCode(int code) {
+        for (LaboratoryType type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return OTHER; // 默认返回其他检验
+    }
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (LaboratoryType type : values()) {
+            if (type.description.equals(description)) {
+                return type.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/MedicationRoute.java

@@ -0,0 +1,57 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 用药途径
+ * @Date:2026/1/5 11:34
+ */
+public enum MedicationRoute {
+
+    CHRONIC_DISEASE(1, "慢病用药"),
+    GYNECOLOGY(2, "妇科用药"),
+    DERMATOLOGY(3, "皮肤用药"),
+    ONCOLOGY(4, "肿瘤用药"),
+    CARDIOVASCULAR(5, "心脑血管"),
+    PEDIATRIC(6, "儿童用药"),
+    OTHER(7, "其他");
+
+    private final int code;
+    private final String description;
+
+    MedicationRoute(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static MedicationRoute fromCode(int code) {
+        for (MedicationRoute route : values()) {
+            if (route.code == code) {
+                return route;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (MedicationRoute route : values()) {
+            if (route.description.equals(description)) {
+                return route.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 58 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/PaymentType.java

@@ -0,0 +1,58 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 支付类型
+ * @Date:2026/1/5 11:16
+ */
+public enum PaymentType {
+
+    CASH(1, "现金"),
+    ONLINE_PAYMENT(2, "在线支付"),
+    WECHAT(21, "微信"),
+    ALIPAY(22, "支付宝"),
+    UNIONPAY(23, "银联"),
+    BANK(24, "银行"),
+    OTHER(99, "其他");
+
+    private final int code;
+    private final String description;
+
+    PaymentType(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static PaymentType fromCode(int code) {
+        for (PaymentType paymentType : values()) {
+            if (paymentType.code == code) {
+                return paymentType;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (PaymentType paymentType : values()) {
+            if (paymentType.description.equals(description)) {
+                return paymentType.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 64 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/PoliticalStatus.java

@@ -0,0 +1,64 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 政治面貌国标码
+ * @Date:2026/1/5 11:20
+ */
+public enum PoliticalStatus {
+
+    CPC_MEMBER(1, "中国共产党党员"),
+    CPC_PROBATIONARY_MEMBER(2, "中国共产党预备党员"),
+    CYLC_MEMBER(3, "中国共产主义青年团团员"),
+    CMRPG_MEMBER(4, "中国国民党革命委员会会员"),
+    CDM_MEMBER(5, "中国民主同盟盟员"),
+    CDBM_MEMBER(6, "中国民主建国会会员"),
+    CFM_MEMBER(7, "中国民主促进会会员"),
+    CADPM_MEMBER(8, "中国农工民主党党员"),
+    CCPP_MEMBER(9, "中国致公党党员"),
+    JSS_MEMBER(10, "九三学社社员"),
+    TDMAT_MEMBER(11, "台湾民主自治同盟盟员"),
+    NON_PARTISAN(12, "无党派民主人士"),
+    PUBLIC(13, "群众");
+
+    private final int code;
+    private final String description;
+
+    PoliticalStatus(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static PoliticalStatus fromCode(int code) {
+        for (PoliticalStatus status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return PUBLIC; // 默认返回群众
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (PoliticalStatus status : values()) {
+            if (status.description.equals(description)) {
+                return status.code;
+            }
+        }
+        return PUBLIC.code;
+    }
+}

+ 84 - 0
fs-service/src/main/java/com/fs/his/enums/hsyy/ProfessionalTitle.java

@@ -0,0 +1,84 @@
+package com.fs.his.enums.hsyy;
+
+/**
+ * @Author:peicj
+ * @Description: 职称
+ * @Date:2026/1/5 11:19
+ */
+public enum ProfessionalTitle {
+
+    // 卫生技术人员(医疗)
+    MEDICAL_STAFF(23, "卫生技术人员(医疗)"),
+    CHIEF_PHYSICIAN(231, "主任医师"),
+    VICE_CHIEF_PHYSICIAN(232, "副主任医师"),
+    ATTENDING_PHYSICIAN(233, "主治医师"),
+    PHYSICIAN(234, "医师"),
+    MEDICAL_CLERK(235, "医士"),
+
+    // 卫生技术人员(药剂)
+    PHARMACY_STAFF(24, "卫生技术人员(药剂)"),
+    CHIEF_PHARMACIST(241, "主任药师"),
+    VICE_CHIEF_PHARMACIST(242, "副主任药师"),
+    CHIEF_PHARMACY_TECHNICIAN(243, "主管药师"),
+    PHARMACIST(244, "药师"),
+    PHARMACY_CLERK(245, "药士"),
+
+    // 卫生技术人员(护理)
+    NURSING_STAFF(25, "卫生技术人员(护理)"),
+    CHIEF_NURSE(251, "主任护师"),
+    VICE_CHIEF_NURSE(252, "副主任护师"),
+    CHIEF_NURSING_TECHNICIAN(253, "主管护师"),
+    NURSE(254, "护师"),
+    NURSE_ASSISTANT(255, "护士"),
+
+    // 卫生技术人员(技师)
+    TECHNICIAN_STAFF(26, "卫生技术人员(技师)"),
+    CHIEF_TECHNICIAN(261, "主任技师"),
+    VICE_CHIEF_TECHNICIAN(262, "副主任技师"),
+    CHIEF_TECHNICAL_SUPERVISOR(263, "主管技师"),
+    TECHNICIAN(264, "技师"),
+    TECHNICAL_CLERK(265, "技士"),
+
+    OTHER(999, "其他");
+
+    private final int code;
+    private final String description;
+
+    ProfessionalTitle(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    // 根据code获取枚举
+    public static ProfessionalTitle fromCode(int code) {
+        for (ProfessionalTitle title : values()) {
+            if (title.code == code) {
+                return title;
+            }
+        }
+        return OTHER; // 默认返回其他
+    }
+
+    // 根据code获取描述
+    public static String getDescriptionByCode(int code) {
+        return fromCode(code).getDescription();
+    }
+
+    // 根据描述获取code
+    public static int getCodeByDescription(String description) {
+        for (ProfessionalTitle title : values()) {
+            if (title.description.equals(description)) {
+                return title.code;
+            }
+        }
+        return OTHER.code;
+    }
+}

+ 2 - 0
fs-service/src/main/java/com/fs/his/mapper/FsStoreSubOrderMapper.java

@@ -168,4 +168,6 @@ public interface FsStoreSubOrderMapper
             "        </where> "+
             "</script>"})
     Long selectFsStoreSubOrderListCount(FsStoreOrderParam fsStoreSubOrder);
+    @Select("SELECT * FROM fs_store_sub_order WHERE create_time >= #{yesterdayStartStr} and create_time <= #{todayStartStr} order by create_time")
+    List<FsStoreSubOrder> selectFsStoreSubOrderListByCreateTime(@Param("yesterdayStartStr") String yesterdayStartStr, @Param("todayStartStr") String todayStartStr);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java

@@ -139,4 +139,9 @@ public interface FsUserIntegralLogsMapper
     Long selectH5VideoIntegralCount(@Param("userId") Long userId,@Param("videoId") Long videoId);
 
     List<FsUserIntegralLogs> selectFsUserIntegralLogsByUserIdAndLogType(@Param("userId") Long userId, @Param("logType") Integer logType, @Param("date") LocalDate date);
+
+    /**
+     * 查询用户最新的积分记录
+     */
+    FsUserIntegralLogs selectLatestIntegralLogByUserId(@Param("userId") Long userId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -471,4 +471,9 @@ public interface FsUserMapper
     List<FsUser> selectFsUserListByPhone(String phone);
 
     void updatePasswordByPhone(@Param("password")String password, @Param("encryptPhone")String encryptPhone);
+
+    /**
+     * 查询用户列表(用于积分管理,支持手机号和昵称模糊查询)
+     */
+    List<FsUser> selectFsUserListForIntegral(FsUser fsUser);
 }

+ 432 - 0
fs-service/src/main/java/com/fs/his/param/FsSubOrderHsyyParam.java

@@ -0,0 +1,432 @@
+package com.fs.his.param;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * @Author:peicj
+ * @Description: 河山医院请求参数
+ * @Date:2026/1/5 9:57
+ * @Fields:
+ * userinfo List 是 患者信息
+ * department List 是 科室信息
+ * doctor List 是 医生信息
+ * medicine List 是 药品信息
+ * offline List 是 线下初诊信息
+ * recipe List 是 处方信息
+ * sign List 是 签名
+ * uniacid String (32) 是 Uniacid 接口提供方提供
+ * orderNo String (255) 是 订单号
+ * hname String (255) 是 医院名称
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class FsSubOrderHsyyParam {
+
+    /**
+     * 患者信息
+     * @Fields:
+     * name String (32) 是 患者姓名
+     * numcard String (16) 是 患者身份证
+     * birthday String (16) 是 患者生日
+     * sex String (2) 是 患者性别
+     * datetime String (16) 是 患者出生日期
+     * age decimal(10,1) 是 患者年龄
+     * tel String (32) 是 患者电话
+     * present_illness_history String (50) 是 现病史
+     * past_history String (200) 是 既往史
+     * personal_history String (200) 是 个人史
+     * family_history String (200) 是 家族史
+     * allergy String (200) 是 过敏史
+     * tijian String (200) 是 体检
+     * usertime String (16) 是 就诊时间
+     */
+    private UserinfoType userinfo;
+
+    /**
+     * 科室信息
+     * @Fields:
+     * dep_name String (50) 是 科室名称 例子:中医科
+     * dep_id String (50) 是 科室 ID HIS 对应 ID
+     * dep_code String (50) 是 科室编码 例子:A50
+     */
+    private DepartmentType department;
+
+    /**
+     * 医生信息
+     * @Fields:
+     * doc_name String (50) 是 医生姓名
+     * doc_numcard String (50) 是 医生身份证号
+     * doc_sex String (50) 是 医生性别
+     * doc_tel String (50) 是 医生电话
+     * doc_zhicheng String (50) 是 医生职称
+     * doc_code String (50) 是 医生编码
+     * doc_type String (50) 是 医生类型 1 医生 3 药师
+     * doc_rank String (50) 是 医生职级
+     * doc_license_no String (50) 是 资格证号码
+     * doc_license_date String (50) 是 资格证发证日期 格式:YYYY-MM-DD
+     * doc_practice_no String (50) 是 执业证号码
+     * doc_practice_address String (50) 是 第一执业地址
+     * doc_practice_category String (50) 是 执业类型
+     * doc_practice_scope String (50) 是 执业范围
+     * doc_first_date String (50) 是 执业证首次注册日期 格式:YYYY-MM-DD
+     * doc_current_date String (50) 是 执业证本次注册日期 格式:YYYY-MM-DD
+     * doc_valid_date String (50) 是 执业证注册有效期 格式:YYYY-MM-DD
+     * doc_politics_status String (50) 是 政治面貌
+     * doc_starting_work_date String (50) 是 参加工作日期 格式:YYYY-MM-DD
+     * doc_independent_work_date String (50) 是 独立工作日期 格式:YYYY-MM-DD
+     * doc_fullTime_degree String (50) 是 全日制最高所学专业情况学历
+     * doc_full_time_degree_year String (50) 是 全日制最高所学专业情况学历取得年份 格式:YYYY
+     */
+    private DoctorType doctor;
+
+    /**
+     * 药品信息
+     * @Fields:
+     * bm String (64) 是 编码
+     * store_id String (64) 否 药店ID
+     * store_name String (255) 否 药店名称
+     * sname String (255) 是 药品名称(商品名)
+     * tname String (255) 是 药品通用名
+     * price decimal(11,2) 是 单价 单位:分
+     * guige String (255) 是 规格
+     * doc_num String (50) 是 批准文号
+     * component String (255) 是 成分
+     * character String (255) 是 性状
+     * adapt String (255) 是 适应症
+     * use String (255) 是 用法用量
+     * dosage String (255) 是 剂量
+     * frequency String (255) 是 使用频次
+     * unit String (255) 是 剂量单位
+     * adverse_reactions String (255) 是 不良反应
+     * dosageform String (255) 是 剂型
+     * package_spec String (255) 是 包装规格 例:0.5g*30 片/盒
+     * count String (10) 是 数量
+     * amount decimal(11,2) 是 金额 单位:分
+     * health_care_code String (100) 是 医保编码
+     * health_care_type String (2) 是 医保类型 药品医保级别,1-有自付,2-无自付,3- 全自付
+     * is_base String (2) 是 是否基药 0 否 1 是
+     * manufacturer String (100) 是 生产厂商
+     * has_prescription String (2) 是 是否包含处方药 0 否 1 是
+     * supplier String (32) 是 药品供应商
+     * medication_route String (2) 是 用药途径 参考字典
+     * checker_name String (50) 审核人姓名
+     * dispenser_name String (50) 发药人姓名
+     */
+    private List<MedicineType> medicine;
+
+    /**
+     * 线下初诊信息
+     * @Fields:
+     * medical_record_no String (50) 是 患者病历号 需要和HIS一样
+     * visit_no String (50) 是 就诊号
+     * visit_date String (10) 是 就诊日期
+     * offline_dept_code String (50) 是 线下初诊就诊科室代号
+     * offline_dept_name String (100) 是 线下初诊就诊科室名
+     * offline_doctor_code String (50) 是 线下初诊就诊医生代号
+     * offline_diagnosis String (100) 是 线下初诊就诊诊断
+     */
+    private OfflineType offline;
+
+    /**
+     * 处方信息
+     * @Fields:
+     * recipe_id String(64) 是 处方 id 需要和HIS相同
+     * serial_no String (128) 是 处方序列号
+     * create_time String(32) 是 开方时间 格式:YYYY-MM-DD HH24:MI:SS
+     * status String (16) 是 处方状态
+     * hospital_name String (50) 是 医院名称
+     * dept_code String (32) 是 科室编码
+     * dept_name String (32) 是 科室名称
+     * doc_code String (32) 是 医生编码
+     * doc_name String (50) 是 医生姓名
+     * zhusu String (50) 是 主诉
+     * advice String (128) 是 医嘱
+     * yijian String (50) 是 意见
+     * diagnose String (64) 是 诊断(ICD10)
+     * diagnose_code String (64) 是 诊断编码(ICD10)
+     * recipe_type String (2) 是 处方分类 1、普通处方 2、儿童处方 3、急诊处方 4、毒麻处方 5、一类精神处方 6、二类精神处方 99、未知
+     * getType String(2) 是 药品发放类型 1-在线配送 2- 自提 3-药店配送
+     * Recipe_img String(500) 医保处方地址
+     * Patienttype String(500) 慢病处方/特病处方
+     * Pharmacy String(500) 药店名
+     * Consultationtype String(500) 视频/图文
+     * Ybtype String(10) 医保区划
+     * process List 是 问诊过程
+     * process_time 是 时间 格式:YYYY-MM-DD HH24:MI:SS
+     * role 是 角色 1-患者 2-医生
+     * content 是 内容
+     */
+    private RecipeType recipe;
+
+    /**
+     * 签名信息
+     * @Fields:
+     * role String (16) 是 角色 处方医生/审核人/发药人等
+     * executor String (16) 是 执行人姓名
+     * person_code String (16) 是 执行人编码
+     * exe_time String (32) 是 执行时间 格式:YYYY-MM-DD HH24:MI:SS
+     */
+    private List<SignType> sign;
+    //Uniacid 接口提供方提供
+    private Integer uniacid;
+    //订单号
+    private String orderNo;
+    //医院名称
+    private String hname;
+
+    private Integer ybtype;
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UserinfoType {
+        private String name;
+        /**
+         * {@link com.fs.his.enums.hsyy.IdCardType}
+         */
+        private String numcard;
+        private String birthday;
+        /**
+         * {@link com.fs.his.enums.hsyy.Gender}
+         */
+        private String sex;
+        private String datetime;
+        private Integer age;
+        private String tel;
+        private String present_illness_history;
+        private String past_history;
+        private String personal_history;
+        private String family_history;
+        private String allergy;
+        private String tijian;
+        private String usertime;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class DepartmentType {
+        private String dep_name;
+        private Integer dep_id;
+        private String dep_code;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class DoctorType {
+        private String doc_name;
+        /**
+         * {@link com.fs.his.enums.hsyy.IdCardType}
+         */
+        private String doc_numcard;
+        /**
+         * {@link com.fs.his.enums.hsyy.Gender}
+         */
+        private Integer doc_sex;
+        private String doc_tel;
+        /**
+         * {@link com.fs.his.enums.hsyy.ProfessionalTitle}
+         */
+        private String doc_zhicheng;
+        private String doc_code;
+        /**
+         * 医生类型 1 医生 3 药师
+         */
+        private String doc_type;
+        private String doc_rank;
+        private String doc_license_no;
+        /**
+         * 资格证发证日期 格式:YYYY-MM-DD
+         */
+        private String doc_license_date;
+        private String doc_practice_no;
+        private String doc_practice_address;
+        private String doc_practice_category;
+        private String doc_practice_scope;
+        /**
+         * 执业证首次注册日期 格式:YYYY-MM-DD
+         */
+        private String doc_first_date;
+        /**
+         *执业证本次注册日期 格式:YYYY-MM-DD
+         */
+        private String doc_current_date;
+        /**
+         * 执业证注册有效期 格式:YYYY-MM-DD
+         */
+        private String doc_valid_date;
+        /**
+         * {@link com.fs.his.enums.hsyy.PoliticalStatus}
+         */
+        private String doc_politics_status;
+        /**
+         * 参加工作日期 格式:YYYY-MM-DD
+         */
+        private String doc_starting_work_date;
+        /**
+         * 独立工作日期 格式:YYYY-MM-DD
+         */
+        private String doc_independent_work_date;
+        /**
+         * {@link com.fs.his.enums.hsyy.EducationLevel}
+         */
+        private String doc_fullTime_degree;
+        /**
+         * 满级教育年限 格式:YYYY
+         */
+        private String doc_full_time_degree_year;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MedicineType {
+        private String bm;
+        private String store_id;
+        private String store_name;
+        private String sname;
+        private String tname;
+        private String price;
+        private String guige;
+        private String doc_num;
+        private String component;
+        private String character;
+        private String adapt;
+        private String use;
+        private String dosage;
+        private String frequency;
+        private String unit;
+        private String adverse_reactions;
+        private String dosageform;
+        private String package_spec;
+        private Integer count;
+        private Integer amount;
+        private String health_care_code;
+        private String health_care_type;
+        private String is_base;
+        private String manufacturer;
+        private String has_prescription;
+        private String supplier;
+        /**
+         * {@link com.fs.his.enums.hsyy.MedicationRoute}
+         */
+        private String medication_route;
+        private Integer getType;
+        private String checker_name;
+        private String dispenser_name;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class OfflineType {
+        private String medical_record_no;
+        private Long visit_no;
+        private String visit_date;
+        private String offline_dept_code;
+        private String offline_dept_name;
+        private String offline_doctor_code;
+        private String offline_diagnosis;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RecipeType {
+        private Long recipe_id;
+        private String serial_no;
+        /**
+         * 开方时间 格式:YYYY-MM-DD HH24:MI:SS
+         */
+        private String create_time;
+        private String status;
+        private String hospital_name;
+        private String dept_code;
+        private String dept_name;
+        private String doc_code;
+        private String doc_name;
+        private String zhusu;
+        private String advice;
+        private String yijian;
+        private String diagnose;
+        private String diagnose_code;
+        /**
+         * 处方分类
+         * 1、普通处方
+         * 2、儿童处方
+         * 3、急诊处方
+         * 4、毒麻处方
+         * 5、一类精神处方
+         * 6、二类精神处方
+         * 99、未知
+         */
+        private String recipe_type;
+        /**
+         * 药品发放类型 1-在线配送 2- 自提 3-药店配送
+         */
+        private Integer getType;
+        private String Recipe_img;
+        /**
+         * 慢病处方/特病处方
+         */
+        private String Patienttype;
+        private String Pharmacy;
+        private String Consultationtype;
+        private String Ybtype;
+        private List<ProcessType> process;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class ProcessType {
+        /**
+         * 时间 格式:YYYY-MM-DD HH24:MI:SS
+         */
+        private String process_time;
+        /**
+         * 角色 1-患者 2-医生
+         */
+        private String role;
+        private String content;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class SignType {
+        /**
+         * 角色 处方医生/审核人/发药人等
+         */
+        private String role;
+        private String executor;
+        private String person_code;
+        /**
+         * 执行时间 格式:YYYY-MM-DD HH24:MI:SS
+         */
+        private String exe_time;
+
+    }
+}

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

@@ -77,4 +77,8 @@ public interface IFsStoreSubOrderService
     Boolean isEntityNull(FsStoreOrderParam fsStoreSubOrder);
 
     Long selectFsStoreSubOrderListCount(FsStoreOrderParam fsStoreSubOrder);
+
+    List<FsStoreSubOrder> selectFsStoreSubOrderListByCreateTime(String yesterdayStartStr, String todayStartStr);
+
+    void pushHsyy(List<FsStoreSubOrder> list);
 }

+ 40 - 22
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -1647,34 +1647,52 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
     @Override
     @Transactional
     public String payConfirm(String payCode, String tradeNo, String bankTransactionId, String bankSerialNo) {
+        // 使用 Redis setNx 加分布式锁,基于支付单号
+        String lockKey = "payConfirm:lock:" + payCode;
+        boolean lockAcquired = false;
         try {
-            //更新订单状态
-            FsStorePayment storePayment=fsStorePaymentMapper.selectFsStorePaymentByPaymentCode(payCode);
-            if(!storePayment.getStatus().equals(0)){
-                return "";
+            // 尝试获取锁,锁过期时间设置为30秒
+            lockAcquired = redisCache.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+            if (!lockAcquired) {
+                // 如果获取锁失败,说明有其他线程正在处理该支付单,直接返回
+                logger.info("支付确认处理中,支付单号: {}", payCode);
+                return "SUCCESS";
             }
-            storePayment.setStatus(1);
-            storePayment.setPayTime(new Date());
-            storePayment.setTradeNo(tradeNo);
-            storePayment.setBankSerialNo(bankSerialNo);
-            storePayment.setBankTransactionId(bankTransactionId);
-            fsStorePaymentMapper.updateFsStorePayment(storePayment);
-            //增加佣金
+
+            try {
+                //更新订单状态
+                FsStorePayment storePayment=fsStorePaymentMapper.selectFsStorePaymentByPaymentCode(payCode);
+                if(!storePayment.getStatus().equals(0)){
+                    return "";
+                }
+                storePayment.setStatus(1);
+                storePayment.setPayTime(new Date());
+                storePayment.setTradeNo(tradeNo);
+                storePayment.setBankSerialNo(bankSerialNo);
+                storePayment.setBankTransactionId(bankTransactionId);
+                fsStorePaymentMapper.updateFsStorePayment(storePayment);
+                //增加佣金
 //        if(storePayment.getCompanyId()!=null&&storePayment.getCompanyId()>0){
 //            companyService.addCompanyPaymentMoney(storePayment);
 //        }
-        } catch (Exception e) {
-            //更新订单状态
-            FsStorePaymentScrm storePayment=fsStorePaymentScrmMapper.selectFsStorePaymentByPaymentCode(payCode);
-            if(!storePayment.getStatus().equals(0)){
-                return "";
+            } catch (Exception e) {
+                //更新订单状态
+                FsStorePaymentScrm storePayment=fsStorePaymentScrmMapper.selectFsStorePaymentByPaymentCode(payCode);
+                if(!storePayment.getStatus().equals(0)){
+                    return "";
+                }
+                storePayment.setStatus(1);
+                storePayment.setPayTime(new Date());
+                storePayment.setTradeNo(tradeNo);
+                storePayment.setBankSerialNo(bankSerialNo);
+                storePayment.setBankTransactionId(bankTransactionId);
+                fsStorePaymentScrmMapper.updateFsStorePayment(storePayment);
+            }
+        } finally {
+            // 释放锁
+            if (lockAcquired) {
+                redisCache.deleteObject(lockKey);
             }
-            storePayment.setStatus(1);
-            storePayment.setPayTime(new Date());
-            storePayment.setTradeNo(tradeNo);
-            storePayment.setBankSerialNo(bankSerialNo);
-            storePayment.setBankTransactionId(bankTransactionId);
-            fsStorePaymentScrmMapper.updateFsStorePayment(storePayment);
         }
         return "SUCCESS";
     }

+ 421 - 4
fs-service/src/main/java/com/fs/his/service/impl/FsStoreSubOrderServiceImpl.java

@@ -6,31 +6,39 @@ import java.math.RoundingMode;
 import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.util.*;
-import java.util.logging.Logger;
 
+import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.BeanCopyUtils;
-import com.fs.common.annotation.Log;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.dto.FsPrescribeUsageDTO;
 import com.fs.his.dto.FsStoreOrderItemDTO;
+import com.fs.his.enums.hsyy.AdministrationRoute;
+import com.fs.his.enums.hsyy.EducationLevel;
+import com.fs.his.enums.hsyy.Gender;
+import com.fs.his.enums.hsyy.ProfessionalTitle;
 import com.fs.his.mapper.*;
 import com.fs.his.param.FsStoreOrderParam;
 import com.fs.his.param.FsSubOrderDrugListParam;
+import com.fs.his.param.FsSubOrderHsyyParam;
 import com.fs.his.param.FsSubOrderParam;
 import com.fs.his.utils.ConfigUtil;
+import com.fs.his.utils.HsyyPushApiClientUtil;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.FsStoreProductAttrVO;
 import com.fs.his.vo.FsStoreSubOrderListVO;
 import com.fs.his.vo.FsStoreSubOrderVO;
 import com.fs.his.vo.FsSubOrderResultVO;
-import com.qcloud.cos.transfer.Copy;
 import org.apache.http.HttpEntity;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -69,7 +77,13 @@ public class FsStoreSubOrderServiceImpl implements IFsStoreSubOrderService
     FsStoreProductMapper fsStoreProductMapper;
     @Autowired
     ConfigUtil configUtil;
-    org.slf4j.Logger logger= LoggerFactory.getLogger(getClass());
+    Logger logger= LoggerFactory.getLogger(getClass());
+    @Autowired
+    private FsDepartmentMapper fsDepartmentMapper;
+    @Autowired
+    private FsInquiryOrderMapper fsInquiryOrderMapper;
+    @Autowired
+    private FsHospitalMapper fsHospitalMapper;
     /**
      * 查询订单
      *
@@ -649,4 +663,407 @@ public class FsStoreSubOrderServiceImpl implements IFsStoreSubOrderService
         return fsStoreSubOrderMapper.selectFsStoreSubOrderListCount(fsStoreSubOrder);
     }
 
+    @Override
+    public List<FsStoreSubOrder> selectFsStoreSubOrderListByCreateTime(String yesterdayStartStr, String todayStartStr) {
+        return fsStoreSubOrderMapper.selectFsStoreSubOrderListByCreateTime(yesterdayStartStr, todayStartStr);
+    }
+
+    /**
+     * 推送河山医院数据
+     * @param list 订单列表
+     */
+    @Override
+    public void pushHsyy(List<FsStoreSubOrder> list) {
+        SimpleDateFormat ymd = new SimpleDateFormat("yyyy-MM-dd");
+        SimpleDateFormat ymdhms = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        HsyyPushApiClientUtil hsyyPushApiClientUtil = new HsyyPushApiClientUtil();
+
+        for (FsStoreSubOrder order : list) {
+            try {
+                FsSubOrderHsyyParam param = processSingleOrder(order, ymd, ymdhms);
+                if (param == null) {
+                    continue;
+                }
+                String result = hsyyPushApiClientUtil.callApi(param);
+                handleApiResponse(order, result, param);
+            } catch (Exception e) {
+                logger.error("处理订单推送异常,订单ID: {}", order.getSubOrderId(), e);
+            }
+        }
+    }
+
+    /**
+     * 处理API响应
+     */
+    private void handleApiResponse(FsStoreSubOrder order, String result,FsSubOrderHsyyParam param) {
+        if (StringUtils.isEmpty(result)) {
+            logger.warn("订单 {} 推送返回结果为空", order.getSubOrderId());
+            return;
+        }
+
+        try {
+            result = JSON.parseObject(result, String.class);
+            // 解析响应结果
+            logger.info("订单 {} 推送响应: {}", order.getSubOrderId(), result);
+
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            Integer code = jsonObject.getInteger("code");
+            if (code != null && code == 0) {
+                logger.info("订单 {} 推送成功", order.getSubOrderId());
+            } else {
+                String errorMsg = jsonObject.getString("msg");
+                logger.warn("订单 {} 推送失败: {},推送数据: {}",
+                        order.getSubOrderId(),
+                        errorMsg,
+                        JSONObject.toJSONString(param));
+            }
+        } catch (Exception e) {
+            logger.error("解析订单 {} 推送响应异常: {}", order.getSubOrderId(), e.getMessage());
+        }
+    }
+    private FsSubOrderHsyyParam processSingleOrder(FsStoreSubOrder order, SimpleDateFormat ymd, SimpleDateFormat ymdhms) {
+        // 验证订单必要数据
+        HsyyValidationResult hsyyValidationResult = validateOrderData(order);
+        if (!hsyyValidationResult.isValid()) {
+            logger.warn("订单 {} 验证失败: {}", order.getSubOrderId(), hsyyValidationResult.getMessage());
+            return null;
+        }
+
+        // 构建参数对象
+        return buildHsyyParam(order, hsyyValidationResult, ymd, ymdhms);
+    }
+
+    /**
+     * 构建河山医院参数对象
+     */
+    private FsSubOrderHsyyParam buildHsyyParam(FsStoreSubOrder order, HsyyValidationResult validation,SimpleDateFormat ymd, SimpleDateFormat ymdhms) {
+        FsSubOrderHsyyParam param = new FsSubOrderHsyyParam();
+        param.setOrderNo(order.getSubOrderId() + "");
+
+        // 设置各个部分的数据
+        param.setUserinfo(buildUserinfo(order, validation.getPatient(), ymd, ymdhms));
+        param.setDepartment(buildDepartment(validation.getDepartment()));
+        param.setDoctor(buildDoctor(validation.getDoctor(), ymd));
+        param.setMedicine(buildMedicines(order, validation.getPrescribeDrugs(),
+                validation.getStore().getStoreId() + "", validation.getStore().getStoreName()));
+        param.setOffline(buildOffline(ymd, validation.getDoctor(), validation.getDepartment(),
+                order.getSubOrderId() + "", validation.getInquiryOrder()));
+        param.setRecipe(buildRecipe(ymdhms, validation.getDoctor(), validation.getDepartment(),
+                order.getSubOrderId(), validation.getPrescribe(),
+                validation.getStore().getStoreName(), validation.getHospital().getHospitalName()));
+        param.setSign(buildSigns(order, ymdhms, validation.getDoctor(), validation.getDrugDoctor()));
+
+        return param;
+    }
+
+    private FsSubOrderHsyyParam.UserinfoType buildUserinfo(FsStoreSubOrder order,FsPatient patient, SimpleDateFormat ymd,SimpleDateFormat ymdhms) {
+        return FsSubOrderHsyyParam.UserinfoType.builder()
+                .name(StringUtils.isNotEmpty(order.getPatientName())?order.getPatientName():"无")
+                .numcard(StringUtils.isNotEmpty(patient.getIdCard())?patient.getIdCard():"无")
+                .birthday(patient.getBirthday() != null ? ymd.format(patient.getBirthday()) : "无")
+                .sex(patient.getSex() == 1 ? Gender.MALE.getCode() + "" : Gender.FEMALE.getCode() + "")
+                .datetime(patient.getBirthday() != null ? ymd.format(patient.getBirthday()) : "")
+                .age(patient.getAge())
+                .tel(StringUtils.isNotEmpty(patient.getMobile()) ? PhoneUtil.decryptPhone(patient.getMobile()) : "无")
+                .present_illness_history("无")
+                .past_history("无")
+                .personal_history(StringUtils.isNotEmpty(patient.getSelfMedHistory())?patient.getSelfMedHistory():"无")
+                .family_history(StringUtils.isNotEmpty(patient.getFamilyMedHistory())?patient.getFamilyMedHistory():"无")
+                .allergy(StringUtils.isNotEmpty(patient.getHistoryAllergic())?patient.getHistoryAllergic():"无")
+                .tijian("无")
+                .usertime(patient.getCreateTime() != null ? ymdhms.format(patient.getCreateTime()):"")
+                .build();
+    }
+
+    private FsSubOrderHsyyParam.DepartmentType buildDepartment(FsDepartment department) {
+        return FsSubOrderHsyyParam.DepartmentType.builder()
+                .dep_name(StringUtils.isNotEmpty(department.getDeptName())?department.getDeptName():"无")
+                .dep_id(department.getDeptId() != null ? Integer.parseInt(department.getDeptId() + "") : 0)
+                .dep_code(StringUtils.isNotEmpty(department.getDeptCode())?department.getDeptCode():"0")
+                .build();
+    }
+
+    private FsSubOrderHsyyParam.DoctorType buildDoctor(FsDoctor doctor,SimpleDateFormat ymd) {
+        return FsSubOrderHsyyParam.DoctorType.builder()
+                .doc_name(StringUtils.isNotEmpty(doctor.getDoctorName())?doctor.getDoctorName():"无")
+                .doc_numcard(StringUtils.isNotEmpty(doctor.getIdCard())?doctor.getIdCard():"无")
+                .doc_sex(doctor.getSex() == 1 ? Gender.MALE.getCode() :
+                        Gender.FEMALE.getCode())
+                .doc_tel(StringUtils.isNotEmpty(doctor.getMobile()) ?
+                        doctor.getMobile() : "无")
+                .doc_zhicheng(StringUtils.isNotEmpty(doctor.getPosition()) ?
+                        ProfessionalTitle.getCodeByDescription(doctor.getPosition()) + "" : ProfessionalTitle.PHYSICIAN.getCode() +"")
+                .doc_code(doctor.getDoctorId() != null ?doctor.getDoctorId() + "" : "0")
+                .doc_type(doctor.getDoctorType() != null ?
+                        (doctor.getDoctorType() == 1 ? "1" : "3") : "1")
+                .doc_license_no(StringUtils.isNotEmpty(doctor.getCertificateCode())?doctor.getCertificateCode():"")
+                //--------虚拟数据堆begin------------
+                .doc_rank("高级")
+                .doc_license_date(doctor.getCreateTime() != null ? ymd.format(doctor.getCreateTime()) : "")
+                .doc_practice_no("")
+                .doc_practice_address("")
+                .doc_practice_category("")
+                .doc_practice_scope("")
+                .doc_first_date(doctor.getCreateTime() != null ? ymd.format(doctor.getCreateTime()) : "")
+                .doc_current_date(doctor.getCreateTime() != null ? ymd.format(doctor.getCreateTime()) : "")
+                .doc_valid_date("2099-01-01")
+                .doc_politics_status("1")
+                .doc_starting_work_date(doctor.getCreateTime() != null ? ymd.format(doctor.getCreateTime()) : "")
+                .doc_independent_work_date(doctor.getCreateTime() != null ? ymd.format(doctor.getCreateTime()) : "")
+                .doc_fullTime_degree(EducationLevel.BACHELOR.getCode() + "")
+                .doc_full_time_degree_year("2099")
+                //--------虚拟数据堆end------------
+                .build();
+    }
+
+    private List<FsSubOrderHsyyParam.MedicineType> buildMedicines(FsStoreSubOrder order,List<FsPrescribeDrug> prescribeDrugs,String storeId,String storeName) {
+        List<FsSubOrderHsyyParam.MedicineType> medicines = new ArrayList<>();
+        for (FsPrescribeDrug drug : prescribeDrugs) {
+            medicines.add(FsSubOrderHsyyParam.MedicineType.builder()
+                    .bm(drug.getProductId() != null?drug.getProductId() + "":"XA10BAE021A010011401021")
+                    .store_id(storeId)
+                    .store_name(storeName)
+                    .sname(StringUtils.isNotEmpty(drug.getDrugName()) ? drug.getDrugName() : "")
+                    .tname(StringUtils.isNotEmpty(drug.getDrugName())?drug.getDrugName():"")
+                    .price(drug.getDrugPrice() != null ? drug.getDrugPrice().multiply(BigDecimal.valueOf(100)).intValue()+"" : "0")
+                    .guige(StringUtils.isNotEmpty(drug.getDrugSpec())?drug.getDrugSpec():"无")
+                    .use(StringUtils.isNotEmpty(drug.getUsageMethod())?drug.getUsageMethod():"无")
+                    .dosage(StringUtils.isNotEmpty(drug.getUsagePerUseCount())?drug.getUsagePerUseCount():"1")
+                    .frequency(StringUtils.isNotEmpty(drug.getUsageFrequencyUnit())?drug.getUsageFrequencyUnit():"无")
+                    .unit(StringUtils.isNotEmpty(drug.getUsagePerUseUnit())?drug.getUsagePerUseUnit():"盒")
+                    .count(drug.getDrugNum() != null ? Integer.parseInt(drug.getDrugNum()+""): 0)
+                    .amount(order.getPayMoney() != null ?order.getPayMoney().multiply(BigDecimal.valueOf(100)).intValue() : 0)
+                    //--------虚拟数据堆begin------------
+                    .doc_num("国药准字H20050349")
+                    .component("")
+                    .character("")
+                    .adapt("")
+                    .adverse_reactions("")
+                    .dosageform("10")
+                    .package_spec(StringUtils.isNotEmpty(drug.getDrugSpec())?drug.getDrugSpec():"")
+                    .health_care_code("XA10BAE021A010011401021")
+                    .health_care_type("1")
+                    .is_base("0")
+                    .manufacturer("重庆科瑞南海制药有限责任公司")
+                    .has_prescription("0")
+                    .supplier("重庆科瑞南海制药有限责任公司")
+                    .medication_route(AdministrationRoute.ORAL.getCode() + "")
+                    .getType(3)
+                    .checker_name("")
+                    .dispenser_name("")
+                    //--------虚拟数据堆end------------
+                    .build());
+            }
+            return medicines;
+    }
+
+    private FsSubOrderHsyyParam.OfflineType buildOffline(SimpleDateFormat ymd,FsDoctor doctor,FsDepartment dept,String hisId,FsInquiryOrder inquiryOrder) {
+        String doctorDeptCode = doctor.getDoctorId() + "";
+        String doctorDeptName =  StringUtils.isNotEmpty(dept.getDeptName())?dept.getDeptName():"";
+        String doctorCode = StringUtils.isNotEmpty(dept.getDeptCode())?dept.getDeptCode():"";
+
+        return FsSubOrderHsyyParam.OfflineType.builder()
+                .medical_record_no(hisId)
+                .visit_no(StringUtils.isNotEmpty(inquiryOrder.getOrderSn())?Long.parseLong(inquiryOrder.getOrderSn()):0L)
+                .visit_date(inquiryOrder.getCreateTime() != null ?
+                        ymd.format(inquiryOrder.getCreateTime()) : "")
+                .offline_dept_code(doctorDeptCode)
+                .offline_dept_name(doctorDeptName)
+                .offline_doctor_code(doctorCode)
+                //--------虚拟数据堆begin------------
+                .offline_diagnosis("高血压")
+                //--------虚拟数据堆end------------
+                .build();
+    }
+
+    private FsSubOrderHsyyParam.RecipeType buildRecipe(SimpleDateFormat ymdhms,FsDoctor doctor,FsDepartment dept,Long hisId,FsPrescribe prescribe,String storeName,String hospitalName) {
+        List<FsSubOrderHsyyParam.ProcessType> process = new ArrayList<>();
+        String doctorName = doctor.getDoctorName(), doctorCode = doctor.getDoctorId() + "",
+                doctorDeptCode = dept.getDeptCode(),doctorDeptName=dept.getDeptName();
+
+        process.add(FsSubOrderHsyyParam.ProcessType
+                .builder()
+                .process_time(ymdhms.format(prescribe.getCreateTime()))
+                .role("2")
+                .content(StringUtils.isNotEmpty(prescribe.getDiagnose())?prescribe.getDiagnose():"无")
+                .build());
+        process.add(FsSubOrderHsyyParam.ProcessType
+                .builder()
+                .process_time(ymdhms.format(prescribe.getCreateTime()))
+                .role("1")
+                .content(StringUtils.isNotEmpty(prescribe.getPatientDescs())?prescribe.getPatientDescs():"无")
+                .build());
+
+        return FsSubOrderHsyyParam.RecipeType.builder()
+                .recipe_id(hisId)
+                .serial_no(StringUtils.isNotEmpty(prescribe.getPrescribeCode())?prescribe.getPrescribeCode():"")
+                .create_time(prescribe.getCreateTime() != null ? ymdhms.format(prescribe.getCreateTime()) : "")
+                .status("已开方")
+                .hospital_name(hospitalName)
+                .dept_code(doctorDeptCode)
+                .dept_name(doctorDeptName)
+                .doc_code(doctorCode)
+                .doc_name(doctorName)
+                .zhusu(StringUtils.isNotEmpty(prescribe.getPatientDescs())?prescribe.getPatientDescs():"无")
+                .diagnose(StringUtils.isNotEmpty(prescribe.getDiagnose())? prescribe.getDiagnose() : "无")
+                .Recipe_img(StringUtils.isNotEmpty(prescribe.getPrescribeImgUrl())?prescribe.getPrescribeImgUrl():"")
+                .Pharmacy(storeName)
+                .process(process)
+                //--------虚拟数据堆begin------------
+                .Consultationtype("视频")
+                .Ybtype("230128")
+                .advice("无")
+                .yijian("无")
+                .diagnose_code("M01603")
+                .recipe_type("1")
+                .getType(3)
+                .Patienttype("慢病处方")
+                //--------虚拟数据堆end------------
+                .build();
+    }
+
+    private List<FsSubOrderHsyyParam.SignType> buildSigns(FsStoreSubOrder order, SimpleDateFormat ymdhms, FsDoctor doctor,FsDoctor drugDoctor) {
+        List<FsSubOrderHsyyParam.SignType> signs = new ArrayList<>();
+        String doctorName = doctor.getDoctorName();
+        String doctorCode = doctor.getDoctorId() + "";
+        signs.add(FsSubOrderHsyyParam.SignType.builder()
+                .role("处方医生")
+                .executor(doctorName)
+                .person_code(doctorCode)
+                .exe_time(order.getCreateTime() != null ?ymdhms.format(order.getCreateTime()) : "")
+                .build());
+        signs.add(FsSubOrderHsyyParam.SignType.builder()
+                .role("药师")
+                .executor(drugDoctor.getDoctorName())
+                .person_code(drugDoctor.getDoctorId() + "")
+                .exe_time(order.getCreateTime() != null ?ymdhms.format(order.getCreateTime()) : "")
+                .build());
+        return signs;
+    }
+
+    /**
+     * 验证订单数据
+     */
+    private HsyyValidationResult validateOrderData(FsStoreSubOrder order) {
+        HsyyValidationResult result = new HsyyValidationResult();
+
+        // 验证患者信息
+        if (order.getPatientId() == null) {
+            result.setValid(false);
+            result.setMessage("缺少患者信息");
+            return result;
+        }
+        result.setPatient(fsPatientMapper.selectFsPatientByPatientId(order.getPatientId()));
+        if (result.getPatient() == null) {
+            result.setValid(false);
+            result.setMessage("患者信息不存在");
+            return result;
+        }
+
+        // 验证医生信息
+        if (order.getDoctorId() == null) {
+            result.setValid(false);
+            result.setMessage("缺少医生信息");
+            return result;
+        }
+        result.setDoctor(fsDoctorMapper.selectFsDoctorByDoctorId(order.getDoctorId()));
+        if (result.getDoctor() == null) {
+            result.setValid(false);
+            result.setMessage("医生信息不存在");
+            return result;
+        }
+
+        // 验证科室信息
+        result.setDepartment(fsDepartmentMapper.selectFsDepartmentByDeptId(result.getDoctor().getDeptId()));
+        if (result.getDepartment() == null) {
+            result.setValid(false);
+            result.setMessage("科室信息不存在");
+            return result;
+        }
+
+        // 验证线下初诊信息
+        if (order.getInquiryOrderId() == null) {
+            result.setValid(false);
+            result.setMessage("缺少线下初诊信息");
+            return result;
+        }
+        result.setInquiryOrder(fsInquiryOrderMapper.selectFsInquiryOrderByOrderId(order.getInquiryOrderId()));
+        if (result.getInquiryOrder() == null) {
+            result.setValid(false);
+            result.setMessage("线下初诊信息不存在");
+            return result;
+        }
+
+        // 验处方信息
+        if (StringUtils.isEmpty(order.getPrescribeJson())) {
+            result.setValid(false);
+            result.setMessage("缺少处方信息");
+            return result;
+        }
+        result.setPrescribe(JSON.parseObject(order.getPrescribeJson(), FsPrescribe.class));
+        if (result.getPrescribe() == null) {
+            result.setValid(false);
+            result.setMessage("处方信息解析失败");
+            return result;
+        }
+
+        // 验证药品信息
+        if (StringUtils.isEmpty(order.getPrescribeDrugJson())) {
+            result.setValid(false);
+            result.setMessage("缺少药品信息");
+            return result;
+        }
+        result.setPrescribeDrugs(JSON.parseArray(order.getPrescribeDrugJson(), FsPrescribeDrug.class));
+        if (CollectionUtil.isEmpty(result.getPrescribeDrugs())) {
+            result.setValid(false);
+            result.setMessage("药品信息解析失败");
+            return result;
+        }
+
+        // 验证医院信息
+        if (result.getDoctor().getHospitalId() == null) {
+            result.setValid(false);
+            result.setMessage("医生缺少医院信息");
+            return result;
+        }
+        result.setHospital(fsHospitalMapper.selectFsHospitalByHospitalId(result.getDoctor().getHospitalId()));
+        if (result.getHospital() == null) {
+            result.setValid(false);
+            result.setMessage("医院信息不存在");
+            return result;
+        }
+
+        // 验证签名药师信息
+        if (order.getDrugDoctorId() == null) {
+            result.setValid(false);
+            result.setMessage("缺少签名药师信息");
+            return result;
+        }
+        if (result.getPrescribe().getDrugDoctorId() == null) {
+            result.setValid(false);
+            result.setMessage("处方缺少签名药师信息");
+            return result;
+        }
+        result.setDrugDoctor(fsDoctorMapper.selectFsDoctorByDoctorId(result.getPrescribe().getDrugDoctorId()));
+        if (result.getDrugDoctor() == null) {
+            result.setValid(false);
+            result.setMessage("签名药师信息不存在");
+            return result;
+        }
+
+        // 验证门店信息
+        if (order.getStoreId() == null) {
+            result.setValid(false);
+            result.setMessage("缺少门店信息");
+            return result;
+        }
+        result.setStore(fsStoreMapper.selectFsStoreByStoreId(order.getStoreId()));
+        if (result.getStore() == null) {
+            result.setValid(false);
+            result.setMessage("门店信息不存在");
+            return result;
+        }
+        return result;
+    }
+
 }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 46 - 0
fs-service/src/main/java/com/fs/his/utils/HsyyPushApiClientUtil.java


+ 45 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductPurchaseLimitScrm.java

@@ -0,0 +1,45 @@
+package com.fs.hisStore.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 商品限购对象 fs_store_product_purchase_limit
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+@Data
+public class FsStoreProductPurchaseLimitScrm implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 限购ID */
+    private Long id;
+
+    /** 商品ID */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 已购买数量 */
+    @Excel(name = "已购买数量")
+    private Integer num;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+}
+

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

@@ -343,4 +343,8 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "所属小程序app_id")
     private String appIds;
 
+    /** 限购数量 */
+    @Excel(name = "限购数量")
+    private Integer purchaseLimit;
+
 }

+ 72 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductPurchaseLimitScrmMapper.java

@@ -0,0 +1,72 @@
+package com.fs.hisStore.mapper;
+
+import java.util.List;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 商品限购Mapper接口
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+public interface FsStoreProductPurchaseLimitScrmMapper
+{
+    /**
+     * 查询商品限购
+     *
+     * @param id 商品限购ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 查询商品限购列表
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 商品限购集合
+     */
+    public List<FsStoreProductPurchaseLimitScrm> selectFsStoreProductPurchaseLimitList(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 根据商品ID和用户ID查询限购记录
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectByProductIdAndUserId(@Param("productId") Long productId, @Param("userId") Long userId);
+
+    /**
+     * 新增商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int insertFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 修改商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int updateFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 删除商品限购
+     *
+     * @param id 商品限购ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 批量删除商品限购
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitByIds(Long[] ids);
+}
+

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

@@ -78,7 +78,7 @@ public interface FsStoreProductScrmMapper
     @Select({"<script> " +
             "select p.*,pc.cate_name, fs.store_name from fs_store_product_scrm p left join fs_store_product_category_scrm pc on p.cate_id=pc.cate_id  " +
             "left join fs_store fs on fs.store_id = p.store_id " +
-            "where 1=1 " +
+            "where 1=1 and p.is_del = 0" +
             "<if test = 'maps.productName != null and  maps.productName !=\"\"    '> " +
             "and p.product_name like CONCAT('%',#{maps.productName},'%') " +
             "</if>" +

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java

@@ -278,6 +278,9 @@ public class FsStoreProductAddEditParam implements Serializable
         // 指定企业
     private String companyIds;
 
+    /** 限购数量 */
+    private Integer purchaseLimit;
+
 
     /** 原产地 */
     @Excel(name = "原产地")

+ 91 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductPurchaseLimitScrmService.java

@@ -0,0 +1,91 @@
+package com.fs.hisStore.service;
+
+import java.util.List;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+
+/**
+ * 商品限购Service接口
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+public interface IFsStoreProductPurchaseLimitScrmService
+{
+    /**
+     * 查询商品限购
+     *
+     * @param id 商品限购ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 查询商品限购列表
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 商品限购集合
+     */
+    public List<FsStoreProductPurchaseLimitScrm> selectFsStoreProductPurchaseLimitList(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 根据商品ID和用户ID查询限购记录
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @return 商品限购
+     */
+    public FsStoreProductPurchaseLimitScrm selectByProductIdAndUserId(Long productId, Long userId);
+
+    /**
+     * 新增商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int insertFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 修改商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    public int updateFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit);
+
+    /**
+     * 批量删除商品限购
+     *
+     * @param ids 需要删除的商品限购ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitByIds(Long[] ids);
+
+    /**
+     * 删除商品限购信息
+     *
+     * @param id 商品限购ID
+     * @return 结果
+     */
+    public int deleteFsStoreProductPurchaseLimitById(Long id);
+
+    /**
+     * 增加用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @param num 增加的数量
+     * @return 结果
+     */
+    public int increasePurchaseLimit(Long productId, Long userId, Integer num);
+
+    /**
+     * 减少用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @param num 减少的数量
+     * @return 结果
+     */
+    public int decreasePurchaseLimit(Long productId, Long userId, Integer num);
+}
+

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

@@ -25,6 +25,10 @@ import com.fs.hisStore.param.FsStoreCartDelParam;
 import com.fs.hisStore.param.FsStoreCartNumParam;
 import com.fs.hisStore.param.FsStoreCartParam;
 import com.fs.hisStore.service.IFsStoreCartScrmService;
+import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.vo.FsStoreCartVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -69,6 +73,12 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
     @Autowired
     private MedicalMallConfig medicalMallConfig;
 
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
+
+    @Autowired
+    private IFsStoreProductScrmService productService;
+
 
 
     /**
@@ -147,6 +157,10 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
 
     @Override
     public R addCart(long uid, FsStoreCartParam cartParam) {
+        // 检查并调整限购数量
+        Integer adjustedNum = adjustPurchaseLimit(uid, cartParam.getProductId(), cartParam.getCartNum());
+        cartParam.setCartNum(adjustedNum);
+        
         //如果是直接购买,直接写入记录
         if(cartParam.getIsBuy()==1){
             FsStoreCartScrm storeCart = FsStoreCartScrm.builder()
@@ -192,7 +206,10 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
             }
             else{
                 storeCart=cart.get(0);
-                storeCart.setCartNum(cartParam.getCartNum() + cart.get(0).getCartNum());
+                int newCartNum = cartParam.getCartNum() + cart.get(0).getCartNum();
+                // 检查并调整限购数量(需要检查新的总数量)
+                Integer adjustedNewNum = adjustPurchaseLimit(uid, cartParam.getProductId(), newCartNum);
+                storeCart.setCartNum(adjustedNewNum);
                 storeCart.setUpdateTime(new Date());
                 checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId());
                 fsStoreCartMapper.updateFsStoreCart(storeCart);
@@ -216,6 +233,8 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
     @Override
     public R changeNum(long userId, FsStoreCartNumParam cartParam) {
         FsStoreCartScrm cart=fsStoreCartMapper.selectFsStoreCartById(cartParam.getId());
+        // 检查限购
+        checkPurchaseLimit(userId, cart.getProductId(), cartParam.getNumber());
         checkProductStock(cart.getProductId(),cart.getProductAttrValueId());
         cart.setCartNum(cartParam.getNumber());
         cart.setUpdateTime(new Date());
@@ -223,6 +242,81 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
         return R.ok();
     }
 
+    /**
+     * 检查限购(用于修改数量时,超过限购则抛出异常)
+     * @param userId 用户ID
+     * @param productId 商品ID
+     * @param num 要购买的数量
+     */
+    private void checkPurchaseLimit(Long userId, Long productId, Integer num) {
+        // 查询商品信息
+        FsStoreProductScrm product = productService.selectFsStoreProductById(productId);
+        if (product == null) {
+            return;
+        }
+        
+        // 如果商品没有设置限购,直接返回
+        if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+            return;
+        }
+        
+        // 查询用户已购买的数量
+        FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(productId, userId);
+        int purchasedNum = 0;
+        if (purchaseLimit != null) {
+            purchasedNum = purchaseLimit.getNum();
+        }
+        
+        // 检查是否超过限购数量
+        if (purchasedNum + num > product.getPurchaseLimit()) {
+            int productTotalNum = purchasedNum + num;
+            int maxAllowed = product.getPurchaseLimit() - (productTotalNum);
+            if (maxAllowed <= 0) {
+                throw new CustomException("该商品已达到限购数量,无法继续购买");
+            }
+            throw new CustomException("该商品限购" + product.getPurchaseLimit() + "件,您已购买" + productTotalNum + "件,最多还能购买" + maxAllowed + "件");
+        }
+    }
+
+    /**
+     * 调整限购数量(用于添加购物车时,超过限购则调整为最大可购买数量)
+     * @param userId 用户ID
+     * @param productId 商品ID
+     * @param num 要购买的数量
+     * @return 调整后的数量
+     */
+    private Integer adjustPurchaseLimit(Long userId, Long productId, Integer num) {
+        // 查询商品信息
+        FsStoreProductScrm product = productService.selectFsStoreProductById(productId);
+        if (product == null) {
+            return num;
+        }
+        
+        // 如果商品没有设置限购,直接返回原数量
+        if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+            return num;
+        }
+        
+        // 查询用户已购买的数量
+        FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(productId, userId);
+        int purchasedNum = 0;
+        if (purchaseLimit != null) {
+            purchasedNum = purchaseLimit.getNum();
+        }
+        
+        // 检查是否超过限购数量
+        if (purchasedNum + num > product.getPurchaseLimit()) {
+            // 如果超过限购,设置数量为限购的最大数量
+            int maxAllowed = product.getPurchaseLimit() - purchasedNum;
+            if (maxAllowed <= 0) {
+                return 0; // 已达到限购,返回0
+            }
+            return maxAllowed; // 返回最大可购买数量
+        }
+        
+        return num; // 未超过限购,返回原数量
+    }
+
     @Override
     public void checkProductStock(Long productId, Long productAttrValueId) {
         IErpGoodsService goodsService = getErpService();

+ 72 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -347,6 +347,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     //fsStoreMapper
     private FsStoreScrmMapper fsStoreMapper;
 
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
+
     @Autowired
     private IFsUserWatchService fsUserWatchService;
 
@@ -1059,6 +1062,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             List<FsStoreOrderItemScrm> listOrderItem = new ArrayList<>();
             //保存购物车商品信息
             for (FsStoreCartQueryVO vo : carts) {
+                // 检查限购
+                checkAndRecordPurchaseLimit(userId, vo.getProductId(), vo.getCartNum());
+                
                 FsStoreCartDTO fsStoreCartDTO = new FsStoreCartDTO();
                 fsStoreCartDTO.setProductId(vo.getProductId());
                 fsStoreCartDTO.setPrice(vo.getPrice());
@@ -1090,6 +1096,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 }
                 fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
                 listOrderItem.add(item);
+                
+                // 记录限购数量(订单创建成功后记录)
+                FsStoreProductScrm product = productService.selectFsStoreProductById(vo.getProductId());
+                if (product != null && product.getPurchaseLimit() != null && product.getPurchaseLimit() > 0) {
+                    purchaseLimitService.increasePurchaseLimit(vo.getProductId(), userId, vo.getCartNum());
+                }
             }
             if (listOrderItem.size() > 0) {
                 String itemJson = JSONUtil.toJsonStr(listOrderItem);
@@ -1285,6 +1297,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             this.refundCoupon(order);
             //退回库存
             this.refundStock(order);
+            // 删除限购记录
+            this.deletePurchaseLimitRecords(order);
             fsStoreOrderMapper.cancelOrder(orderId);
             //添加记录
             orderStatusService.create(order.getId(), OrderLogEnum.CANCEL_ORDER.getValue(),
@@ -3145,6 +3159,64 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     /**
      * 退回库存
      */
+    /**
+     * 检查并记录限购
+     * @param userId 用户ID
+     * @param productId 商品ID
+     * @param num 购买数量
+     */
+    private void checkAndRecordPurchaseLimit(Long userId, Long productId, Integer num) {
+        // 查询商品信息
+        FsStoreProductScrm product = productService.selectFsStoreProductById(productId);
+        if (product == null) {
+            return;
+        }
+        
+        // 如果商品没有设置限购,直接返回
+        if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+            return;
+        }
+        
+        // 查询用户已购买的数量
+        FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(productId, userId);
+        int purchasedNum = 0;
+        if (purchaseLimit != null) {
+            purchasedNum = purchaseLimit.getNum();
+        }
+        
+        // 检查是否超过限购数量
+        if (purchasedNum + num > product.getPurchaseLimit()) {
+            int productTotalNum = purchasedNum + num;
+            throw new CustomException("该商品限购" + product.getPurchaseLimit() + "件,您已购买" + productTotalNum + "件,无法继续购买");
+        }
+        
+        // 记录限购数量(在订单创建成功后记录,这里先检查)
+    }
+
+    /**
+     * 删除限购记录
+     * @param order 订单
+     */
+    private void deletePurchaseLimitRecords(FsStoreOrderScrm order) {
+        // 获取订单下的商品
+        List<FsStoreOrderItemVO> orderItemVOS = fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+        for (FsStoreOrderItemVO vo : orderItemVOS) {
+            // 查询商品信息
+            FsStoreProductScrm product = productService.selectFsStoreProductById(vo.getProductId());
+            if (product == null) {
+                continue;
+            }
+            
+            // 如果商品没有设置限购,跳过
+            if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+                continue;
+            }
+            
+            // 减少限购数量
+            purchaseLimitService.decreasePurchaseLimit(vo.getProductId(), order.getUserId(), Math.toIntExact(vo.getNum()));
+        }
+    }
+
     private void refundStock(FsStoreOrderScrm order) {
         //获取订单下的商品
         List<FsStoreOrderItemVO> orderItemVOS = fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderId(order.getId());

+ 163 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductPurchaseLimitScrmServiceImpl.java

@@ -0,0 +1,163 @@
+package com.fs.hisStore.service.impl;
+
+import java.util.Date;
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
+import com.fs.hisStore.mapper.FsStoreProductPurchaseLimitScrmMapper;
+import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 商品限购Service业务层处理
+ *
+ * @author fs
+ * @date 2024-01-01
+ */
+@Service
+public class FsStoreProductPurchaseLimitScrmServiceImpl implements IFsStoreProductPurchaseLimitScrmService
+{
+    @Autowired
+    private FsStoreProductPurchaseLimitScrmMapper fsStoreProductPurchaseLimitMapper;
+
+    /**
+     * 查询商品限购
+     *
+     * @param id 商品限购ID
+     * @return 商品限购
+     */
+    @Override
+    public FsStoreProductPurchaseLimitScrm selectFsStoreProductPurchaseLimitById(Long id)
+    {
+        return fsStoreProductPurchaseLimitMapper.selectFsStoreProductPurchaseLimitById(id);
+    }
+
+    /**
+     * 查询商品限购列表
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 商品限购
+     */
+    @Override
+    public List<FsStoreProductPurchaseLimitScrm> selectFsStoreProductPurchaseLimitList(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit)
+    {
+        return fsStoreProductPurchaseLimitMapper.selectFsStoreProductPurchaseLimitList(fsStoreProductPurchaseLimit);
+    }
+
+    /**
+     * 根据商品ID和用户ID查询限购记录
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @return 商品限购
+     */
+    @Override
+    public FsStoreProductPurchaseLimitScrm selectByProductIdAndUserId(Long productId, Long userId)
+    {
+        return fsStoreProductPurchaseLimitMapper.selectByProductIdAndUserId(productId, userId);
+    }
+
+    /**
+     * 新增商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    @Override
+    public int insertFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit)
+    {
+        fsStoreProductPurchaseLimit.setCreateTime(DateUtils.getNowDate());
+        return fsStoreProductPurchaseLimitMapper.insertFsStoreProductPurchaseLimit(fsStoreProductPurchaseLimit);
+    }
+
+    /**
+     * 修改商品限购
+     *
+     * @param fsStoreProductPurchaseLimit 商品限购
+     * @return 结果
+     */
+    @Override
+    public int updateFsStoreProductPurchaseLimit(FsStoreProductPurchaseLimitScrm fsStoreProductPurchaseLimit)
+    {
+        fsStoreProductPurchaseLimit.setUpdateTime(DateUtils.getNowDate());
+        return fsStoreProductPurchaseLimitMapper.updateFsStoreProductPurchaseLimit(fsStoreProductPurchaseLimit);
+    }
+
+    /**
+     * 批量删除商品限购
+     *
+     * @param ids 需要删除的商品限购ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductPurchaseLimitByIds(Long[] ids)
+    {
+        return fsStoreProductPurchaseLimitMapper.deleteFsStoreProductPurchaseLimitByIds(ids);
+    }
+
+    /**
+     * 删除商品限购信息
+     *
+     * @param id 商品限购ID
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductPurchaseLimitById(Long id)
+    {
+        return fsStoreProductPurchaseLimitMapper.deleteFsStoreProductPurchaseLimitById(id);
+    }
+
+    /**
+     * 增加用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @param num 增加的数量
+     * @return 结果
+     */
+    @Override
+    public int increasePurchaseLimit(Long productId, Long userId, Integer num)
+    {
+        FsStoreProductPurchaseLimitScrm limit = selectByProductIdAndUserId(productId, userId);
+        if (limit == null) {
+            // 创建新记录
+            limit = new FsStoreProductPurchaseLimitScrm();
+            limit.setProductId(productId);
+            limit.setUserId(userId);
+            limit.setNum(num);
+            return insertFsStoreProductPurchaseLimit(limit);
+        } else {
+            // 更新现有记录
+            limit.setNum(limit.getNum() + num);
+            return updateFsStoreProductPurchaseLimit(limit);
+        }
+    }
+
+    /**
+     * 减少用户限购数量
+     *
+     * @param productId 商品ID
+     * @param userId 用户ID
+     * @param num 减少的数量
+     * @return 结果
+     */
+    @Override
+    public int decreasePurchaseLimit(Long productId, Long userId, Integer num)
+    {
+        FsStoreProductPurchaseLimitScrm limit = selectByProductIdAndUserId(productId, userId);
+        if (limit != null) {
+            int newNum = limit.getNum() - num;
+            if (newNum <= 0) {
+                // 如果数量为0或负数,删除记录
+                return deleteFsStoreProductPurchaseLimitById(limit.getId());
+            } else {
+                // 更新数量
+                limit.setNum(newNum);
+                return updateFsStoreProductPurchaseLimit(limit);
+            }
+        }
+        return 0;
+    }
+}
+

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -661,6 +661,12 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         product.setVideo(param.getVideo());
         product.setStoreId(param.getStoreId());
         product.setIsDrug(param.getIsDrug().toString());
+        // 处理限购字段:如果不为null并且大于0,保存限购数字
+        if (param.getPurchaseLimit() != null && param.getPurchaseLimit() > 0) {
+            product.setPurchaseLimit(param.getPurchaseLimit());
+        } else {
+            product.setPurchaseLimit(0);
+        }
         //校验店铺资质信息
         if (!CompanyEnum.contains(cloudHostProper.getCompanyName())) {
             //获取店铺

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductQueryVO.java

@@ -139,4 +139,7 @@ public class FsStoreProductQueryVO implements Serializable
 
     private String storeId;
 
+    /** 限购数量 */
+    private Integer purchaseLimit;
+
 }

+ 105 - 9
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -80,6 +80,8 @@ import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import com.fs.hisStore.mapper.FsUserScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
+import com.fs.hisStore.service.IFsStoreProductPurchaseLimitScrmService;
+import com.fs.hisStore.domain.FsStoreProductPurchaseLimitScrm;
 import com.fs.hisStore.vo.*;
 import com.fs.huifuPay.domain.HuiFuCreateOrder;
 import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
@@ -203,6 +205,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private LiveUserLotteryRecordMapper liveUserLotteryRecordMapper;
 
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
+
     @Autowired
     ICompanyUserService companyUserService;
 
@@ -697,8 +702,27 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Override
     @Transactional
     public String payConfirm(Integer type,Long orderId,String payCode,String tradeNo,String bankTransactionId,String bankSerialNo) {
+        // 使用 Redis setNx 加分布式锁,基于订单ID或支付单号
+        String lockKey;
+        if (type.equals(1) && StringUtils.isNotEmpty(payCode)) {
+            lockKey = "livePayConfirm:lock:" + payCode;
+        } else if (orderId != null) {
+            lockKey = "livePayConfirm:lock:" + orderId;
+        } else {
+            lockKey = "livePayConfirm:lock:" + System.currentTimeMillis();
+        }
+        
+        boolean lockAcquired = false;
         Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
         try {
+            // 尝试获取锁,锁过期时间设置为30秒
+            lockAcquired = redisCache.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+            if (!lockAcquired) {
+                // 如果获取锁失败,说明有其他线程正在处理该订单,直接返回
+                log.info("支付确认处理中,订单ID: {}, 支付单号: {}", orderId, payCode);
+                return "SUCCESS";
+            }
+
             LiveOrder order=null;
             if(type.equals(1)){
                 LiveOrderPayment storePayment = liveOrderPaymentMapper.selectLiveOrderPaymentByPaymentCode(payCode);
@@ -777,6 +801,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             err.setMsg("支付错误:"+e.getMessage());
             err.setCreateTime(DateUtils.getNowDate());
             liveOrderPaymentErrorMapper.insertLiveOrderPaymentError(err);
+        } finally {
+            // 释放锁
+            if (lockAcquired) {
+                redisCache.deleteObject(lockKey);
+            }
         }
         return "SUCCESS";
     }
@@ -1161,15 +1190,15 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
 
 
             //模板消息支付成功发布事件
-            TemplateBean templateBean = TemplateBean.builder()
-                    .orderId(order.getOrderId().toString())
-                    .orderCode(order.getOrderCode().toString())
-                    .remark("您的订单已签收成功")
-                    .finishTime(order.getFinishTime())
-                    .userId(Long.valueOf(order.getUserId()))
-                    .templateType(TemplateListenEnum.TYPE_3.getValue())
-                    .build();
-            publisher.publishEvent(new TemplateEvent(this, templateBean));
+//            TemplateBean templateBean = TemplateBean.builder()
+//                    .orderId(order.getOrderId().toString())
+//                    .orderCode(order.getOrderCode().toString())
+//                    .remark("您的订单已签收成功")
+//                    .finishTime(order.getFinishTime())
+//                    .userId(Long.valueOf(order.getUserId()))
+//                    .templateType(TemplateListenEnum.TYPE_3.getValue())
+//                    .build();
+//            publisher.publishEvent(new TemplateEvent(this, templateBean));
             return R.ok("操作成功");
         } else {
             return R.error("非法操作");
@@ -3659,6 +3688,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         if(goods.getStock() == null) return R.error("直播间商品库存不足");
         if(fsStoreProduct.getStock() < Integer.parseInt(liveOrder.getTotalNum()) || goods.getStock() < Integer.parseInt(liveOrder.getTotalNum())) return R.error("抱歉,这款商品已被抢光,暂时无库存~");
 
+        // 检查限购
+        Long userId = Long.parseLong(liveOrder.getUserId());
+        Integer purchaseNum = Integer.parseInt(liveOrder.getTotalNum());
+        checkPurchaseLimitForLiveOrder(userId, liveOrder.getProductId(), purchaseNum);
+
         FsStoreProductAttrValueScrm attrValue = null;
         if (!Objects.isNull(liveOrder.getAttrValueId())) {
             attrValue = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueById(liveOrder.getAttrValueId());
@@ -3773,6 +3807,12 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 liveOrderItem.setNum(Long.valueOf(liveOrder.getTotalNum()));
                 liveOrderItem.setJsonInfo(JSON.toJSONString(dto));
                 liveOrderItemMapper.insertLiveOrderItem(liveOrderItem);
+                
+                // 记录限购数量(订单创建成功后记录)
+                if (fsStoreProduct.getPurchaseLimit() != null && fsStoreProduct.getPurchaseLimit() > 0) {
+                    purchaseLimitService.increasePurchaseLimit(liveOrder.getProductId(), userId, purchaseNum);
+                }
+                
                 redisCache.deleteObject("orderKey:" + liveOrder.getOrderKey());
                 //添加支付到期时间
                 Calendar calendar = Calendar.getInstance();
@@ -3943,6 +3983,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             goods.setStock(goods.getStock()+Long.parseLong(liveOrder.getTotalNum()));
             // 更新商品库存
             liveGoodsMapper.updateLiveGoods(goods);
+            // 删除限购记录
+            deletePurchaseLimitRecordsForLiveOrder(liveOrder);
             // 退券
             this.refundCoupon(order);
             TemplateBean templateBean = TemplateBean.builder()
@@ -3960,6 +4002,60 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
     }
 
+    /**
+     * 检查限购(用于直播订单)
+     * @param userId 用户ID
+     * @param productId 商品ID
+     * @param num 购买数量
+     */
+    private void checkPurchaseLimitForLiveOrder(Long userId, Long productId, Integer num) {
+        // 查询商品信息
+        FsStoreProductScrm product = fsStoreProductService.selectFsStoreProductById(productId);
+        if (product == null) {
+            return;
+        }
+        
+        // 如果商品没有设置限购,直接返回
+        if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+            return;
+        }
+        
+        // 查询用户已购买的数量
+        FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(productId, userId);
+        int purchasedNum = 0;
+        if (purchaseLimit != null) {
+            purchasedNum = purchaseLimit.getNum();
+        }
+        
+        // 检查是否超过限购数量
+        if (purchasedNum + num > product.getPurchaseLimit()) {
+            int productTotalNum = purchasedNum + num;
+            throw new CustomException("该商品限购" + product.getPurchaseLimit() + "件,您已购买" + productTotalNum + "件,无法继续购买");
+        }
+    }
+
+    /**
+     * 删除限购记录(用于直播订单)
+     * @param liveOrder 订单
+     */
+    private void deletePurchaseLimitRecordsForLiveOrder(LiveOrder liveOrder) {
+        // 查询商品信息
+        FsStoreProductScrm product = fsStoreProductService.selectFsStoreProductById(liveOrder.getProductId());
+        if (product == null) {
+            return;
+        }
+        
+        // 如果商品没有设置限购,跳过
+        if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
+            return;
+        }
+        
+        // 减少限购数量
+        Long userId = Long.parseLong(liveOrder.getUserId());
+        Integer num = Integer.parseInt(liveOrder.getTotalNum());
+        purchaseLimitService.decreasePurchaseLimit(liveOrder.getProductId(), userId, num);
+    }
+
     private void refundCoupon(LiveOrder order) {
         if(order.getCouponUserId()!=null){
             LiveCouponUser couponUser=liveCouponUserService.selectLiveCouponUserById(order.getUserCouponId());

+ 4 - 4
fs-service/src/main/java/com/fs/live/vo/LiveAfterSalesVo.java

@@ -143,11 +143,11 @@ public class LiveAfterSalesVo {
     /** 创建时间 */
     @Excel(name = "下单开始时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private Date createTimeBegin;
+    private String createTimeBegin;
     /** 创建时间 */
     @Excel(name = "下单结束时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private Date createTimeEnd;
+    private String createTimeEnd;
 
 
 
@@ -160,10 +160,10 @@ public class LiveAfterSalesVo {
 
     @Excel(name ="产品名称")
     private String productName;
-    
+
     /** 产品名称查询参数(用于搜索) */
     private String productNameQuery;
-    
+
     @Excel(name ="产品编码")
     private String productBarCode;
     @Excel(name ="规格")

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

@@ -304,7 +304,7 @@ public interface QwUserMapper extends BaseMapper<QwUser>
     @Select("select corp_id as dictValue,corp_name as dictLabel,corp_id,corp_name from qw_company where FIND_IN_SET(#{companyId},company_ids)")
     List<QwOptionsVO> selectQwCompanyListOptionsVOByCompanyId(Long companyId);
 
-    @Select("select corp_id as dictValue,corp_name as dictLabel from qw_company where status=1")
+    @Select("select corp_id as dictValue,corp_name as dictLabel,corp_id,corp_name from qw_company where status=1")
     List<QwOptionsVO> selectQwCompanyListOptionsVOAll();
 
     @Select("select  *  from qw_user where qw_hook_id=#{qwHookId} ")

+ 2 - 1
fs-service/src/main/java/com/fs/qw/param/QwMaterialParam.java

@@ -64,7 +64,8 @@ public class QwMaterialParam extends BaseEntity implements Serializable
     /** 创建人id */
     @Excel(name = "创建人id")
     private Long createUserId;
-    private Long createName;
+
+    private String createName;
 
     /** 发送次数 */
     @Excel(name = "发送次数")

+ 2 - 0
fs-service/src/main/java/com/fs/qw/param/sidebar/TagGroupListParam.java

@@ -20,5 +20,7 @@ public class TagGroupListParam {
     @ApiModelProperty(value = "标签名称")
     private String tagName;
 
+    private String corpId;
+
 
 }

+ 49 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -5795,7 +5795,55 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(param.getQwExternalContactId());
         if(qwExternalContact != null){
             qwExternalContact.setTagIds(JSONUtil.toJsonStr(param.getTagIds()));
-            return qwExternalContactMapper.updateQwExternalContact(qwExternalContact);
+            LocalDate currentDate = LocalDate.now();
+            // 获取当前系统时间 (HH:mm)
+            LocalTime localTime = LocalTime.now();
+
+            QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+            qwEditUserTagParam.setAdd_tag(param.getTagIds());
+            qwEditUserTagParam.setUserid(qwExternalContact.getUserId());
+            qwEditUserTagParam.setExternal_userid(qwExternalContact.getExternalUserId());
+
+            QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, qwExternalContact.getCorpId());
+            if (qwResult.getErrcode() == 0) {
+                // 处理标签
+                String tagIds = qwExternalContact.getTagIds();
+                Set<String> uniqueIds = new HashSet<>();
+
+                if (tagIds != null && !tagIds.isEmpty()) {
+                    List<String> parsedTags = JSON.parseArray(tagIds, String.class);
+                    if (parsedTags != null && !parsedTags.isEmpty()) {
+                        uniqueIds.addAll(parsedTags);
+                    }
+                }
+
+                if (param.getTagIds() != null && !param.getTagIds().isEmpty()) {
+                    uniqueIds.addAll(param.getTagIds());
+                }
+
+                QwExternalContact qwExternal = new QwExternalContact();
+                qwExternal.setTagIds(JSON.toJSONString(uniqueIds));
+                qwExternal.setId(qwExternalContact.getId());
+
+                List<String> tagIdsList = new ArrayList<>();
+                if (qwExternal.getTagIds() != null && !qwExternal.getTagIds().isEmpty()) {
+                    tagIdsList = JSON.parseArray(qwExternal.getTagIds(), String.class);
+                }
+
+                logger.info("侧边栏客户修改标签addUserTag:" + qwExternalContact.getName() +
+                        "|公司" + qwExternalContact.getCorpId() +
+                        "|员工" + qwExternalContact.getUserId() +
+                        "|总标签" + tagIdsList);
+
+                // 插件sop处理
+                processTagsAll(qwExternalContact, qwExternalContact.getCorpId(),
+                        tagIdsList, currentDate, localTime);
+
+                return qwExternalContactMapper.updateQwExternalContact(qwExternalContact);
+
+            } else {
+               return 0;
+            }
         }
         return 0;
     }

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwTagGroupServiceImpl.java

@@ -263,7 +263,7 @@ public class QwTagGroupServiceImpl implements IQwTagGroupService {
         for (QwTagGroupListVO qwTagGroupListVO : vo) {
             QwTag qwTag = new QwTag();
             qwTag.setGroupId(qwTagGroupListVO.getGroupId());
-            qwTag.setCompanyId(qwTagGroupListVO.getCompanyId());
+            qwTag.setCorpId(qwTagGroupListVO.getCorpId());
             qwTag.setName(qwTagGroup.getName());
             List<QwTagVO> qwTags = qwTagMapper.selectQwTagListVO(qwTag);
             qwTagGroupListVO.setTag(qwTags);

+ 2 - 2
fs-service/src/main/resources/application-config-druid-bjzm-test.yml

@@ -15,8 +15,8 @@ logging:
 wx:
   miniapp:
     configs:
-      - appid: wxd791d5933ed42218   #北京卓美
-        secret: 3d2e220de33d67aeb2140edeec692b73 #北京卓美
+      - appid: wxe67df00c8a1e6bed   #北京卓美
+        secret: 7ded976d7aa1901cf5e73e8da70fb37d #北京卓美
         token: cbnd7lJvkripVOpyTFAna6NAWCxCrvC
         aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
         msgDataFormat: JSON

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

@@ -166,7 +166,7 @@ openIM:
     url: https://web.im.ya.top/api
 #是否使用新im
 im:
-    type: OPENIM
+    type: NONE
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: false
 enableRedPackAccount: 1

+ 35 - 7
fs-service/src/main/resources/mapper/company/CompanyMoneyLogsMapper.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.CompanyMoneyLogsMapper">
-    
+
     <resultMap type="CompanyMoneyLogs" id="CompanyMoneyLogsResult">
         <result property="logsId"    column="logs_id"    />
         <result property="companyId"    column="company_id"    />
@@ -13,20 +13,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="createTime"    column="create_time"    />
         <result property="logsType"    column="logs_type"    />
         <result property="businessId"    column="business_id"    />
+        <result property="type"    column="type"    />
     </resultMap>
 
     <sql id="selectCompanyMoneyLogsVo">
-        select logs_id, company_id, money,balance, remark, create_time,logs_type,business_id from company_money_logs
+        select logs_id, company_id, money,balance, remark, create_time,logs_type,business_id,type from company_money_logs
     </sql>
 
     <select id="selectCompanyMoneyLogsList" parameterType="CompanyMoneyLogs" resultMap="CompanyMoneyLogsResult">
         <include refid="selectCompanyMoneyLogsVo"/>
-        <where>  
+        <where>
             <if test="companyId != null "> and company_id = #{companyId}</if>
             <if test="money != null "> and money = #{money}</if>
         </where>
     </select>
-    
+
     <select id="selectCompanyMoneyLogsById" parameterType="Long" resultMap="CompanyMoneyLogsResult">
         <include refid="selectCompanyMoneyLogsVo"/>
         where logs_id = #{logsId}
@@ -47,6 +48,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         GROUP BY l.company_user_id
         order by c.company_id,u.user_id
     </select>
+    <!-- 批量更新公司金额日志 -->
+    <update id="batchUpdateCompanyMoneyLogs" parameterType="java.util.List">
+        update company_money_logs set
+        money = CASE logs_id
+        <foreach collection="list" item="log" separator=" ">
+            WHEN #{log.logsId} THEN #{log.money}
+        </foreach>
+        END,
+        balance = CASE logs_id
+        <foreach collection="list" item="log" separator=" ">
+            WHEN #{log.logsId} THEN #{log.balance}
+        </foreach>
+        END,
+        remark = CASE logs_id
+        <foreach collection="list" item="log" separator=" ">
+            WHEN #{log.logsId} THEN #{log.remark}
+        </foreach>
+        END
+        where logs_id in
+        <foreach collection="list" index="index" item="log" separator="," open="(" close=")">
+            #{log.logsId}
+        </foreach>
+    </update>
 
     <insert id="insertCompanyMoneyLogs" parameterType="CompanyMoneyLogs" useGeneratedKeys="true" keyProperty="logsId">
         insert into company_money_logs
@@ -58,6 +82,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time,</if>
             <if test="logsType != null">logs_type,</if>
             <if test="businessId != null">business_id,</if>
+            <if test="type != null">type,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
@@ -67,6 +92,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">#{createTime},</if>
             <if test="logsType != null">#{logsType},</if>
             <if test="businessId != null">#{businessId},</if>
+            <if test="type != null">#{type},</if>
          </trim>
     </insert>
 
@@ -80,6 +106,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="logsType != null">logs_type = #{logsType},</if>
             <if test="businessId != null">business_id = #{businessId},</if>
+            <if test="type != null">business_id = #{type},</if>
+
         </trim>
         where logs_id = #{logsId}
     </update>
@@ -89,10 +117,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteCompanyMoneyLogsByIds" parameterType="String">
-        delete from company_money_logs where logs_id in 
+        delete from company_money_logs where logs_id in
         <foreach item="logsId" collection="array" open="(" separator="," close=")">
             #{logsId}
         </foreach>
     </delete>
-    
-</mapper>
+
+</mapper>

+ 198 - 0
fs-service/src/main/resources/mapper/course/FinishCourseStatisticsSyncMapper.xml

@@ -0,0 +1,198 @@
+<?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.course.mapper.FinishCourseStatisticsSyncMapper">
+    <!-- 公司维度统计 -->
+    <select id="selectCompanyStatistics" resultType="map">
+        SELECT
+            fwl.company_id,
+            NULL as course_id,
+            NULL as video_id,
+            'company' as dimension_type,
+            COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END) AS finished_count,
+            COUNT(CASE WHEN fwl.log_type = 2 THEN fwl.log_id END) AS course_complete_times,
+            COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END) AS access_count,
+            IFNULL(
+                    ROUND(
+                            (COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END) /
+                             NULLIF(COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END), 0)) * 100,
+                            2
+                    ),
+                    0
+            ) AS finish_rate
+        FROM fs_course_watch_log fwl
+        WHERE fwl.send_type = 2
+        AND fwl.create_time &gt;= #{startTime}
+        AND fwl.create_time &lt; #{endTime}
+        GROUP BY fwl.company_id
+    </select>
+
+    <!-- 课程维度统计 -->
+    <select id="selectCourseStatistics" resultType="map">
+        SELECT
+            NULL as company_id,
+            fwl.course_id,
+            NULL as video_id,
+            'course' as dimension_type,
+            COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END) AS finished_count,
+            COUNT(CASE WHEN fwl.log_type = 2 THEN fwl.log_id END) AS course_complete_times,
+            COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END) AS access_count,
+            IFNULL(
+                    ROUND(
+                            (COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END) /
+                             NULLIF(COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END), 0)) * 100,
+                            2
+                    ),
+                    0
+            ) AS finish_rate
+        FROM fs_course_watch_log fwl
+        WHERE fwl.send_type = 2
+          AND fwl.create_time &gt;= #{startTime}
+          AND fwl.create_time &lt; #{endTime}
+        GROUP BY fwl.course_id
+    </select>
+
+    <!-- 小节维度统计 -->
+    <select id="selectVideoStatistics" resultType="map">
+        SELECT
+            NULL as company_id,
+            NULL as course_id,
+            fwl.video_id,
+            'video' as dimension_type,
+            COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END) AS finished_count,
+            COUNT(CASE WHEN fwl.log_type = 2 THEN fwl.log_id END) AS course_complete_times,
+            COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END) AS access_count,
+            IFNULL(
+                    ROUND(
+                            (COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END) /
+                             NULLIF(COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END), 0)) * 100,
+                            2
+                    ),
+                    0
+            ) AS finish_rate
+        FROM fs_course_watch_log fwl
+        WHERE fwl.send_type = 2
+          AND fwl.create_time &gt;= #{startTime}
+          AND fwl.create_time &lt; #{endTime}
+        GROUP BY fwl.video_id
+    </select>
+
+    <!-- 批量插入 -->
+    <insert id="batchInsertStatistics" parameterType="list">
+        INSERT INTO fs_finish_course_statistics_sync (
+        company_id, course_id, video_id, dimension_type,
+        stat_date, finished_count, course_complete_times, access_count,
+        finish_rate, sync_type, sync_time, create_time, update_time
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.companyId}, #{item.courseId}, #{item.videoId}, #{item.dimensionType},
+            #{item.statDate}, #{item.finishedCount}, #{item.courseCompleteTimes}, #{item.accessCount},
+            #{item.finishRate}, #{item.syncType}, #{item.syncTime},
+            #{item.createTime}, #{item.updateTime}
+            )
+        </foreach>
+        ON DUPLICATE KEY UPDATE
+        finished_count = VALUES(finished_count),
+        course_complete_times = VALUES(course_complete_times),
+        access_count = VALUES(access_count),
+        finish_rate = VALUES(finish_rate),
+        update_time = VALUES(update_time),
+        sync_time = VALUES(sync_time)
+    </insert>
+
+    <!-- 删除指定维度数据 -->
+    <delete id="deleteByDimension">
+        DELETE FROM fs_finish_course_statistics_sync
+        WHERE stat_date = #{statDate}
+          AND dimension_type = #{dimensionType}
+    </delete>
+
+    <!-- 检查维度数据是否存在 -->
+    <select id="checkDimensionExists" resultType="int">
+        SELECT COUNT(1)
+        FROM fs_finish_course_statistics_sync
+        WHERE stat_date = #{statDate}
+          AND dimension_type = #{dimensionType}
+    </select>
+
+    <!-- 清理旧数据 -->
+    <delete id="cleanOldData">
+        DELETE FROM fs_finish_course_statistics_sync
+        WHERE stat_date &lt; DATE_SUB(CURDATE(), INTERVAL #{keepDays} DAY)
+          AND sync_type = 'DAILY'
+    </delete>
+
+    <!-- 获取数据日期范围 -->
+    <select id="selectDateRange" resultType="map">
+        SELECT
+            MIN(DATE(create_time)) as start_date,
+            MAX(DATE(create_time)) as end_date
+        FROM fs_course_watch_log
+        WHERE send_type = 2
+    </select>
+
+    <!-- 查询未同步的日期 -->
+    <select id="selectUnsyncedDates" resultType="java.util.Date">
+        SELECT DISTINCT DATE(create_time) as stat_date
+        FROM fs_course_watch_log
+        WHERE send_type = 2
+          AND DATE(create_time) BETWEEN #{startDate} AND #{endDate}
+          AND DATE(create_time) NOT IN (
+            SELECT DISTINCT stat_date
+            FROM fs_finish_course_statistics_sync
+            WHERE sync_type = 'DAILY'
+          AND dimension_type = 'COMPANY'
+            )
+        ORDER BY stat_date
+    </select>
+    <select id="querySimpleStatistics" resultType="com.fs.course.vo.FsCourseReportVO">
+        SELECT
+        <choose>
+            <when test="dimensionType == 'company'">
+              company_id AS companyId,
+            </when>
+            <when test="dimensionType == 'course'">
+              course_id As courseId,
+            </when>
+            <when test="dimensionType == 'video'">
+              video_id AS videoId,
+            </when>
+        </choose>
+        SUM(access_count) AS accessCount,
+        SUM(finished_count) AS finishedCount,
+        SUM(course_complete_times) AS courseCompleteTimes,
+        ROUND(
+        IF(SUM(access_count) > 0,
+        SUM(finished_count) * 100.0 / SUM(access_count),
+        0),
+        2
+        ) AS finishRate
+
+        FROM `fs_his`.`fs_finish_course_statistics_sync`
+        WHERE dimension_type LIKE CONCAT('%', #{dimensionType}, '%')
+        <if test="startDate != null and startDate != ''">
+            AND stat_date &gt;= #{startDate}
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND stat_date  &lt;= #{endDate}
+        </if>
+        <if test="companyId != null">
+            AND company_id = #{companyId}
+        </if>
+        <if test="courseId != null">
+            AND course_id = #{courseId}
+        </if>
+        <if test="videoId != null">
+            AND video_id = #{videoId}
+        </if>
+        GROUP BY
+        <choose>
+            <when test="dimensionType == 'company'">company_id</when>
+            <when test="dimensionType == 'course'">course_id</when>
+            <otherwise>video_id</otherwise>
+        </choose>
+        ORDER BY accessCount desc
+    </select>
+
+</mapper>

+ 72 - 1
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -2,7 +2,7 @@
 <!DOCTYPE mapper
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.fs.course.mapper.FsCourseWatchLogMapper">
+    <mapper namespace="com.fs.course.mapper.FsCourseWatchLogMapper">
 
     <resultMap type="FsCourseWatchLog" id="FsCourseWatchLogResult">
         <result property="logId"    column="log_id"    />
@@ -1120,4 +1120,75 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         DATE(o.create_time)
         ) AS t
     </select>
+    <select id="selectCompanyBaseInfo" resultType="com.fs.course.vo.FsCourseReportVO">
+        SELECT
+        c.company_id AS companyId,
+        c.company_name AS companyName
+        FROM company c
+        <where>
+             c.is_del=0
+            <if test="companyId != null and companyId != ''">
+                AND c.company_id = #{companyId}
+            </if>
+        </where>
+        GROUP BY c.company_id, c.company_name
+        ORDER BY c.create_time DESC
+    </select>
+    <select id="selectWatchStatistics" resultType="com.fs.course.vo.FsCourseReportVO">
+        SELECT
+        <choose>
+            <when test="dimension == 'company'">
+                fwl.company_id AS companyId,
+            </when>
+            <when test="dimension == 'course'">
+                fwl.course_id As courseId,
+            </when>
+            <when test="dimension == 'video'">
+                fwl.video_id AS videoId,
+            </when>
+        </choose>
+        COUNT(DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END ) AS finishedCount,
+        COUNT(CASE WHEN fwl.log_type = 2 THEN fwl.log_id END) AS courseCompleteTimes,
+        COUNT(DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END) AS accessCount,
+        ifnull(
+        ROUND(
+        (
+        COUNT( DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END ) / count( DISTINCT CASE WHEN fwl.log_type != 3
+        THEN fwl.user_id END )) * 100,
+        2
+        ),
+        0
+        ) AS finishRate
+        FROM
+        fs_course_watch_log fwl
+        <where>
+            fwl.send_type = 2
+            <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+                AND fwl.create_time &gt;= #{startDate} AND fwl.create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+            </if>
+            <choose>
+                <when test="dimension == 'company' and companyId != null and companyId > 0">
+                    AND fwl.company_id = #{companyId}
+                </when>
+                <when test="dimension == 'video' and videoId != null and videoId > 0">
+                    AND fwl.video_id = #{videoId}
+                </when>
+                <when test="dimension == 'course' and courseId != null and courseId != ''">
+                    AND fwl.course_id = #{courseId}
+                </when>
+            </choose>
+        </where>
+        <choose>
+            <when test="dimension == 'course'">
+                GROUP BY fwl.course_id
+            </when>
+            <when test="dimension == 'video'">
+                GROUP BY fwl.video_id
+            </when>
+            <otherwise>
+                GROUP BY fwl.company_id
+            </otherwise>
+        </choose>
+        ORDER BY accessCount desc
+    </select>
 </mapper>

+ 8 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseMapper.xml

@@ -162,6 +162,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where cwl.user_id = #{userId} and uc.project = #{projectId}
           and cwl.create_time between curdate() and date_add(curdate(), interval 1 day) and cwl.send_type = 1
     </select>
+    <select id="selectCourseNamesByIds" resultType="java.util.Map">
+        SELECT CAST(course_id AS SIGNED) AS courseId, course_name AS courseName
+        FROM fs_user_course
+        WHERE is_del = 0 AND course_id IN
+        <foreach collection="courseIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
 
 
     <insert id="insertFsUserCourse" parameterType="FsUserCourse" useGeneratedKeys="true" keyProperty="courseId">

+ 8 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml

@@ -503,6 +503,14 @@
            and video.is_del = 0  and video.course_id= #{courseId}
             limit 1
     </select>
+    <select id="selectVideoNamesByIds" resultType="java.util.Map">
+        SELECT CAST(video_id AS SIGNED) AS videoId, title AS videoName
+        FROM fs_user_course_video
+        WHERE is_del = 0 AND video_id IN
+        <foreach collection="videoIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
 
     <update id="batchDown" parameterType="String">
         update fs_user_course_video set is_on_put = 1 where video_id in

+ 9 - 0
fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml

@@ -31,6 +31,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null "> and create_time = #{createTime}</if>
             <if test="businessType != null "> and business_type = #{businessType}</if>
         </where>
+        order by create_time desc
     </select>
 
     <select id="selectFsUserIntegralLogsById" parameterType="Long" resultMap="FsUserIntegralLogsResult">
@@ -98,4 +99,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </delete>
+
+    <!-- 查询用户最新的积分记录 -->
+    <select id="selectLatestIntegralLogByUserId" parameterType="Long" resultMap="FsUserIntegralLogsResult">
+        <include refid="selectFsUserIntegralLogsVo"/>
+        where user_id = #{userId}
+        order by create_time desc, id desc
+        limit 1
+    </select>
 </mapper>

+ 15 - 0
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -2443,4 +2443,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <!-- 查询用户列表(用于积分管理) -->
+    <select id="selectFsUserListForIntegral" parameterType="FsUser" resultMap="FsUserResult">
+        select user_id, nick_name, phone, integral from fs_user
+        <where>
+            and is_del = 0
+            <if test="phone != null and phone != ''">
+                and phone like concat('%', #{phone}, '%')
+            </if>
+            <if test="nickName != null and nickName != ''">
+                and (nick_name like concat('%', #{nickName}, '%') or nickname like concat('%', #{nickName}, '%'))
+            </if>
+        </where>
+        order by user_id desc
+    </select>
+
 </mapper>

+ 81 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductPurchaseLimitScrmMapper.xml

@@ -0,0 +1,81 @@
+<?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.hisStore.mapper.FsStoreProductPurchaseLimitScrmMapper">
+
+    <resultMap type="FsStoreProductPurchaseLimitScrm" id="FsStoreProductPurchaseLimitResult">
+        <result property="id"    column="id"    />
+        <result property="productId"    column="product_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="num"    column="num"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectFsStoreProductPurchaseLimitVo">
+        select id, product_id, user_id, num, create_time, update_time from fs_store_product_purchase_limit_scrm
+    </sql>
+
+    <select id="selectFsStoreProductPurchaseLimitList" parameterType="FsStoreProductPurchaseLimitScrm" resultMap="FsStoreProductPurchaseLimitResult">
+        <include refid="selectFsStoreProductPurchaseLimitVo"/>
+        <where>
+            <if test="productId != null "> and product_id = #{productId}</if>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="num != null "> and num = #{num}</if>
+        </where>
+    </select>
+
+    <select id="selectFsStoreProductPurchaseLimitById" parameterType="Long" resultMap="FsStoreProductPurchaseLimitResult">
+        <include refid="selectFsStoreProductPurchaseLimitVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectByProductIdAndUserId" resultMap="FsStoreProductPurchaseLimitResult">
+        <include refid="selectFsStoreProductPurchaseLimitVo"/>
+        where product_id = #{productId} and user_id = #{userId}
+    </select>
+
+    <insert id="insertFsStoreProductPurchaseLimit" parameterType="FsStoreProductPurchaseLimitScrm" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_product_purchase_limit_scrm
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="productId != null">product_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="num != null">num,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="productId != null">#{productId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="num != null">#{num},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsStoreProductPurchaseLimit" parameterType="FsStoreProductPurchaseLimitScrm">
+        update fs_store_product_purchase_limit_scrm
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="productId != null">product_id = #{productId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="num != null">num = #{num},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsStoreProductPurchaseLimitById" parameterType="Long">
+        delete from fs_store_product_purchase_limit_scrm where id = #{id}
+    </delete>
+
+    <delete id="deleteFsStoreProductPurchaseLimitByIds" parameterType="String">
+        delete from fs_store_product_purchase_limit_scrm where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+</mapper>
+

+ 19 - 15
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -1,7 +1,7 @@
 <?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">
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.hisStore.mapper.FsStoreProductScrmMapper">
 
     <resultMap type="FsStoreProductScrm" id="FsStoreProductResult">
@@ -76,6 +76,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="shelfLife"    column="shelf_life"    />
         <result property="domesticImported"    column="domestic_imported"    />
         <result property="appIds"    column="app_ids"    />
+        <result property="purchaseLimit"    column="purchase_limit"    />
     </resultMap>
 
     <sql id="selectFsStoreProductVo">
@@ -87,8 +88,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                is_display,tui_cate_id,company_ids,is_drug,drug_image,drug_reg_cert_no,common_name,dosage_form,
                unit_price,batch_number,mah,mah_address,manufacturer,manufacturer_address,indications,dosage,
                adverse_reactions,contraindications,precautions,is_audit,store_id,return_address,brand,food_production_license_code,
-               origin_place,net_content,shelf_life,domestic_imported,app_ids
-               from fs_store_product_scrm
+               origin_place,net_content,shelf_life,domestic_imported,app_ids,purchase_limit
+        from fs_store_product_scrm
     </sql>
 
     <sql id="selectFsStoreProductPVo">
@@ -100,7 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                p.is_display,p.tui_cate_id,p.company_ids,p.is_drug,p.drug_image,p.drug_reg_cert_no,p.common_name,p.dosage_form,
                p.unit_price,p.batch_number,p.mah,p.mah_address,p.manufacturer,p.manufacturer_address,p.indications,p.dosage,
                p.adverse_reactions,p.contraindications,p.precautions,p.is_audit,p.store_id,p.return_address,p.brand,p.food_production_license_code,
-               p.origin_place,p.net_content,p.shelf_life,p.domestic_imported,app_ids
+               p.origin_place,p.net_content,p.shelf_life,p.domestic_imported,app_ids,p.purchase_limit
         from fs_store_product_scrm p
     </sql>
 
@@ -278,7 +279,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="shelfLife != null">shelf_life,</if>
             <if test="domesticImported != null and domesticImported != ''">domestic_imported,</if>
             <if test="appIds != null and appIds != ''">app_ids, </if>
-         </trim>
+            <if test="purchaseLimit != null">purchase_limit,</if>
+        </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="image != null and image != ''">#{image},</if>
             <if test="video != null and video != ''">#{video},</if>
@@ -350,7 +352,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="shelfLife != null">#{shelfLife},</if>
             <if test="domesticImported != null and domesticImported != ''">#{domesticImported},</if>
             <if test="appIds != null and appIds != ''">#{appIds}, </if>
-         </trim>
+            <if test="purchaseLimit != null">#{purchaseLimit},</if>
+        </trim>
     </insert>
 
     <update id="updateFsStoreProduct" parameterType="FsStoreProductScrm">
@@ -426,6 +429,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="shelfLife != null">shelf_life = #{shelfLife},</if>
             <if test="domesticImported != null">domestic_imported = #{domesticImported},</if>
             <if test="appIds != null and appIds != ''">app_ids = #{appIds}, </if>
+            <if test="purchaseLimit != null">purchase_limit = #{purchaseLimit},</if>
         </trim>
         where product_id = #{productId}
     </update>
@@ -480,14 +484,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             inner join fs_store_scrm fs on fs.store_id = fsp.store_id
         </if>
         <if test='config.isAudit == "1" '>
-        and fs.is_audit = '1'
+            and fs.is_audit = '1'
         </if>
         where fsp.is_del=0 and fsp.is_show=1
         <if test='config.isAudit == "1" '>
-        and fsp.is_audit = '1'
+            and fsp.is_audit = '1'
         </if>
         <if test = 'param.appId != null and param.appId != ""'>
-        and ((FIND_IN_SET(#{param.appId}, fsp.app_ids) > 0))
+            and ((FIND_IN_SET(#{param.appId}, fsp.app_ids) > 0))
         </if>
         and fsp.is_best=1 and fsp.is_display=1 order by fsp.sort desc,fsp.product_id desc
     </select>
@@ -521,11 +525,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectFsStoreProductHotQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
         <if test='config.isAudit == "1" '>
-        inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
+            inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
         </if>
         where p.is_del=0 and p.is_show=1
         <if test='config.isAudit == "1" '>
-        and p.is_audit = '1'
+            and p.is_audit = '1'
         </if>
         <if test='appId != null and appId = "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
@@ -535,14 +539,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectFsStoreProductGoodListQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
         <if test='config.isAudit == "1" '>
-        inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
+            inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
         </if>
         where p.is_del=0 and p.is_show=1
         <if test='config.isAudit == "1" '>
-        and p.is_audit = '1'
+            and p.is_audit = '1'
         </if>
         <if test = 'param.appId != null and param.appId != ""'>
-        and ((FIND_IN_SET(#{param.appId}, p.app_ids) > 0))
+            and ((FIND_IN_SET(#{param.appId}, p.app_ids) > 0))
         </if>
         and  p.is_good=1 and p.is_display=1 order by p.sort desc
     </select>

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

@@ -99,9 +99,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <select id="selectQwTagGroups" resultType="com.fs.qw.vo.QwTagGroupListVO">
-        select * from qw_tag_group  where group_id in (
+        select * from qw_tag_group  where  corp_id=#{corpId} and group_id in (
             select DISTINCT group_id from qw_tag
      <where>
+         <if test="corpId != null and corpId != '' ">
+             and corp_id=#{corpId}
+         </if>
         <if test="name != null and name != '' ">
          and name like concat( '%', #{name}, '%')
         </if>

+ 28 - 9
fs-user-app/src/main/java/com/fs/app/controller/live/LiveGoodsController.java

@@ -10,14 +10,8 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.his.service.IFsStoreProductService;
-import com.fs.hisStore.domain.FsStoreProductAttrScrm;
-import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
-import com.fs.hisStore.domain.FsStoreProductRelationScrm;
-import com.fs.hisStore.domain.FsStoreProductScrm;
-import com.fs.hisStore.service.IFsStoreProductAttrScrmService;
-import com.fs.hisStore.service.IFsStoreProductAttrValueScrmService;
-import com.fs.hisStore.service.IFsStoreProductRelationScrmService;
-import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.hisStore.domain.*;
+import com.fs.hisStore.service.*;
 import com.fs.live.domain.LiveGoods;
 import com.fs.live.service.ILiveGoodsService;
 import com.github.pagehelper.PageHelper;
@@ -56,6 +50,8 @@ public class LiveGoodsController extends AppBaseController
 
     @Autowired
     private IFsStoreProductAttrValueScrmService attrValueService;
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
 
     /**
      * 查询直播商品列表
@@ -167,6 +163,7 @@ public class LiveGoodsController extends AppBaseController
             cacheData.put("productValues", productValues);
             redisCache.setCacheObject(cacheKey, cacheData, LiveKeysConstant.PRODUCT_DETAIL_CACHE_EXPIRE, TimeUnit.SECONDS);
         }
+
         
         // 获取用户的TOKEN写入足迹
         String userId=getUserId();
@@ -194,7 +191,29 @@ public class LiveGoodsController extends AppBaseController
                 productRelationService.insertFsStoreProductRelation(relation);
             }
         }
-        return R.ok().put("product",product).put("productAttr",productAttr).put("productValues",productValues);
+
+        // 查询限购信息
+        Integer remainingPurchaseLimit = null; // 剩余可购买数量
+        Integer purchasedNum = 0; // 已购买数量
+        if (product.getPurchaseLimit() != null && product.getPurchaseLimit() > 0) {
+            // 商品有限购,查询用户是否购买过
+            if (userId != null) {
+                FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(
+                        product.getProductId(), Long.parseLong(userId));
+                if (purchaseLimit != null) {
+                    purchasedNum = purchaseLimit.getNum();
+                }
+                // 计算剩余可购买数量
+                remainingPurchaseLimit = product.getPurchaseLimit() - purchasedNum;
+                if (remainingPurchaseLimit < 0) {
+                    remainingPurchaseLimit = 0;
+                }
+            } else {
+                // 未登录用户,剩余可购买数量等于限购数量
+                remainingPurchaseLimit = product.getPurchaseLimit();
+            }
+        }
+        return R.ok().put("remainingPurchaseLimit", remainingPurchaseLimit).put("product",product).put("productAttr",productAttr).put("productValues",productValues);
     }
 
     /**

+ 32 - 1
fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

@@ -54,6 +54,9 @@ public class ProductScrmController extends AppBaseController {
 
     @Autowired
     private IFsStoreScrmService storeScrmService;
+
+    @Autowired
+    private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
     /**
      * 获取用户信息
      * @param storeId
@@ -205,7 +208,35 @@ public class ProductScrmController extends AppBaseController {
                 productRelationService.insertFsStoreProductRelation(relation);
             }
         }
-        return R.ok().put("product",product).put("productAttr",productAttr).put("productValues",productValues).put("store",fsStoreScrm);
+
+        // 查询限购信息
+        Integer remainingPurchaseLimit = null; // 剩余可购买数量
+        Integer purchasedNum = 0; // 已购买数量
+        if (product.getPurchaseLimit() != null && product.getPurchaseLimit() > 0) {
+            // 商品有限购,查询用户是否购买过
+            if (userId != null) {
+                FsStoreProductPurchaseLimitScrm purchaseLimit = purchaseLimitService.selectByProductIdAndUserId(
+                        product.getProductId(), Long.parseLong(userId));
+                if (purchaseLimit != null) {
+                    purchasedNum = purchaseLimit.getNum();
+                }
+                // 计算剩余可购买数量
+                remainingPurchaseLimit = product.getPurchaseLimit() - purchasedNum;
+                if (remainingPurchaseLimit < 0) {
+                    remainingPurchaseLimit = 0;
+                }
+            } else {
+                // 未登录用户,剩余可购买数量等于限购数量
+                remainingPurchaseLimit = product.getPurchaseLimit();
+            }
+        }
+
+        return R.ok().put("product",product)
+                .put("productAttr",productAttr)
+                .put("productValues",productValues)
+                .put("store",fsStoreScrm)
+                .put("remainingPurchaseLimit", remainingPurchaseLimit) // 剩余可购买数量
+                .put("purchasedNum", purchasedNum); // 已购买数量
     }
 
     @Login

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels