Prechádzať zdrojové kódy

外部订单优化,消息弹窗优化

wangxy 3 dní pred
rodič
commit
f7a8493424
57 zmenil súbory, kde vykonal 2136 pridanie a 137 odobranie
  1. 64 0
      fs-admin/src/main/java/com/fs/admin/controller/crm/ComplaintMsgController.java
  2. 91 19
      fs-admin/src/main/java/com/fs/crm/controller/CrmMsgController.java
  3. 64 0
      fs-admin/src/main/java/com/fs/crm/controller/IntegralOrderMsgController.java
  4. 9 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  5. 35 17
      fs-admin/src/main/java/com/fs/hisStore/task/ErpTask.java
  6. 47 0
      fs-admin/src/main/java/com/fs/task/AdminMsgTask.java
  7. 103 0
      fs-admin/src/main/java/com/fs/task/CompanyMsgTask.java
  8. 28 23
      fs-company/src/main/java/com/fs/company/controller/crm/CrmMsgController.java
  9. 78 0
      fs-company/src/main/java/com/fs/company/controller/crm/OrderAuditMsgController.java
  10. 80 0
      fs-company/src/main/java/com/fs/company/controller/crm/RedPacketBalanceMsgController.java
  11. 39 0
      fs-company/src/main/java/com/fs/company/controller/store/FsExternalOrderController.java
  12. 2 1
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  13. 8 2
      fs-company/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  14. 20 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  15. 4 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseComplaintRecordMapper.java
  16. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  17. 2 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  18. 72 0
      fs-service/src/main/java/com/fs/crm/domain/CrmMsg.java
  19. 66 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmMsgMapper.java
  20. 25 0
      fs-service/src/main/java/com/fs/crm/service/IComplaintMsgService.java
  21. 26 0
      fs-service/src/main/java/com/fs/crm/service/IIntegralOrderMsgService.java
  22. 27 0
      fs-service/src/main/java/com/fs/crm/service/IOrderAuditMsgService.java
  23. 28 0
      fs-service/src/main/java/com/fs/crm/service/IRedPacketBalanceMsgService.java
  24. 91 0
      fs-service/src/main/java/com/fs/crm/service/impl/ComplaintMsgServiceImpl.java
  25. 117 0
      fs-service/src/main/java/com/fs/crm/service/impl/IntegralOrderMsgServiceImpl.java
  26. 109 0
      fs-service/src/main/java/com/fs/crm/service/impl/OrderAuditMsgServiceImpl.java
  27. 106 0
      fs-service/src/main/java/com/fs/crm/service/impl/RedPacketBalanceMsgServiceImpl.java
  28. 6 0
      fs-service/src/main/java/com/fs/his/domain/FsAppActiveUserDaily.java
  29. 1 1
      fs-service/src/main/java/com/fs/his/domain/FsExternalOrder.java
  30. 3 0
      fs-service/src/main/java/com/fs/his/domain/FsStoreOrderFinanceAudit.java
  31. 2 0
      fs-service/src/main/java/com/fs/his/domain/vo/FsExternalOrderListVO.java
  32. 9 0
      fs-service/src/main/java/com/fs/his/dto/ExternalOrderProductDTO.java
  33. 27 1
      fs-service/src/main/java/com/fs/his/mapper/FsAppActiveUserDailyMapper.java
  34. 3 0
      fs-service/src/main/java/com/fs/his/mapper/FsExternalOrderMapper.java
  35. 6 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreMapper.java
  36. 4 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderFinanceAuditMapper.java
  37. 3 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  38. 3 1
      fs-service/src/main/java/com/fs/his/param/FsExternalOrderAddParam.java
  39. 2 0
      fs-service/src/main/java/com/fs/his/param/FsOrderFinanceAuditApplyParam.java
  40. 26 0
      fs-service/src/main/java/com/fs/his/service/IFsAppActiveUserDailyService.java
  41. 19 0
      fs-service/src/main/java/com/fs/his/service/IFsExternalOrderService.java
  42. 1 1
      fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java
  43. 11 0
      fs-service/src/main/java/com/fs/his/service/IFsStoreProductService.java
  44. 64 12
      fs-service/src/main/java/com/fs/his/service/impl/FsAppActiveUserDailyServiceImpl.java
  45. 172 13
      fs-service/src/main/java/com/fs/his/service/impl/FsExternalOrderServiceImpl.java
  46. 34 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  47. 2 1
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  48. 264 24
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreProductServiceImpl.java
  49. 10 5
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  50. 6 0
      fs-service/src/main/java/com/fs/hisStore/vo/h5/FsUserSummaryCountVO.java
  51. 6 0
      fs-service/src/main/java/com/fs/store/vo/h5/FsUserSummaryCountVO.java
  52. 1 0
      fs-service/src/main/resources/mapper/course/FsUserCourseComplaintRecordMapper.xml
  53. 26 2
      fs-service/src/main/resources/mapper/crm/CrmMsgMapper.xml
  54. 36 0
      fs-service/src/main/resources/mapper/his/FsAppActiveUserDailyMapper.xml
  55. 5 3
      fs-service/src/main/resources/mapper/his/FsExternalOrderMapper.xml
  56. 5 1
      fs-service/src/main/resources/mapper/his/FsStoreOrderFinanceAuditMapper.xml
  57. 37 7
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml

+ 64 - 0
fs-admin/src/main/java/com/fs/admin/controller/crm/ComplaintMsgController.java

@@ -0,0 +1,64 @@
+package com.fs.admin.controller.crm;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 投诉消息Controller
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+@RestController
+@RequestMapping("/crm/complaintMsg")
+public class ComplaintMsgController extends BaseController
+{
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    private static final String PLATFORM_ADMIN = "admin";
+    private static final int MSG_TYPE_COMPLAINT = 9;
+
+    @GetMapping("/getLatestUnread")
+    public R getLatestUnreadMsg()
+    {
+        CrmMsg msg = crmMsgMapper.selectLatestBroadcastMsg(PLATFORM_ADMIN, MSG_TYPE_COMPLAINT);
+        Long unreadCount = crmMsgMapper.selectBroadcastUnreadCountByType(PLATFORM_ADMIN, MSG_TYPE_COMPLAINT);
+        Long totalCount = crmMsgMapper.selectBroadcastTotalCountByType(PLATFORM_ADMIN, MSG_TYPE_COMPLAINT);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("msg", msg);
+        result.put("unreadCount", unreadCount);
+        result.put("totalCount", totalCount);
+        return R.ok().put("data", result);
+    }
+
+    @GetMapping("/getUnreadCount")
+    public R getUnreadCount()
+    {
+        Long count = crmMsgMapper.selectBroadcastUnreadCountByType(PLATFORM_ADMIN, MSG_TYPE_COMPLAINT);
+        return R.ok().put("data", count);
+    }
+
+    @PostMapping("/markAsRead/{msgId}")
+    public AjaxResult markAsRead(@PathVariable Long msgId)
+    {
+        crmMsgMapper.markMsgAsRead(msgId);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/markAllAsRead")
+    public AjaxResult markAllAsRead()
+    {
+        crmMsgMapper.markAllBroadcastAsRead(PLATFORM_ADMIN);
+        return AjaxResult.success();
+    }
+}

+ 91 - 19
fs-admin/src/main/java/com/fs/crm/controller/CrmMsgController.java

@@ -3,15 +3,23 @@ package com.fs.crm.controller;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
 import com.fs.crm.service.ICrmMsgService;
+import com.fs.crm.vo.CrmMsgTypeVO;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.system.vo.DictVO;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.ApiParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -26,10 +34,89 @@ public class CrmMsgController extends BaseController
 {
     @Autowired
     private ICrmMsgService crmMsgService;
+    @Autowired
+    private ISysDictDataService dictDataService;
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    private static final String PLATFORM_ADMIN = "admin";
+
+    @GetMapping("/getMsgCount")
+    public R getMsgCount(){
+        Long count = crmMsgMapper.selectBroadcastUnreadCount(PLATFORM_ADMIN);
+        return R.ok().put("counts",count);
+    }
+
+    @GetMapping("/getMsg")
+    public R getMsg(){
+        List<DictVO> types = dictDataService.selectDictDataListByType("crm_msg_type");
+        List<CrmMsgTypeVO> counts = new ArrayList<>();
+        for(DictVO v : types){
+            Long count = crmMsgMapper.selectBroadcastUnreadCountByType(
+                PLATFORM_ADMIN, 
+                Integer.parseInt(v.getDictValue())
+            );
+            if(count > 0){
+                CrmMsgTypeVO typeBO = new CrmMsgTypeVO();
+                typeBO.setMsgType(Integer.parseInt(v.getDictValue()));
+                typeBO.setTotal(count);
+                typeBO.setMsgTypeName(v.getDictLabel());
+                counts.add(typeBO);
+            }
+        }
+        return R.ok().put("counts", counts);
+    }
+
+    @GetMapping("/getMsgList")
+    public R getMsgList(
+            @ApiParam(required = true, name = "type", value = "类型") @RequestParam(value = "type", required = false) Integer type
+    ){
+        startPage();
+        List<CrmMsg> list = crmMsgMapper.selectBroadcastMsgList(PLATFORM_ADMIN, type);
+        PageInfo<CrmMsg> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+    @GetMapping("/getLatestUnread")
+    public R getLatestUnread(){
+        CrmMsg msg = crmMsgMapper.selectLatestBroadcastMsg(PLATFORM_ADMIN, 6);
+        return R.ok().put("data", msg);
+    }
+
+    @GetMapping("/getUnreadCount")
+    public R getUnreadCount(){
+        Long count = crmMsgMapper.selectBroadcastUnreadCountByType(PLATFORM_ADMIN, 6);
+        return R.ok().put("data", count);
+    }
+
+    @PostMapping("/markAsRead/{msgId}")
+    public AjaxResult markAsRead(@PathVariable Long msgId)
+    {
+        crmMsgMapper.markMsgAsRead(msgId);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/markAllAsRead")
+    public AjaxResult markAllAsRead()
+    {
+        crmMsgMapper.markAllBroadcastAsRead(PLATFORM_ADMIN);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/setRead")
+    public R setRead(@RequestBody CrmMsg msg)
+    {
+        crmMsgMapper.markMsgAsRead(msg.getMsgId());
+        return R.ok();
+    }
+
+    @PostMapping("/setAllRead")
+    public R setAllRead()
+    {
+        crmMsgMapper.markAllBroadcastAsRead(PLATFORM_ADMIN);
+        return R.ok();
+    }
 
-    /**
-     * 查询 消息列表
-     */
     @PreAuthorize("@ss.hasPermi('crm:msg:list')")
     @GetMapping("/list")
     public TableDataInfo list(CrmMsg crmMsg)
@@ -39,9 +126,6 @@ public class CrmMsgController extends BaseController
         return getDataTable(list);
     }
 
-    /**
-     * 导出 消息列表
-     */
     @PreAuthorize("@ss.hasPermi('crm:msg:export')")
     @Log(title = " 消息", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
@@ -52,19 +136,13 @@ public class CrmMsgController extends BaseController
         return util.exportExcel(list, "msg");
     }
 
-    /**
-     * 获取 消息详细信息
-     */
     @PreAuthorize("@ss.hasPermi('crm:msg:query')")
-    @GetMapping(value = "/{msgId}")
+    @GetMapping(value = "/detail/{msgId}")
     public AjaxResult getInfo(@PathVariable("msgId") Long msgId)
     {
         return AjaxResult.success(crmMsgService.selectCrmMsgById(msgId));
     }
 
-    /**
-     * 新增 消息
-     */
     @PreAuthorize("@ss.hasPermi('crm:msg:add')")
     @Log(title = " 消息", businessType = BusinessType.INSERT)
     @PostMapping
@@ -73,9 +151,6 @@ public class CrmMsgController extends BaseController
         return toAjax(crmMsgService.insertCrmMsg(crmMsg));
     }
 
-    /**
-     * 修改 消息
-     */
     @PreAuthorize("@ss.hasPermi('crm:msg:edit')")
     @Log(title = " 消息", businessType = BusinessType.UPDATE)
     @PutMapping
@@ -84,9 +159,6 @@ public class CrmMsgController extends BaseController
         return toAjax(crmMsgService.updateCrmMsg(crmMsg));
     }
 
-    /**
-     * 删除 消息
-     */
     @PreAuthorize("@ss.hasPermi('crm:msg:remove')")
     @Log(title = " 消息", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{msgIds}")

+ 64 - 0
fs-admin/src/main/java/com/fs/crm/controller/IntegralOrderMsgController.java

@@ -0,0 +1,64 @@
+package com.fs.admin.controller.crm;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 积分商城订单消息Controller
+ * 
+ * @author fs
+ * @date 2026-04-21
+ */
+@RestController
+@RequestMapping("/crm/integralOrderMsg")
+public class IntegralOrderMsgController extends BaseController
+{
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    private static final String PLATFORM_ADMIN = "admin";
+    private static final int MSG_TYPE_INTEGRAL_ORDER = 6;
+
+    @GetMapping("/getLatestUnread")
+    public R getLatestUnreadMsg()
+    {
+        CrmMsg msg = crmMsgMapper.selectLatestBroadcastMsg(PLATFORM_ADMIN, MSG_TYPE_INTEGRAL_ORDER);
+        Long unreadCount = crmMsgMapper.selectBroadcastUnreadCountByType(PLATFORM_ADMIN, MSG_TYPE_INTEGRAL_ORDER);
+        Long totalCount = crmMsgMapper.selectBroadcastTotalCountByType(PLATFORM_ADMIN, MSG_TYPE_INTEGRAL_ORDER);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("msg", msg);
+        result.put("unreadCount", unreadCount);
+        result.put("totalCount", totalCount);
+        return R.ok().put("data", result);
+    }
+
+    @GetMapping("/getUnreadCount")
+    public R getUnreadCount()
+    {
+        Long count = crmMsgMapper.selectBroadcastUnreadCountByType(PLATFORM_ADMIN, MSG_TYPE_INTEGRAL_ORDER);
+        return R.ok().put("data", count);
+    }
+
+    @PostMapping("/markAsRead/{msgId}")
+    public AjaxResult markAsRead(@PathVariable Long msgId)
+    {
+        crmMsgMapper.markMsgAsRead(msgId);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/markAllAsRead")
+    public AjaxResult markAllAsRead()
+    {
+        crmMsgMapper.markAllBroadcastAsRead(PLATFORM_ADMIN);
+        return AjaxResult.success();
+    }
+}

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

@@ -1894,6 +1894,15 @@ public class Task {
         fsExternalOrderService.pushToJstBatch(orderIds);
     }
 
+    /**
+     * 同步外部订单状态(从聚水潭)
+     */
+    public void syncExternalOrderStatusFromJst() throws Exception {
+        log.info("开始同步外部订单状态");
+        fsExternalOrderService.syncOrderStatusFromJst();
+        log.info("同步外部订单状态完成");
+    }
+
     /**
      * 初始化产品来源类型和连锁品牌
      * 根据店铺名称判断:包含红德堂、优身、乘济、诚质的为大包品,否则为自库品

+ 35 - 17
fs-admin/src/main/java/com/fs/hisStore/task/ErpTask.java

@@ -13,6 +13,8 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 
@@ -20,6 +22,9 @@ import java.util.List;
 @Slf4j
 public class ErpTask {
 
+    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+    private static final int DEFAULT_PAGE_SIZE = 100;
+
     @Autowired
     private FsErpFinishPushMapper fsErpFinishPushMapper;
     @Autowired
@@ -31,57 +36,70 @@ public class ErpTask {
     @Autowired
     private IFsStoreProductService fsStoreProductService;
 
-
     /**
      * 推送完成订单到ERP
      */
-    public void pushFinishOrderToErp(){
+    public void pushFinishOrderToErp() {
         List<FsErpFinishPush> fsErpFinishPushes = fsErpFinishPushMapper.queryPenddingOrder();
         for (FsErpFinishPush fsErpFinishPush : fsErpFinishPushes) {
             FsStoreOrderScrm fsStoreOrder = fsStoreOrderService.selectFsStoreOrderById(fsErpFinishPush.getOrderId());
 
             try {
-
                 ErpOrder erpOrder = fsStoreOrderService.getErpOrder(fsStoreOrder);
-
                 ErpOrderResponse erpOrderResponse = erpOrderService.finishOrder(erpOrder);
 
                 fsErpFinishPush.setParams(erpOrderResponse.getRequestRawData());
                 fsErpFinishPush.setResult(erpOrderResponse.getResponseRawData());
                 fsErpFinishPush.setUpdateTime(new Date());
 
-                if(erpOrderResponse.getSuccess()!= null && erpOrderResponse.getSuccess()){
+                if (erpOrderResponse.getSuccess() != null && erpOrderResponse.getSuccess()) {
                     fsErpFinishPush.setTaskStatus(1);
-
-                    log.error("推送完成订单到ERP成功! 订单号: {}",fsErpFinishPush.getOrderId());
+                    log.error("推送完成订单到ERP成功! 订单号: {}", fsErpFinishPush.getOrderId());
                 } else {
                     fsErpFinishPush.setTaskStatus(2);
-                    fsErpFinishPush.setRetryCount(fsErpFinishPush.getRetryCount()+1);
-                    log.error("推送完成订单到ERP失败! 订单号: {}",fsErpFinishPush.getOrderId());
+                    fsErpFinishPush.setRetryCount(fsErpFinishPush.getRetryCount() + 1);
+                    log.error("推送完成订单到ERP失败! 订单号: {}", fsErpFinishPush.getOrderId());
                 }
             } catch (Throwable e) {
-                fsErpFinishPush.setRetryCount(fsErpFinishPush.getRetryCount()+1);
+                fsErpFinishPush.setRetryCount(fsErpFinishPush.getRetryCount() + 1);
                 fsErpFinishPush.setErrorMessage(ExceptionUtils.getStackTrace(e));
                 fsErpFinishPush.setTaskStatus(2);
-                log.error("订单推送失败!原因: {}", ExceptionUtils.getStackTrace(e),e);
+                log.error("订单推送失败!原因: {}", ExceptionUtils.getStackTrace(e), e);
             }
 
-
             fsErpFinishPushMapper.updateById(fsErpFinishPush);
         }
-
     }
 
     /**
-     * 根据ERP商品编码同步更新商品信息
+     * 页面定时任务使用的无参入口,默认同步昨天整天的数据。
      */
     public void syncProductFromErp() {
+        String[] range = buildYesterdayRange();
+        syncProductFromErp(range[0], range[1], DEFAULT_PAGE_SIZE);
+    }
+
+    public void syncProductFromErp(String modifiedBegin, String modifiedEnd, Integer pageSize) {
         try {
-            int count = fsStoreProductService.updateStoreProductByErpProductCode();
-            log.info("ERP商品同步完成,更新商品数量: {}", count);
+            int count = fsStoreProductService.syncProductFromErp(modifiedBegin, modifiedEnd, pageSize);
+            log.info("ERP商品区间同步完成,modifiedBegin: {}, modifiedEnd: {}, 同步数量: {}",
+                    modifiedBegin, modifiedEnd, count);
         } catch (Exception e) {
-            log.error("ERP商品同步失败: {}", ExceptionUtils.getStackTrace(e), e);
+            log.error("ERP商品区间同步失败: {}", ExceptionUtils.getStackTrace(e), e);
         }
     }
 
+    private String[] buildYesterdayRange() {
+        SimpleDateFormat formatter = new SimpleDateFormat(DATE_TIME_PATTERN);
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        Date todayStart = calendar.getTime();
+        calendar.add(Calendar.DAY_OF_MONTH, -1);
+        Date yesterdayStart = calendar.getTime();
+        return new String[]{formatter.format(yesterdayStart), formatter.format(todayStart)};
+    }
+
 }

+ 47 - 0
fs-admin/src/main/java/com/fs/task/AdminMsgTask.java

@@ -0,0 +1,47 @@
+package com.fs.task;
+
+import com.fs.course.domain.FsUserCourseComplaintRecord;
+import com.fs.course.mapper.FsUserCourseComplaintRecordMapper;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IComplaintMsgService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@AllArgsConstructor
+@Component("adminMsgTask")
+@Slf4j
+public class AdminMsgTask {
+
+    private final FsUserCourseComplaintRecordMapper complaintRecordMapper;
+    private final CrmMsgMapper crmMsgMapper;
+    private final IComplaintMsgService complaintMsgService;
+
+    private static final int MSG_TYPE_COMPLAINT = 9;
+    private static final String PLATFORM_ADMIN = "admin";
+
+    public void checkPendingComplaint() {
+        log.info("开始检查待处理投诉...");
+        try {
+            List<FsUserCourseComplaintRecord> pendingComplaints = complaintRecordMapper.selectPendingComplaintList();
+            for (FsUserCourseComplaintRecord complaint : pendingComplaints) {
+                try {
+                    CrmMsg existMsg = crmMsgMapper.selectMsgByObjIdAndType(complaint.getRecordId(), MSG_TYPE_COMPLAINT);
+                    if (existMsg == null) {
+                        complaintMsgService.sendComplaintReminder(complaint.getRecordId(), complaint.getComplaintContent());
+                        log.info("投诉待处理提醒已发送,投诉记录ID: {}", complaint.getRecordId());
+                    }
+                } catch (Exception e) {
+                    log.error("发送投诉提醒失败,投诉记录ID: {}", complaint.getRecordId(), e);
+                }
+            }
+            log.info("检查待处理投诉完成");
+        } catch (Exception e) {
+            log.error("检查待处理投诉任务执行失败", e);
+        }
+    }
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/task/CompanyMsgTask.java

@@ -0,0 +1,103 @@
+package com.fs.task;
+
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.util.CompanyRedPacketBalanceUtil;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IOrderAuditMsgService;
+import com.fs.crm.service.IRedPacketBalanceMsgService;
+import com.fs.his.domain.FsStoreOrderFinanceAudit;
+import com.fs.his.mapper.FsStoreOrderFinanceAuditMapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.var;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@AllArgsConstructor
+@Component("companyMsgTask")
+@Slf4j
+public class CompanyMsgTask {
+
+    private final CompanyMapper companyMapper;
+    private final CompanyUserMapper companyUserMapper;
+    private final CompanyRedPacketBalanceUtil redPacketBalanceUtil;
+    private final FsStoreOrderFinanceAuditMapper financeAuditMapper;
+    private final CrmMsgMapper crmMsgMapper;
+    private final IRedPacketBalanceMsgService redPacketBalanceMsgService;
+    private final IOrderAuditMsgService orderAuditMsgService;
+
+    private static final BigDecimal BALANCE_THRESHOLD = new BigDecimal("100");
+    private static final int MSG_TYPE_RED_PACKET_BALANCE = 7;
+    private static final int MSG_TYPE_ORDER_AUDIT = 8;
+
+    public void checkRedPacketBalance() {
+        log.info("开始检查公司红包余额...");
+        try {
+            List<Company> companies = companyMapper.selectCompanyList(new Company());
+            for (Company company : companies) {
+                try {
+                    BigDecimal balance = redPacketBalanceUtil.getCacheRedPacketBalance(company.getCompanyId());
+                    if (balance.compareTo(BALANCE_THRESHOLD) < 0) {
+                        CrmMsg existMsg = crmMsgMapper.selectMsgByCompanyIdAndType(company.getCompanyId(), MSG_TYPE_RED_PACKET_BALANCE);
+                        if (existMsg == null) {
+                            redPacketBalanceMsgService.sendRedPacketBalanceWarningToAdmins(
+                                company.getCompanyId(), 
+                                balance, 
+                                BALANCE_THRESHOLD
+                            );
+                            log.info("公司红包余额不足,已发送提醒消息,公司ID: {}, 余额: {}", company.getCompanyId(), balance);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("检查公司红包余额失败,公司ID: {}", company.getCompanyId(), e);
+                }
+            }
+            log.info("检查公司红包余额完成");
+        } catch (Exception e) {
+            log.error("检查公司红包余额任务执行失败", e);
+        }
+    }
+
+    public void checkPendingOrderAudit() {
+        log.info("开始检查待审批订单...");
+        try {
+            List<FsStoreOrderFinanceAudit> pendingAudits = financeAuditMapper.selectPendingAuditList();
+            for (FsStoreOrderFinanceAudit audit : pendingAudits) {
+                try {
+                    CrmMsg existMsg = crmMsgMapper.selectMsgByObjIdAndType(audit.getId(), MSG_TYPE_ORDER_AUDIT);
+                    if (existMsg == null) {
+                        Long companyId = getCompanyIdByApplyUserId(audit.getApplyUserId());
+                        if (companyId != null) {
+                            orderAuditMsgService.sendOrderAuditReminderToLeaders(companyId, audit);
+                            log.info("订单待审批提醒已发送,审批ID: {}, 订单编号: {}", audit.getId(), audit.getOrderCode());
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("发送订单审批提醒失败,审批ID: {}", audit.getId(), e);
+                }
+            }
+            log.info("检查待审批订单完成");
+        } catch (Exception e) {
+            log.error("检查待审批订单任务执行失败", e);
+        }
+    }
+
+    private Long getCompanyIdByApplyUserId(Long applyUserId) {
+        if (applyUserId == null) {
+            return null;
+        }
+        try {
+            var user = companyUserMapper.selectCompanyUserById(applyUserId);
+            return user != null ? user.getCompanyId() : null;
+        } catch (Exception e) {
+            log.error("获取用户公司ID失败,用户ID: {}", applyUserId, e);
+            return null;
+        }
+    }
+}

+ 28 - 23
fs-company/src/main/java/com/fs/company/controller/crm/CrmMsgController.java

@@ -4,6 +4,7 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.ServletUtils;
 import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
 import com.fs.crm.service.ICrmMsgService;
 import com.fs.crm.vo.CrmMsgTypeVO;
 import com.fs.framework.security.LoginUser;
@@ -35,32 +36,39 @@ public class CrmMsgController extends BaseController
     private TokenService tokenService;
     @Autowired
     private ISysDictDataService dictDataService;
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
 
+    private static final String PLATFORM_COMPANY = "company";
 
     @GetMapping("/getMsgCount")
     public R getMsgCount(HttpServletRequest request){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        Long count= crmMsgService.selectCrmMsgCountByUserId(loginUser.getUser().getUserId());
-        return R.ok().put("counts",count);
-
+        Long count = crmMsgMapper.selectUnreadCountByPlatform(loginUser.getUser().getUserId(), PLATFORM_COMPANY);
+        return R.ok().put("counts", count);
     }
 
     @GetMapping("/getMsg")
     public R getMsg(HttpServletRequest request){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        //获取用户未读总数
-        //获取所有类型
-        List<DictVO> types=dictDataService.selectDictDataListByType("crm_msg_type");
-        List<CrmMsgTypeVO> counts=new ArrayList<>();
-        for(DictVO v:types){
-            Long count= crmMsgService.selectCrmMsgCountByUserId(loginUser.getUser().getUserId(),Integer.parseInt(v.getDictValue()));
-            CrmMsgTypeVO typeBO=new CrmMsgTypeVO();
-            typeBO.setMsgType(Integer.parseInt(v.getDictValue()));
-            typeBO.setTotal(count);
-            typeBO.setMsgTypeName(v.getDictLabel());
-            counts.add(typeBO);
+        Long userId = loginUser.getUser().getUserId();
+        List<DictVO> types = dictDataService.selectDictDataListByType("crm_msg_type");
+        List<CrmMsgTypeVO> counts = new ArrayList<>();
+        for(DictVO v : types){
+            Long count = crmMsgMapper.selectUnreadCountByPlatformAndType(
+                userId,
+                PLATFORM_COMPANY, 
+                Integer.parseInt(v.getDictValue())
+            );
+            if(count > 0){
+                CrmMsgTypeVO typeBO = new CrmMsgTypeVO();
+                typeBO.setMsgType(Integer.parseInt(v.getDictValue()));
+                typeBO.setTotal(count);
+                typeBO.setMsgTypeName(v.getDictLabel());
+                counts.add(typeBO);
+            }
         }
-        return R.ok().put("counts",counts);
+        return R.ok().put("counts", counts);
     }
 
 
@@ -75,21 +83,20 @@ public class CrmMsgController extends BaseController
             HttpServletRequest request,
             @ApiParam(required = true, name = "type", value = "类型") @RequestParam(value = "type", required = false) Integer type
     ){
-
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        CrmMsg map=new CrmMsg();
+        CrmMsg map = new CrmMsg();
         map.setMsgType(type);
         map.setCompanyUserId(loginUser.getUser().getUserId());
         startPage();
         List<CrmMsg> list = crmMsgService.selectCrmMsgList(map);
-        PageInfo<CrmMsg> listPageInfo=new PageInfo<>(list);
-        return R.ok().put("data",listPageInfo);
+        PageInfo<CrmMsg> listPageInfo = new PageInfo<>(list);
+        return R.ok().put("data", listPageInfo);
     }
 
     @PostMapping("/setRead")
     public R setRead(@RequestBody CrmMsg msg)
     {
-        crmMsgService.updateCrmMsg(msg);
+        crmMsgMapper.markMsgAsRead(msg.getMsgId());
         return R.ok();
     }
 
@@ -97,9 +104,7 @@ public class CrmMsgController extends BaseController
     public R setAllRead()
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        crmMsgService.setAllRead(loginUser.getUser().getUserId());
+        crmMsgMapper.markAllAsReadByPlatform(loginUser.getUser().getUserId(), PLATFORM_COMPANY);
         return R.ok();
     }
-
-
 }

+ 78 - 0
fs-company/src/main/java/com/fs/company/controller/crm/OrderAuditMsgController.java

@@ -0,0 +1,78 @@
+package com.fs.company.controller.crm;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IOrderAuditMsgService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+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.util.HashMap;
+import java.util.Map;
+
+@Api(tags = "订单审批提醒消息")
+@RestController
+@RequestMapping("/crm/orderAuditMsg")
+public class OrderAuditMsgController extends BaseController
+{
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    @Autowired
+    private IOrderAuditMsgService orderAuditMsgService;
+
+    private static final String PLATFORM_COMPANY = "company";
+
+
+    @ApiOperation("获取最新未读订单审批提醒消息")
+    @GetMapping("/getLatestUnread")
+    public R getLatestUnreadMsg()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        CrmMsg msg = orderAuditMsgService.getLatestUnreadMsg(userId, PLATFORM_COMPANY);
+        Long unreadCount = orderAuditMsgService.getUnreadCount(userId, PLATFORM_COMPANY);
+        Long totalCount = crmMsgMapper.selectTotalCountByPlatformAndType(userId, PLATFORM_COMPANY, 8);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("msg", msg);
+        result.put("unreadCount", unreadCount);
+        result.put("totalCount", totalCount);
+        return R.ok().put("data", result);
+    }
+
+    @ApiOperation("获取未读订单审批提醒消息数量")
+    @GetMapping("/getUnreadCount")
+    public R getUnreadCount()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long count = orderAuditMsgService.getUnreadCount(loginUser.getUser().getUserId(), PLATFORM_COMPANY);
+        return R.ok().put("data", count);
+    }
+
+    @ApiOperation("标记订单审批提醒消息为已读")
+    @PostMapping("/markAsRead/{msgId}")
+    public R markAsRead(@PathVariable Long msgId)
+    {
+        crmMsgMapper.markMsgAsRead(msgId);
+        return R.ok();
+    }
+
+    @ApiOperation("标记所有订单审批提醒消息为已读")
+    @PostMapping("/markAllAsRead")
+    public R markAllAsRead()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        orderAuditMsgService.markAllAsRead(loginUser.getUser().getUserId(), PLATFORM_COMPANY);
+        return R.ok();
+    }
+}

+ 80 - 0
fs-company/src/main/java/com/fs/company/controller/crm/RedPacketBalanceMsgController.java

@@ -0,0 +1,80 @@
+package com.fs.company.controller.crm;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IOrderAuditMsgService;
+import com.fs.crm.service.IRedPacketBalanceMsgService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+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.util.HashMap;
+import java.util.Map;
+
+@Api(tags = "红包余额不足消息")
+@RestController
+@RequestMapping("/crm/redPacketBalanceMsg")
+public class RedPacketBalanceMsgController extends BaseController
+{
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    @Autowired
+    private IRedPacketBalanceMsgService redPacketBalanceMsgService;
+
+    private static final String PLATFORM_COMPANY = "company";
+
+
+
+    @ApiOperation("获取最新未读红包余额不足消息")
+    @GetMapping("/getLatestUnread")
+    public R getLatestUnreadMsg()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        CrmMsg msg = redPacketBalanceMsgService.getLatestUnreadMsg(userId, PLATFORM_COMPANY);
+        Long unreadCount = redPacketBalanceMsgService.getUnreadCount(userId, PLATFORM_COMPANY);
+        Long totalCount = crmMsgMapper.selectTotalCountByPlatformAndType(userId, PLATFORM_COMPANY, 7);
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("msg", msg);
+        result.put("unreadCount", unreadCount);
+        result.put("totalCount", totalCount);
+        return R.ok().put("data", result);
+    }
+
+    @ApiOperation("获取未读红包余额不足消息数量")
+    @GetMapping("/getUnreadCount")
+    public R getUnreadCount()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long count = redPacketBalanceMsgService.getUnreadCount(loginUser.getUser().getUserId(), PLATFORM_COMPANY);
+        return R.ok().put("data", count);
+    }
+
+    @ApiOperation("标记红包余额不足消息为已读")
+    @PostMapping("/markAsRead/{msgId}")
+    public R markAsRead(@PathVariable Long msgId)
+    {
+        crmMsgMapper.markMsgAsRead(msgId);
+        return R.ok();
+    }
+
+    @ApiOperation("标记所有红包余额不足消息为已读")
+    @PostMapping("/markAllAsRead")
+    public R markAllAsRead()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        redPacketBalanceMsgService.markAllAsRead(loginUser.getUser().getUserId(), PLATFORM_COMPANY);
+        return R.ok();
+    }
+}

+ 39 - 0
fs-company/src/main/java/com/fs/company/controller/store/FsExternalOrderController.java

@@ -14,7 +14,9 @@ import com.fs.his.domain.FsExternalOrderItem;
 import com.fs.his.domain.vo.FsExternalOrderListVO;
 import com.fs.his.mapper.FsExternalOrderItemMapper;
 import com.fs.his.param.FsExternalOrderAddParam;
+import com.fs.his.param.FsOrderFinanceAuditApplyParam;
 import com.fs.his.service.IFsExternalOrderService;
+import com.fs.his.service.IFsStoreOrderService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -31,6 +33,9 @@ public class FsExternalOrderController extends BaseController {
     @Autowired
     private FsExternalOrderItemMapper fsExternalOrderItemMapper;
 
+    @Autowired
+    private IFsStoreOrderService fsStoreOrderService;
+
     @GetMapping("/list")
     @Log(title = "外部订单列表", businessType = BusinessType.OTHER)
     @PreAuthorize("@ss.hasPermi('his:externalOrder:list')")
@@ -76,4 +81,38 @@ public class FsExternalOrderController extends BaseController {
     public AjaxResult remove(@PathVariable Long[] orderIds) {
         return toAjax(fsExternalOrderService.deleteFsExternalOrderByOrderIds(orderIds));
     }
+
+    @Log(title = "审核外部订单", businessType = BusinessType.UPDATE)
+    @PostMapping("/audit/{orderId}")
+    @PreAuthorize("@ss.hasPermi('store:externalOrder:audit')")
+    public R audit(@PathVariable("orderId") Long orderId) {
+        return fsExternalOrderService.auditExternalOrder(orderId);
+    }
+
+    @Log(title = "取消外部订单", businessType = BusinessType.UPDATE)
+    @PostMapping("/cancel/{orderId}")
+    @PreAuthorize("@ss.hasPermi('store:externalOrder:cancel')")
+    public R cancel(@PathVariable("orderId") Long orderId) {
+        return fsExternalOrderService.cancelExternalOrder(orderId);
+    }
+
+    @Log(title = "外部订单申请审核", businessType = BusinessType.INSERT)
+    @PreAuthorize("@ss.hasPermi('store:externalOrder:approveOrder')")
+    @PostMapping("/approveOrder")
+    public AjaxResult approveOrder(@RequestBody FsOrderFinanceAuditApplyParam param) {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        Long userId = loginUser.getUser().getUserId();
+        String userName = loginUser.getUser().getUserName();
+        return toAjax(fsStoreOrderService.applyFinanceAudit(
+                param.getOrderIds(),
+                param.getAuditType(),
+                param.getNewTotalPrice(),
+                param.getNewPayPrice(),
+                param.getPriceChangeReason(),
+                param.getVoucherImages(),
+                param.getVoucherRemark(),
+                userId,
+                userName,
+                param.getOrderType()));
+    }
 }

+ 2 - 1
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -455,7 +455,8 @@ public class FsStoreOrderController extends BaseController
                 param.getVoucherImages(),
                 param.getVoucherRemark(),
                 userId, 
-                userName));
+                userName,
+                param.getOrderType()));
     }
 
     @Autowired

+ 8 - 2
fs-company/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -167,12 +167,15 @@ public class DataScopeAspect
                                 " OR {}.user_id IN ( " +
                                 "   SELECT ur.user_id FROM company_user_role ur " +
                                 "   INNER JOIN company_role r ON ur.role_id = r.role_id " +
+                                "   INNER JOIN company_user cu ON ur.user_id = cu.user_id " +
                                 "   WHERE r.company_id = {} " +
+                                "   AND cu.dept_id = {} " +
                                 "   AND (r.role_key IN ('member', 'zuyuan') OR r.role_name LIKE '%组员%') " +
                                 ") ",
                                 userAlias, user.getUserId(),
                                 userAlias,
-                                user.getCompanyId()));
+                                user.getCompanyId(),
+                                user.getDeptId()));
                     }
                 }
                 else if (DATA_SCOPE_MANAGER.equals(dataScope))
@@ -184,13 +187,16 @@ public class DataScopeAspect
                                 " OR {}.user_id IN ( " +
                                 "   SELECT ur.user_id FROM company_user_role ur " +
                                 "   INNER JOIN company_role r ON ur.role_id = r.role_id " +
+                                "   INNER JOIN company_user cu ON ur.user_id = cu.user_id " +
                                 "   WHERE r.company_id = {} " +
+                                "   AND cu.dept_id = {} " +
                                 "   AND (r.role_key IN ('leader', 'zuzhang', 'member', 'zuyuan') " +
                                 "        OR r.role_name LIKE '%组长%' OR r.role_name LIKE '%组员%') " +
                                 ") ",
                                 userAlias, user.getUserId(),
                                 userAlias,
-                                user.getCompanyId()));
+                                user.getCompanyId(),
+                                user.getDeptId()));
                     }
                 }
             }

+ 20 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -365,4 +365,24 @@ public interface CompanyUserMapper
     public int updateCompanyUserByAiSipCall(@Param("companyUserId") Long companyUserId, @Param("aiSipCallId") Long aiSipCallId);
 
     int unbindCidServer(@Param("companyUserId") Long companyUserId);
+
+    @Select("SELECT u.user_id FROM company_user u " +
+            "INNER JOIN company_user_role ur ON u.user_id = ur.user_id " +
+            "INNER JOIN company_role r ON ur.role_id = r.role_id " +
+            "WHERE u.company_id = #{companyId} " +
+            "AND u.del_flag = '0' " +
+            "AND u.status = '0' " +
+            "AND (u.user_type IN ('00', '02') OR r.role_key IN ('admin', 'manager') OR r.role_name LIKE '%管理员%')")
+    List<Long> selectAdminUserIdsByCompanyId(@Param("companyId") Long companyId);
+
+    @Select("SELECT u.user_id FROM company_user u " +
+            "INNER JOIN company_user_role ur ON u.user_id = ur.user_id " +
+            "INNER JOIN company_role r ON ur.role_id = r.role_id " +
+            "WHERE u.company_id = #{companyId} " +
+            "AND u.del_flag = '0' " +
+            "AND u.status = '0' " +
+            "AND (r.role_key IN ('leader', 'zuzhang', 'admin', 'manager') " +
+            "OR r.role_name LIKE '%组长%' OR r.role_name LIKE '%管理员%' " +
+            "OR u.user_type IN ('00', '02'))")
+    List<Long> selectLeaderAndAboveUserIdsByCompanyId(@Param("companyId") Long companyId);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseComplaintRecordMapper.java

@@ -4,6 +4,7 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsUserCourseComplaintRecord;
 import com.fs.course.vo.FsUserCourseComplaintRecordPageListVO;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * 看课投诉记录Mapper接口
@@ -66,4 +67,7 @@ public interface FsUserCourseComplaintRecordMapper extends BaseMapper<FsUserCour
      * @return 结果
      */
     int deleteFsUserCourseComplaintRecordByRecordIds(Long[] recordIds);
+
+    @Select("SELECT * FROM fs_user_course_complaint_record WHERE record_status = 0 ORDER BY create_time ASC")
+    List<FsUserCourseComplaintRecord> selectPendingComplaintList();
 }

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

@@ -1374,7 +1374,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         // 2. 查询指定公司和时间范围内的 APP 会员
         List<AppUserCompanyDTO> appUserList = userMapper.selectAppUserListForActiveCount(param);
         
-        // 3. 如果有APP会员数据,则统计活跃用户数
+        //todo 3. 采用活跃用户表统计
         Map<Long, int[]> companyStatsMap = null;
         if (!CollectionUtils.isEmpty(appUserList)) {
             // 提取所有唯一的 userId 并查询活跃用户

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

@@ -2168,7 +2168,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
             redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
             //记录活跃用户
-            fsAppActiveUserDailyService.recordActiveUserToRedis(param.getUserId());
+            fsAppActiveUserDailyService.recordActiveUserToRedis(param.getUserId(), companyUser.getCompanyId(), param.getCompanyUserId());
         }
         return ResponseResult.ok(fsUser);
     }
@@ -2314,7 +2314,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             redisCache.expire(redisKey, 300, TimeUnit.SECONDS);
 
             //记录活跃用户
-            fsAppActiveUserDailyService.recordActiveUserToRedis(param.getUserId());
+            fsAppActiveUserDailyService.recordActiveUserToRedis(param.getUserId(), companyUser.getCompanyId(), param.getCompanyUserId());
         }
         //导入im好友
         openIMService.checkAndImportFriendByDianBoNew(param.getCompanyUserId(), param.getUserId().toString(), true);

+ 72 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmMsg.java

@@ -44,6 +44,21 @@ public class CrmMsg extends BaseEntity
 
     private Long objId;
 
+    /** 操作跳转URL */
+    private String actionUrl;
+
+    /** 操作跳转参数JSON */
+    private String actionParams;
+
+    /** 目标平台: admin-总后台, company-销售后台 */
+    private String platform;
+
+    /** 优先级: 0-普通 1-重要 2-紧急 */
+    private Integer priority;
+
+    /** 消息过期时间 */
+    private java.util.Date expireTime;
+
     public CrmMsg() {
 
     }
@@ -57,6 +72,17 @@ public class CrmMsg extends BaseEntity
         this.objId = objId;
     }
 
+    public CrmMsg(Integer msgType, String title, String content, Long companyId, Long companyUserId, Long objId, String actionUrl, String platform) {
+        this.msgType = msgType;
+        this.title = title;
+        this.content = content;
+        this.companyId = companyId;
+        this.companyUserId = companyUserId;
+        this.objId = objId;
+        this.actionUrl = actionUrl;
+        this.platform = platform;
+    }
+
     public Long getObjId() {
         return objId;
     }
@@ -65,6 +91,46 @@ public class CrmMsg extends BaseEntity
         this.objId = objId;
     }
 
+    public String getActionUrl() {
+        return actionUrl;
+    }
+
+    public void setActionUrl(String actionUrl) {
+        this.actionUrl = actionUrl;
+    }
+
+    public String getActionParams() {
+        return actionParams;
+    }
+
+    public void setActionParams(String actionParams) {
+        this.actionParams = actionParams;
+    }
+
+    public String getPlatform() {
+        return platform;
+    }
+
+    public void setPlatform(String platform) {
+        this.platform = platform;
+    }
+
+    public Integer getPriority() {
+        return priority;
+    }
+
+    public void setPriority(Integer priority) {
+        this.priority = priority;
+    }
+
+    public java.util.Date getExpireTime() {
+        return expireTime;
+    }
+
+    public void setExpireTime(java.util.Date expireTime) {
+        this.expireTime = expireTime;
+    }
+
     public void setMsgId(Long msgId)
     {
         this.msgId = msgId;
@@ -140,6 +206,12 @@ public class CrmMsg extends BaseEntity
             .append("isRead", getIsRead())
             .append("companyId", getCompanyId())
             .append("companyUserId", getCompanyUserId())
+            .append("objId", getObjId())
+            .append("actionUrl", getActionUrl())
+            .append("actionParams", getActionParams())
+            .append("platform", getPlatform())
+            .append("priority", getPriority())
+            .append("expireTime", getExpireTime())
             .append("updateTime", getUpdateTime())
             .toString();
     }

+ 66 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmMsgMapper.java

@@ -92,4 +92,70 @@ public interface CrmMsgMapper
     String  selectCrmNewMsgByUserId(@Param("userId") Long userId, @Param("type") int type);
     @Update("update  crm_msg set is_read=1 where company_user_id=#{userId} and msg_type=#{type} ")
     int setReadByType(@Param("userId") Long userId, @Param("type") int type);
+
+    @Select("select IFNULL(count(0),0) from crm_msg where (company_user_id=#{userId} or (company_user_id is null and platform=#{platform})) and is_read=0 and (expire_time is null or expire_time > NOW())")
+    Long selectUnreadCountByPlatform(@Param("userId") Long userId, @Param("platform") String platform);
+
+    @Select("select IFNULL(count(0),0) from crm_msg where (company_user_id=#{userId} or (company_user_id is null and platform=#{platform})) and is_read=0 and msg_type=#{type} and (expire_time is null or expire_time > NOW())")
+    Long selectUnreadCountByPlatformAndType(@Param("userId") Long userId, @Param("platform") String platform, @Param("type") int type);
+
+    @Select({"<script> " +
+            "select * from crm_msg " +
+            "where (company_user_id=#{userId} or (company_user_id is null and platform=#{platform})) " +
+            "and is_read=0 " +
+            "and msg_type=#{type} " +
+            "and (expire_time is null or expire_time > NOW()) " +
+            "order by priority desc, msg_id desc " +
+            "limit 1 " +
+            "</script>"})
+    CrmMsg selectLatestUnreadMsg(@Param("userId") Long userId, @Param("platform") String platform, @Param("type") int type);
+
+    @Update("update crm_msg set is_read=1 where (company_user_id=#{userId} or (company_user_id is null and platform=#{platform})) and is_read=0")
+    int markAllAsReadByPlatform(@Param("userId") Long userId, @Param("platform") String platform);
+
+    @Select({"<script> " +
+            "select * from crm_msg " +
+            "where company_user_id is null " +
+            "and platform=#{platform} " +
+            "<if test='type != null'> and msg_type=#{type} </if>" +
+            "and (expire_time is null or expire_time > NOW()) " +
+            "order by priority desc, msg_id desc " +
+            "</script>"})
+    List<CrmMsg> selectBroadcastMsgList(@Param("platform") String platform, @Param("type") Integer type);
+
+    @Select("select IFNULL(count(0),0) from crm_msg where company_user_id is null and platform=#{platform} and is_read=0 and (expire_time is null or expire_time > NOW())")
+    Long selectBroadcastUnreadCount(@Param("platform") String platform);
+
+    @Select("select IFNULL(count(0),0) from crm_msg where company_user_id is null and platform=#{platform} and is_read=0 and msg_type=#{type} and (expire_time is null or expire_time > NOW())")
+    Long selectBroadcastUnreadCountByType(@Param("platform") String platform, @Param("type") int type);
+
+    @Select({"<script> " +
+            "select * from crm_msg " +
+            "where company_user_id is null " +
+            "and platform=#{platform} " +
+            "and is_read=0 " +
+            "and msg_type=#{type} " +
+            "and (expire_time is null or expire_time > NOW()) " +
+            "order by priority desc, msg_id desc " +
+            "limit 1 " +
+            "</script>"})
+    CrmMsg selectLatestBroadcastMsg(@Param("platform") String platform, @Param("type") int type);
+
+    @Update("update crm_msg set is_read=1 where msg_id=#{msgId}")
+    int markMsgAsRead(@Param("msgId") Long msgId);
+
+    @Update("update crm_msg set is_read=1 where company_user_id is null and platform=#{platform} and is_read=0")
+    int markAllBroadcastAsRead(@Param("platform") String platform);
+
+    @Select("select * from crm_msg where company_id=#{companyId} and msg_type=#{type} order by msg_id desc limit 1")
+    CrmMsg selectMsgByCompanyIdAndType(@Param("companyId") Long companyId, @Param("type") int type);
+
+    @Select("select * from crm_msg where obj_id=#{objId} and msg_type=#{type} order by msg_id desc limit 1")
+    CrmMsg selectMsgByObjIdAndType(@Param("objId") Long objId, @Param("type") int type);
+
+    @Select("select IFNULL(count(0),0) from crm_msg where (company_user_id=#{userId} or (company_user_id is null and platform=#{platform})) and msg_type=#{type} and (expire_time is null or expire_time > NOW())")
+    Long selectTotalCountByPlatformAndType(@Param("userId") Long userId, @Param("platform") String platform, @Param("type") int type);
+
+    @Select("select IFNULL(count(0),0) from crm_msg where company_user_id is null and platform=#{platform} and msg_type=#{type} and (expire_time is null or expire_time > NOW())")
+    Long selectBroadcastTotalCountByType(@Param("platform") String platform, @Param("type") int type);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/crm/service/IComplaintMsgService.java

@@ -0,0 +1,25 @@
+package com.fs.crm.service;
+
+import com.fs.course.domain.FsUserCourseComplaintRecord;
+import com.fs.crm.domain.CrmMsg;
+
+/**
+ * 投诉消息服务接口
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+public interface IComplaintMsgService
+{
+    int MSG_TYPE_COMPLAINT = 9;
+
+    void sendComplaintReminder(Long recordId, String complaintContent);
+
+    CrmMsg getLatestUnreadMsg(String platform);
+
+    Long getUnreadCount(String platform);
+
+    void markAsRead(Long msgId);
+
+    void markAllAsRead(String platform);
+}

+ 26 - 0
fs-service/src/main/java/com/fs/crm/service/IIntegralOrderMsgService.java

@@ -0,0 +1,26 @@
+package com.fs.crm.service;
+
+import com.fs.crm.domain.CrmMsg;
+
+/**
+ * 积分商城订单消息服务接口
+ * 
+ * @author fs
+ * @date 2026-04-21
+ */
+public interface IIntegralOrderMsgService
+{
+    void sendIntegralOrderMsg(Long orderId, String orderCode, String userName, String goodsName, String platform);
+
+    void sendIntegralOrderMsgToAdmin(Long orderId, String orderCode, String userName, String goodsName);
+
+    void sendIntegralOrderMsgToCompany(Long orderId, String orderCode, String userName, String goodsName, Long companyId, Long companyUserId);
+
+    CrmMsg getLatestUnreadIntegralOrderMsg(Long userId, String platform);
+
+    Long getUnreadIntegralOrderMsgCount(Long userId, String platform);
+
+    void markIntegralOrderMsgAsRead(Long msgId);
+
+    void markAllIntegralOrderMsgAsRead(Long userId, String platform);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/crm/service/IOrderAuditMsgService.java

@@ -0,0 +1,27 @@
+package com.fs.crm.service;
+
+import com.fs.crm.domain.CrmMsg;
+import com.fs.his.domain.FsStoreOrderFinanceAudit;
+
+/**
+ * 订单审批提醒消息服务接口
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+public interface IOrderAuditMsgService
+{
+    int MSG_TYPE_ORDER_AUDIT = 8;
+
+    void sendOrderAuditReminder(Long companyId,Long auditId, String orderCode, Long auditUserId, String applyUserName, Integer auditType);
+
+    void sendOrderAuditReminderToLeaders(Long companyId, FsStoreOrderFinanceAudit audit);
+
+    CrmMsg getLatestUnreadMsg(Long userId, String platform);
+
+    Long getUnreadCount(Long userId, String platform);
+
+    void markAsRead(Long msgId);
+
+    void markAllAsRead(Long userId, String platform);
+}

+ 28 - 0
fs-service/src/main/java/com/fs/crm/service/IRedPacketBalanceMsgService.java

@@ -0,0 +1,28 @@
+package com.fs.crm.service;
+
+import com.fs.crm.domain.CrmMsg;
+
+import java.math.BigDecimal;
+
+/**
+ * 红包余额不足消息服务接口
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+public interface IRedPacketBalanceMsgService
+{
+    int MSG_TYPE_RED_PACKET_BALANCE = 7;
+
+    void sendRedPacketBalanceWarning(Long companyId, Long companyUserId, BigDecimal balance, BigDecimal threshold);
+
+    void sendRedPacketBalanceWarningToAdmins(Long companyId, BigDecimal balance, BigDecimal threshold);
+
+    CrmMsg getLatestUnreadMsg(Long userId, String platform);
+
+    Long getUnreadCount(Long userId, String platform);
+
+    void markAsRead(Long msgId);
+
+    void markAllAsRead(Long userId, String platform);
+}

+ 91 - 0
fs-service/src/main/java/com/fs/crm/service/impl/ComplaintMsgServiceImpl.java

@@ -0,0 +1,91 @@
+package com.fs.crm.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.FsUserCourseComplaintRecord;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IComplaintMsgService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 投诉消息服务实现
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+@Service
+public class ComplaintMsgServiceImpl implements IComplaintMsgService
+{
+    private static final Logger log = LoggerFactory.getLogger(ComplaintMsgServiceImpl.class);
+
+    private static final String PLATFORM_ADMIN = "admin";
+
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    @Override
+    public void sendComplaintReminder(Long recordId, String complaintContent)
+    {
+        try {
+            CrmMsg msg = new CrmMsg();
+            msg.setMsgType(MSG_TYPE_COMPLAINT);
+            msg.setTitle("投诉提醒");
+            String content = complaintContent;
+            if (content != null && content.length() > 50) {
+                content = content.substring(0, 50) + "...";
+            }
+            msg.setContent(String.format("您有新的投诉待处理,投诉内容: %s,请及时处理", content));
+            msg.setObjId(recordId);
+            msg.setActionUrl("/course/userCourseComplaintRecord");
+            msg.setPlatform(PLATFORM_ADMIN);
+            msg.setPriority(1);
+            msg.setIsRead(0);
+            msg.setCreateTime(DateUtils.getNowDate());
+            msg.setExpireTime(getExpireTime(7));
+            
+            crmMsgMapper.insertCrmMsg(msg);
+            log.info("发送投诉提醒消息成功, 投诉记录ID: {}", recordId);
+        } catch (Exception e) {
+            log.error("发送投诉提醒消息失败, 投诉记录ID: {}", recordId, e);
+        }
+    }
+
+    @Override
+    public CrmMsg getLatestUnreadMsg(String platform)
+    {
+        return crmMsgMapper.selectLatestBroadcastMsg(platform, MSG_TYPE_COMPLAINT);
+    }
+
+    @Override
+    public Long getUnreadCount(String platform)
+    {
+        return crmMsgMapper.selectBroadcastUnreadCountByType(platform, MSG_TYPE_COMPLAINT);
+    }
+
+    @Override
+    public void markAsRead(Long msgId)
+    {
+        CrmMsg msg = new CrmMsg();
+        msg.setMsgId(msgId);
+        msg.setIsRead(1);
+        crmMsgMapper.updateCrmMsg(msg);
+    }
+
+    @Override
+    public void markAllAsRead(String platform)
+    {
+        crmMsgMapper.markAllBroadcastAsRead(platform);
+    }
+
+    private Date getExpireTime(int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, days);
+        return calendar.getTime();
+    }
+}

+ 117 - 0
fs-service/src/main/java/com/fs/crm/service/impl/IntegralOrderMsgServiceImpl.java

@@ -0,0 +1,117 @@
+package com.fs.crm.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IIntegralOrderMsgService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 积分商城订单消息服务实现
+ * 
+ * @author fs
+ * @date 2026-04-21
+ */
+@Service
+public class IntegralOrderMsgServiceImpl implements IIntegralOrderMsgService
+{
+    private static final Logger log = LoggerFactory.getLogger(IntegralOrderMsgServiceImpl.class);
+
+    private static final int MSG_TYPE_INTEGRAL_ORDER = 6;
+
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    @Override
+    public void sendIntegralOrderMsg(Long orderId, String orderCode, String userName, String goodsName, String platform)
+    {
+        try {
+            CrmMsg msg = new CrmMsg();
+            msg.setMsgType(MSG_TYPE_INTEGRAL_ORDER);
+            msg.setTitle("您有新的积分商城订单待处理");
+            msg.setContent(String.format("订单编号: %s, 用户: %s, 商品: %s, 请及时处理", orderCode, userName, goodsName));
+            msg.setObjId(orderId);
+            msg.setActionUrl("/hisStoreorderlist/integralOrder");
+            msg.setPlatform(platform);
+            msg.setPriority(1);
+            msg.setIsRead(0);
+            msg.setCreateTime(DateUtils.getNowDate());
+            msg.setExpireTime(getExpireTime(7));
+            
+            crmMsgMapper.insertCrmMsg(msg);
+            log.info("发送积分商城订单消息成功, 订单ID: {}, 平台: {}", orderId, platform);
+        } catch (Exception e) {
+            log.error("发送积分商城订单消息失败, 订单ID: {}", orderId, e);
+        }
+    }
+
+    @Override
+    public void sendIntegralOrderMsgToAdmin(Long orderId, String orderCode, String userName, String goodsName)
+    {
+        sendIntegralOrderMsg(orderId, orderCode, userName, goodsName, "admin");
+    }
+
+    @Override
+    public void sendIntegralOrderMsgToCompany(Long orderId, String orderCode, String userName, String goodsName, Long companyId, Long companyUserId)
+    {
+        try {
+            CrmMsg msg = new CrmMsg();
+            msg.setMsgType(MSG_TYPE_INTEGRAL_ORDER);
+            msg.setTitle("您有新的积分商城订单待处理");
+            msg.setContent(String.format("订单编号: %s, 用户: %s, 商品: %s, 请及时处理", orderCode, userName, goodsName));
+            msg.setCompanyId(companyId);
+            msg.setCompanyUserId(companyUserId);
+            msg.setObjId(orderId);
+            msg.setActionUrl("/hisStore/integralOrder");
+            msg.setPlatform("company");
+            msg.setPriority(1);
+            msg.setIsRead(0);
+            msg.setCreateTime(DateUtils.getNowDate());
+            msg.setExpireTime(getExpireTime(7));
+            
+            crmMsgMapper.insertCrmMsg(msg);
+            log.info("发送积分商城订单消息到销售后台成功, 订单ID: {}, 公司ID: {}, 用户ID: {}", orderId, companyId, companyUserId);
+        } catch (Exception e) {
+            log.error("发送积分商城订单消息到销售后台失败, 订单ID: {}", orderId, e);
+        }
+    }
+
+    @Override
+    public CrmMsg getLatestUnreadIntegralOrderMsg(Long userId, String platform)
+    {
+        return crmMsgMapper.selectLatestUnreadMsg(userId, platform, MSG_TYPE_INTEGRAL_ORDER);
+    }
+
+    @Override
+    public Long getUnreadIntegralOrderMsgCount(Long userId, String platform)
+    {
+        return crmMsgMapper.selectUnreadCountByPlatformAndType(userId, platform, MSG_TYPE_INTEGRAL_ORDER);
+    }
+
+    @Override
+    public void markIntegralOrderMsgAsRead(Long msgId)
+    {
+        CrmMsg msg = new CrmMsg();
+        msg.setMsgId(msgId);
+        msg.setIsRead(1);
+        crmMsgMapper.updateCrmMsg(msg);
+    }
+
+    @Override
+    public void markAllIntegralOrderMsgAsRead(Long userId, String platform)
+    {
+        crmMsgMapper.markAllAsReadByPlatform(userId, platform);
+    }
+
+    private Date getExpireTime(int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, days);
+        return calendar.getTime();
+    }
+}

+ 109 - 0
fs-service/src/main/java/com/fs/crm/service/impl/OrderAuditMsgServiceImpl.java

@@ -0,0 +1,109 @@
+package com.fs.crm.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IOrderAuditMsgService;
+import com.fs.his.domain.FsStoreOrderFinanceAudit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 订单审批提醒消息服务实现
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+@Service
+public class OrderAuditMsgServiceImpl implements IOrderAuditMsgService
+{
+    private static final Logger log = LoggerFactory.getLogger(OrderAuditMsgServiceImpl.class);
+
+    private static final String PLATFORM_COMPANY = "company";
+
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+
+    @Override
+    public void sendOrderAuditReminder(Long companyId,Long auditId, String orderCode, Long auditUserId, String applyUserName, Integer auditType)
+    {
+        try {
+            String auditTypeName = auditType == 1 ? "价格变更审核" : "订单完成审核";
+            CrmMsg msg = new CrmMsg();
+            msg.setMsgType(MSG_TYPE_ORDER_AUDIT);
+            msg.setTitle("订单审批提醒");
+            msg.setContent(String.format("您有一条%s待审批,订单编号: %s,申请人: %s,请及时处理", auditTypeName, orderCode, applyUserName));
+            msg.setCompanyUserId(auditUserId);
+            msg.setCompanyId(companyId);
+            msg.setObjId(auditId);
+            msg.setActionUrl("money/orderApprove");
+            msg.setPlatform(PLATFORM_COMPANY);
+            msg.setPriority(1);
+            msg.setIsRead(0);
+            msg.setCreateTime(DateUtils.getNowDate());
+            msg.setExpireTime(getExpireTime(7));
+            
+            crmMsgMapper.insertCrmMsg(msg);
+            log.info("发送订单审批提醒消息成功, 审批ID: {}, 审核人ID: {}", auditId, auditUserId);
+        } catch (Exception e) {
+            log.error("发送订单审批提醒消息失败, 审批ID: {}", auditId, e);
+        }
+    }
+
+    @Override
+    public void sendOrderAuditReminderToLeaders(Long companyId, FsStoreOrderFinanceAudit audit)
+    {
+        List<Long> leaderUserIds = companyUserMapper.selectLeaderAndAboveUserIdsByCompanyId(companyId);
+        if (leaderUserIds == null || leaderUserIds.isEmpty()) {
+            log.warn("公司没有组长及以上角色用户, 公司ID: {}", companyId);
+            return;
+        }
+        
+        for (Long userId : leaderUserIds) {
+            sendOrderAuditReminder(companyId, audit.getId(), audit.getOrderCode(), userId, audit.getApplyUserName(), audit.getAuditType());
+        }
+    }
+
+    @Override
+    public CrmMsg getLatestUnreadMsg(Long userId, String platform)
+    {
+        return crmMsgMapper.selectLatestUnreadMsg(userId, platform, MSG_TYPE_ORDER_AUDIT);
+    }
+
+    @Override
+    public Long getUnreadCount(Long userId, String platform)
+    {
+        return crmMsgMapper.selectUnreadCountByPlatformAndType(userId, platform, MSG_TYPE_ORDER_AUDIT);
+    }
+
+    @Override
+    public void markAsRead(Long msgId)
+    {
+        CrmMsg msg = new CrmMsg();
+        msg.setMsgId(msgId);
+        msg.setIsRead(1);
+        crmMsgMapper.updateCrmMsg(msg);
+    }
+
+    @Override
+    public void markAllAsRead(Long userId, String platform)
+    {
+        crmMsgMapper.markAllAsReadByPlatform(userId, platform);
+    }
+
+    private Date getExpireTime(int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, days);
+        return calendar.getTime();
+    }
+}

+ 106 - 0
fs-service/src/main/java/com/fs/crm/service/impl/RedPacketBalanceMsgServiceImpl.java

@@ -0,0 +1,106 @@
+package com.fs.crm.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.mapper.CrmMsgMapper;
+import com.fs.crm.service.IRedPacketBalanceMsgService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 红包余额不足消息服务实现
+ * 
+ * @author fs
+ * @date 2026-04-22
+ */
+@Service
+public class RedPacketBalanceMsgServiceImpl implements IRedPacketBalanceMsgService
+{
+    private static final Logger log = LoggerFactory.getLogger(RedPacketBalanceMsgServiceImpl.class);
+
+    private static final String PLATFORM_COMPANY = "company";
+
+    @Autowired
+    private CrmMsgMapper crmMsgMapper;
+
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+
+    @Override
+    public void sendRedPacketBalanceWarning(Long companyId, Long companyUserId, BigDecimal balance, BigDecimal threshold)
+    {
+        try {
+            CrmMsg msg = new CrmMsg();
+            msg.setMsgType(MSG_TYPE_RED_PACKET_BALANCE);
+            msg.setTitle("红包余额不足提醒");
+            msg.setContent(String.format("您的红包余额不足,当前余额: %.2f元,预警阈值: %.2f元,请及时充值!", balance, threshold));
+            msg.setCompanyId(companyId);
+            msg.setCompanyUserId(companyUserId);
+            msg.setPlatform(PLATFORM_COMPANY);
+            msg.setPriority(2);
+            msg.setIsRead(0);
+            msg.setCreateTime(DateUtils.getNowDate());
+            msg.setExpireTime(getExpireTime(1));
+            
+            crmMsgMapper.insertCrmMsg(msg);
+            log.info("发送红包余额不足消息成功, 公司ID: {}, 用户ID: {}, 余额: {}", companyId, companyUserId, balance);
+        } catch (Exception e) {
+            log.error("发送红包余额不足消息失败, 公司ID: {}", companyId, e);
+        }
+    }
+
+    @Override
+    public void sendRedPacketBalanceWarningToAdmins(Long companyId, BigDecimal balance, BigDecimal threshold)
+    {
+        List<Long> adminUserIds = companyUserMapper.selectAdminUserIdsByCompanyId(companyId);
+        if (adminUserIds == null || adminUserIds.isEmpty()) {
+            log.warn("公司没有管理员用户, 公司ID: {}", companyId);
+            return;
+        }
+        
+        for (Long userId : adminUserIds) {
+            sendRedPacketBalanceWarning(companyId, userId, balance, threshold);
+        }
+    }
+
+    @Override
+    public CrmMsg getLatestUnreadMsg(Long userId, String platform)
+    {
+        return crmMsgMapper.selectLatestUnreadMsg(userId, platform, MSG_TYPE_RED_PACKET_BALANCE);
+    }
+
+    @Override
+    public Long getUnreadCount(Long userId, String platform)
+    {
+        return crmMsgMapper.selectUnreadCountByPlatformAndType(userId, platform, MSG_TYPE_RED_PACKET_BALANCE);
+    }
+
+    @Override
+    public void markAsRead(Long msgId)
+    {
+        CrmMsg msg = new CrmMsg();
+        msg.setMsgId(msgId);
+        msg.setIsRead(1);
+        crmMsgMapper.updateCrmMsg(msg);
+    }
+
+    @Override
+    public void markAllAsRead(Long userId, String platform)
+    {
+        crmMsgMapper.markAllAsReadByPlatform(userId, platform);
+    }
+
+    private Date getExpireTime(int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, days);
+        return calendar.getTime();
+    }
+}

+ 6 - 0
fs-service/src/main/java/com/fs/his/domain/FsAppActiveUserDaily.java

@@ -25,6 +25,12 @@ public class FsAppActiveUserDaily {
     /** 用户ID */
     private Long userId;
 
+    /** 公司ID */
+    private Long companyId;
+
+    /** 销售ID */
+    private Long companyUserId;
+
     /** 创建时间 */
     private Date createTime;
 }

+ 1 - 1
fs-service/src/main/java/com/fs/his/domain/FsExternalOrder.java

@@ -60,7 +60,7 @@ public class FsExternalOrder extends BaseEntity
     @Excel(name = "支付方式 1微信 2支付宝 3其他")
     private Integer payType;
 
-    @Excel(name = "订单状态 1待支付 2待发货 3待收货 4已完成 5已取消")
+    @Excel(name = "订单状态 1待审核 2待发货 3待收货 4已完成 5已取消")
     private Integer status;
 
     @Excel(name = "快递公司编号")

+ 3 - 0
fs-service/src/main/java/com/fs/his/domain/FsStoreOrderFinanceAudit.java

@@ -62,6 +62,9 @@ public class FsStoreOrderFinanceAudit extends BaseEntity {
     @Excel(name = "凭证说明")
     private String voucherRemark;
 
+    @Excel(name = "订单类型", readConverterExp = "0=外部订单审核,1=普通订单审核")
+    private Integer orderType;
+
     @Excel(name = "申请人ID")
     private Long applyUserId;
 

+ 2 - 0
fs-service/src/main/java/com/fs/his/domain/vo/FsExternalOrderListVO.java

@@ -35,4 +35,6 @@ public class FsExternalOrderListVO {
     private Integer status;
 
     private String statusDesc;
+
+    private Boolean isApplyAudit;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/his/dto/ExternalOrderProductDTO.java

@@ -0,0 +1,9 @@
+package com.fs.his.dto;
+
+import lombok.Data;
+
+@Data
+public class ExternalOrderProductDTO {
+    private Long productId;
+    private Long num;
+}

+ 27 - 1
fs-service/src/main/java/com/fs/his/mapper/FsAppActiveUserDailyMapper.java

@@ -10,13 +10,21 @@ import org.apache.ibatis.annotations.Param;
 public interface FsAppActiveUserDailyMapper extends BaseMapper<FsAppActiveUserDaily> {
 
     /**
-     * 批量插入活跃用户
+     * 批量插入活跃用户(旧方法,兼容)
      * @param statDate 统计日期
      * @param userIds 用户ID列表
      * @return 插入数量
      */
     int batchInsert(@Param("statDate") String statDate, @Param("userIds") java.util.List<Long> userIds);
 
+    /**
+     * 批量插入活跃用户(带公司ID和销售ID)
+     * @param statDate 统计日期
+     * @param dataList 活跃用户数据列表
+     * @return 插入数量
+     */
+    int batchInsertWithCompany(@Param("statDate") String statDate, @Param("dataList") java.util.List<FsAppActiveUserDaily> dataList);
+
     /**
      * 查询指定日期范围的活跃用户数
      * @param startDate 开始日期
@@ -25,6 +33,24 @@ public interface FsAppActiveUserDailyMapper extends BaseMapper<FsAppActiveUserDa
      */
     Long selectActiveUserCount(@Param("startDate") String startDate, @Param("endDate") String endDate);
 
+    /**
+     * 查询指定日期范围和公司的活跃用户数
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param companyId 公司ID
+     * @return 活跃用户数
+     */
+    Long selectActiveUserCountByCompany(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("companyId") Long companyId);
+
+    /**
+     * 查询指定日期范围和销售的活跃用户数
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param companyUserId 销售ID
+     * @return 活跃用户数
+     */
+    Long selectActiveUserCountByCompanyUser(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("companyUserId") Long companyUserId);
+
     /**
      * 查询指定日期的活跃用户ID列表
      * @param statDate 统计日期

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

@@ -30,4 +30,7 @@ public interface FsExternalOrderMapper {
 
     @Select("SELECT order_id FROM fs_external_order WHERE extend_order_id is null AND status =2")
     List<Long> selectUnsyncedOrderIds();
+
+    @Select("SELECT * FROM fs_external_order WHERE extend_order_id IS NOT NULL AND status IN (2, 3)")
+    List<FsExternalOrder> selectSyncedOrders();
 }

+ 6 - 0
fs-service/src/main/java/com/fs/his/mapper/FsStoreMapper.java

@@ -69,4 +69,10 @@ public interface FsStoreMapper
 
     @Select("SELECT * from fs_store WHERE   FIND_IN_SET(store_id, (SELECT store_ids FROM fs_doctor where doctor_id=#{doctorId}))")
     List<FsStore> selectFsStoreListByDoctorId(Long doctorId);
+
+    /**
+     * 根据店铺编号查询
+     */
+    @Select("SELECT * from fs_store WHERE  shop_code=#{shopCode}")
+    FsStore selectFsStoreByShopCode(String shopCode);
 }

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

@@ -2,6 +2,7 @@ package com.fs.his.mapper;
 
 import com.fs.his.domain.FsStoreOrderFinanceAudit;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
 
@@ -23,4 +24,7 @@ public interface FsStoreOrderFinanceAuditMapper {
     int deleteFsStoreOrderFinanceAuditById(Long id);
 
     int deleteFsStoreOrderFinanceAuditByIds(Long[] ids);
+
+    @Select("SELECT * FROM fs_store_order_finance_audit WHERE audit_status = 0 ORDER BY create_time ASC")
+    List<FsStoreOrderFinanceAudit> selectPendingAuditList();
 }

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

@@ -404,6 +404,9 @@ public interface FsUserMapper
 
     Integer getTodayNewUser(@Param("userId") Long userId, @Param("companyId") Long companyId);
 
+    Integer getNormalUserCount(@Param("userId") Long userId, @Param("companyId") Long companyId);
+
+    Integer getBlacklistUserCount(@Param("userId") Long userId, @Param("companyId") Long companyId);
 
     List<FsUserSummaryCountTagVO> countTag(@Param("userId") Long userId, @Param("companyId") Long companyId);
 

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

@@ -1,5 +1,6 @@
 package com.fs.his.param;
 
+import com.fs.his.dto.ExternalOrderProductDTO;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -32,7 +33,8 @@ public class FsExternalOrderAddParam {
 
     private String remark;
 
-    private List<Long> productIds;
+    private List<ExternalOrderProductDTO> products;
 
     private Long addressId;
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/param/FsOrderFinanceAuditApplyParam.java

@@ -28,4 +28,6 @@ public class FsOrderFinanceAuditApplyParam {
     private String voucherImages;
 
     private String voucherRemark;
+
+    private Integer orderType;
 }

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

@@ -15,6 +15,14 @@ public interface IFsAppActiveUserDailyService {
      */
     void recordActiveUserToRedis(Long userId);
 
+    /**
+     * 记录用户活跃到Redis(带公司ID和销售ID)
+     * @param userId 用户ID
+     * @param companyId 公司ID
+     * @param companyUserId 销售ID
+     */
+    void recordActiveUserToRedis(Long userId, Long companyId, Long companyUserId);
+
     /**
      * 同步Redis活跃用户到数据库
      */
@@ -27,4 +35,22 @@ public interface IFsAppActiveUserDailyService {
      * @return 活跃用户数
      */
     Long getActiveUserCount(String startDate, String endDate);
+
+    /**
+     * 查询指定日期范围和公司的活跃用户数
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param companyId 公司ID
+     * @return 活跃用户数
+     */
+    Long getActiveUserCount(String startDate, String endDate, Long companyId);
+
+    /**
+     * 查询指定日期范围和销售的活跃用户数
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param companyUserId 销售ID
+     * @return 活跃用户数
+     */
+    Long getActiveUserCountByCompanyUser(String startDate, String endDate, Long companyUserId);
 }

+ 19 - 0
fs-service/src/main/java/com/fs/his/service/IFsExternalOrderService.java

@@ -39,4 +39,23 @@ public interface IFsExternalOrderService {
      * 批量推送外部订单到聚水潭
      */
     void pushToJstBatch(List<Long> orderIds) throws Exception;
+
+    /**
+     * 同步外部订单状态(从聚水潭)
+     */
+    void syncOrderStatusFromJst() throws Exception;
+
+    /**
+     * 审核外部订单
+     * @param orderId 订单ID
+     * @return 结果
+     */
+    R auditExternalOrder(Long orderId);
+
+    /**
+     * 取消外部订单
+     * @param orderId 订单ID
+     * @return 结果
+     */
+    R cancelExternalOrder(Long orderId);
 }

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

@@ -305,7 +305,7 @@ public interface IFsStoreOrderService
     int applyFinanceAudit(List<Long> orderIds, Integer auditType,
                           BigDecimal newTotalPrice, BigDecimal newPayPrice,
                           String priceChangeReason, String voucherImages, String voucherRemark,
-                          Long applyUserId, String applyUserName);
+                          Long applyUserId, String applyUserName, Integer orderType);
 
     /**
      * 财务审核

+ 11 - 0
fs-service/src/main/java/com/fs/his/service/IFsStoreProductService.java

@@ -113,4 +113,15 @@ public interface IFsStoreProductService
      * 根据商品编码 获取erp商品信息更新商品及属性等信息
      */
     int updateStoreProductByErpProductCode();
+
+    /**
+     * 按ERP修改时间区间同步商品,自动拆分7天窗口并处理分页
+     */
+    int syncProductFromErp(String modifiedBegin, String modifiedEnd, Integer pageSize);
+
+    /**
+     * 批量修复已入库ERP图片地址
+     */
+    int replaceErpImageUrls();
+
 }

+ 64 - 12
fs-service/src/main/java/com/fs/his/service/impl/FsAppActiveUserDailyServiceImpl.java

@@ -32,22 +32,35 @@ public class FsAppActiveUserDailyServiceImpl implements IFsAppActiveUserDailySer
     private FsAppActiveUserDailyMapper fsAppActiveUserDailyMapper;
 
     /**
-     * 记录用户活跃到Redis
+     * 记录用户活跃到Redis(兼容旧方法)
      * 使用Set结构存储,自动去重
      * @param userId 用户ID
      */
     @Override
     public void recordActiveUserToRedis(Long userId) {
+        recordActiveUserToRedis(userId, null, null);
+    }
+
+    /**
+     * 记录用户活跃到Redis(带公司ID和销售ID)
+     * 存储格式:companyId:companyUserId:userId
+     * @param userId 用户ID
+     * @param companyId 公司ID
+     * @param companyUserId 销售ID
+     */
+    @Override
+    public void recordActiveUserToRedis(Long userId, Long companyId, Long companyUserId) {
         try {
             String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
             String redisKey = REDIS_KEY_PREFIX + today;
-            Set<Long> userIdSet = new HashSet<>();
-            userIdSet.add(userId);
-            redisCache.setCacheSet(redisKey, userIdSet);
+            String value = (companyId != null ? companyId : 0) + ":" + (companyUserId != null ? companyUserId : 0) + ":" + userId;
+            Set<String> valueSet = new HashSet<>();
+            valueSet.add(value);
+            redisCache.setCacheSet(redisKey, valueSet);
             redisCache.expire(redisKey, 86400 * 2);
-            log.debug("记录活跃用户到Redis: userId={}, date={}", userId, today);
+            log.debug("记录活跃用户到Redis: userId={}, companyId={}, companyUserId={}, date={}", userId, companyId, companyUserId, today);
         } catch (Exception e) {
-            log.error("记录活跃用户到Redis失败: userId={}", userId, e);
+            log.error("记录活跃用户到Redis失败: userId={}, companyId={}, companyUserId={}", userId, companyId, companyUserId, e);
         }
     }
 
@@ -63,22 +76,37 @@ public class FsAppActiveUserDailyServiceImpl implements IFsAppActiveUserDailySer
             String yesterday = new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime());
             
             String redisKey = REDIS_KEY_PREFIX + yesterday;
-            Set<Long> userIds = redisCache.getCacheSet(redisKey);
+            Set<String> values = redisCache.getCacheSet(redisKey);
             
-            if (userIds == null || userIds.isEmpty()) {
+            if (values == null || values.isEmpty()) {
                 log.info("昨日无活跃用户数据: date={}", yesterday);
                 return;
             }
             
-            List<Long> userIdList = new ArrayList<>(userIds);
+            List<FsAppActiveUserDaily> dataList = new ArrayList<>();
+            for (String value : values) {
+                try {
+                    String[] parts = value.split(":");
+                    if (parts.length == 3) {
+                        FsAppActiveUserDaily data = new FsAppActiveUserDaily();
+                        data.setCompanyId(Long.parseLong(parts[0]));
+                        data.setCompanyUserId(Long.parseLong(parts[1]));
+                        data.setUserId(Long.parseLong(parts[2]));
+                        dataList.add(data);
+                    }
+                } catch (Exception e) {
+                    log.warn("解析活跃用户数据失败: value={}", value, e);
+                }
+            }
+            
             int batchSize = 1000;
-            int total = userIdList.size();
+            int total = dataList.size();
             int inserted = 0;
             
             for (int i = 0; i < total; i += batchSize) {
                 int end = Math.min(i + batchSize, total);
-                List<Long> batch = userIdList.subList(i, end);
-                inserted += fsAppActiveUserDailyMapper.batchInsert(yesterday, batch);
+                List<FsAppActiveUserDaily> batch = dataList.subList(i, end);
+                inserted += fsAppActiveUserDailyMapper.batchInsertWithCompany(yesterday, batch);
             }
             
             log.info("同步活跃用户数据完成: date={}, total={}, inserted={}", yesterday, total, inserted);
@@ -100,4 +128,28 @@ public class FsAppActiveUserDailyServiceImpl implements IFsAppActiveUserDailySer
     public Long getActiveUserCount(String startDate, String endDate) {
         return fsAppActiveUserDailyMapper.selectActiveUserCount(startDate, endDate);
     }
+
+    /**
+     * 查询指定日期范围和公司的活跃用户数
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param companyId 公司ID
+     * @return 活跃用户数
+     */
+    @Override
+    public Long getActiveUserCount(String startDate, String endDate, Long companyId) {
+        return fsAppActiveUserDailyMapper.selectActiveUserCountByCompany(startDate, endDate, companyId);
+    }
+
+    /**
+     * 查询指定日期范围和销售的活跃用户数
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param companyUserId 销售ID
+     * @return 活跃用户数
+     */
+    @Override
+    public Long getActiveUserCountByCompanyUser(String startDate, String endDate, Long companyUserId) {
+        return fsAppActiveUserDailyMapper.selectActiveUserCountByCompanyUser(startDate, endDate, companyUserId);
+    }
 }

+ 172 - 13
fs-service/src/main/java/com/fs/his/service/impl/FsExternalOrderServiceImpl.java

@@ -3,11 +3,7 @@ package com.fs.his.service.impl;
 import java.math.BigDecimal;
 import java.sql.Timestamp;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import javax.annotation.PostConstruct;
 
@@ -20,9 +16,13 @@ import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.common.core.domain.R;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrder;
 import com.fs.erp.domain.ErpOrderItem;
 import com.fs.erp.domain.ErpOrderPayment;
+import com.fs.erp.domain.ErpOrderQuery;
+import com.fs.erp.dto.ErpOrderQueryRequert;
+import com.fs.erp.dto.ErpOrderQueryResponse;
 import com.fs.erp.dto.ErpOrderResponseDTO;
 import com.fs.erp.dto.OrderItemDTO;
 import com.fs.erp.dto.PaymentDTO;
@@ -38,6 +38,7 @@ import com.fs.his.domain.FsStoreProduct;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserAddress;
 import com.fs.his.domain.vo.FsExternalOrderListVO;
+import com.fs.his.dto.ExternalOrderProductDTO;
 import com.fs.his.mapper.FsExternalOrderMapper;
 import com.fs.his.mapper.FsExternalOrderItemMapper;
 import com.fs.his.mapper.FsStoreMapper;
@@ -182,11 +183,26 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
             return R.error("订单号已存在");
         }
 
-        if (CollectionUtils.isEmpty(param.getProductIds())) {
+        if (CollectionUtils.isEmpty(param.getProducts())) {
             return R.error("商品列表不能为空");
         }
 
-        List<FsStoreProduct> storeProductList = fsStoreProductMapper.getStoreProductInProductIds(param.getProductIds());
+        // 构建产品ID列表和数量映射
+        List<Long> productIds = new ArrayList<>();
+        Map<Long, Long> productNumMap = new HashMap<>();
+        for (ExternalOrderProductDTO productDTO : param.getProducts()) {
+            if (productDTO.getProductId() != null) {
+                productIds.add(productDTO.getProductId());
+                Long num = productDTO.getNum() != null && productDTO.getNum() > 0 ? productDTO.getNum() : 1L;
+                productNumMap.put(productDTO.getProductId(), num);
+            }
+        }
+
+        if (CollectionUtils.isEmpty(productIds)) {
+            return R.error("商品列表不能为空");
+        }
+
+        List<FsStoreProduct> storeProductList = fsStoreProductMapper.getStoreProductInProductIds(productIds);
         if (CollectionUtils.isEmpty(storeProductList)) {
             return R.error("商品不存在");
         }
@@ -206,7 +222,7 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
         order.setCompanyId(param.getCompanyId());
         order.setCompanyUserId(param.getCompanyUserId());
         order.setRemark(param.getRemark());
-        order.setStatus(2);
+        order.setStatus(1);
         order.setIsPay(1);
         order.setIsSync(0);
         order.setShippingType(1);
@@ -217,7 +233,7 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
         List<FsExternalOrderItem> items = new ArrayList<>();
         Long storeId = 0L;
         for (FsStoreProduct product : storeProductList) {
-            Long num = 1L;
+            Long num = productNumMap.getOrDefault(product.getProductId(), 1L);
             BigDecimal price = product.getPrice() != null ? product.getPrice() : BigDecimal.ZERO;
             BigDecimal itemTotalPrice = price.multiply(BigDecimal.valueOf(num));
 
@@ -357,7 +373,7 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
             return null;
         }
 
-        FsStore fsStore = fsStoreMapper.selectFsStoreByStoreId(order.getStoreId());
+        FsStore fsStore = fsStoreMapper.selectFsStoreByShopCode(order.getStoreId().toString());
         if (fsStore == null) {
             log.warn("外部订单店铺不存在: {}", order.getStoreId());
             return null;
@@ -420,6 +436,11 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
                     shopOrderDTO.setReceiverAddress((String) addData.get("StreetName") + addData.get("Address"));
                 }
             } catch (Exception e) {
+                //默认帮我解析一下地址 给这几个字段赋值
+                shopOrderDTO.setReceiverState(Arrays.toString(address));
+                shopOrderDTO.setReceiverCity(Arrays.toString(address));
+                shopOrderDTO.setReceiverDistrict(Arrays.toString(address));
+                shopOrderDTO.setReceiverAddress(Arrays.toString(address));
                 log.error("地址解析失败", e);
             }
         } else {
@@ -437,9 +458,9 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
             }
         }
 
-        if (shopOrderDTO.getReceiverAddress() != null) {
-            shopOrderDTO.setReceiverAddress(shopOrderDTO.getReceiverAddress().replace("+", "加").replace("\n", ""));
-        }
+//        if (shopOrderDTO.getReceiverAddress() != null) {
+//            shopOrderDTO.setReceiverAddress(shopOrderDTO.getReceiverAddress().replace("+", "加").replace("\n", ""));
+//        }
 
         List<FsExternalOrderItem> orderItems = fsExternalOrderItemMapper.selectFsExternalOrderItemByOrderId(order.getOrderId());
         List<OrderItemDTO> itemDTOList = new ArrayList<>();
@@ -469,4 +490,142 @@ public class FsExternalOrderServiceImpl implements IFsExternalOrderService {
 
         return shopOrderDTO;
     }
+
+    @Override
+    public void syncOrderStatusFromJst() throws Exception {
+        FsSysConfig sysConfig = configUtil.getSysConfig();
+        Integer erpOpen = sysConfig.getErpOpen();
+        if (erpOpen == null || erpOpen == 0) {
+            log.info("ERP未开启,跳过外部订单状态同步");
+            return;
+        }
+
+        Integer erpType = sysConfig.getErpType();
+        if (erpType == null || erpType != 5) {
+            log.info("ERP类型不是聚水潭,跳过同步: {}", erpType);
+            return;
+        }
+
+        List<FsExternalOrder> syncedOrders = fsExternalOrderMapper.selectSyncedOrders();
+        if (CollectionUtils.isEmpty(syncedOrders)) {
+            log.info("没有需要同步状态的外部订单");
+            return;
+        }
+
+        log.info("开始同步外部订单状态,订单数: {}", syncedOrders.size());
+
+        Map<Long, FsStore> storeMap = new HashMap<>();
+        for (FsExternalOrder order : syncedOrders) {
+            try {
+                if (order.getStoreId() == null || StringUtils.isBlank(order.getExtendOrderId())) {
+                    continue;
+                }
+
+                FsStore fsStore = storeMap.get(order.getStoreId());
+                if (fsStore == null) {
+                    fsStore = fsStoreMapper.selectFsStoreByShopCode(String.valueOf(order.getStoreId()));
+                    if (fsStore != null) {
+                        storeMap.put(order.getStoreId(), fsStore);
+                    }
+                }
+
+                if (fsStore == null || StringUtils.isBlank(fsStore.getShopCode())) {
+                    log.warn("外部订单店铺不存在或店铺编码为空: {}", order.getOrderId());
+                    continue;
+                }
+
+                ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+                request.setShop_id(fsStore.getShopCode());
+                request.setO_ids(Collections.singletonList(Long.valueOf(order.getExtendOrderId())));
+
+                ErpOrderQueryResponse response = jstOrderService.getOrder(request);
+                if (response == null || response.getOrders() == null) {
+                    log.warn("查询聚水潭订单失败: {}", order.getOrderCode());
+                    continue;
+                }
+
+                for (ErpOrderQuery orderQuery : response.getOrders()) {
+                    if (orderQuery.getDeliverys() != null && !orderQuery.getDeliverys().isEmpty()) {
+                        for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+                            if (delivery.getDelivery() && StringUtils.isNotEmpty(delivery.getMail_no())) {
+                                updateOrderDelivery(order, delivery);
+                            }
+                        }
+                    }
+                }
+
+                Thread.sleep(200);
+            } catch (Exception e) {
+                log.error("同步外部订单状态异常: {}", order.getOrderCode(), e);
+            }
+        }
+
+        log.info("外部订单状态同步完成");
+    }
+
+    private void updateOrderDelivery(FsExternalOrder localOrder, ErpDeliverys delivery) {
+        if (localOrder.getStatus() != 2) {
+            return;
+        }
+
+        FsExternalOrder updateOrder = new FsExternalOrder();
+        updateOrder.setOrderId(localOrder.getOrderId());
+        updateOrder.setStatus(3);
+        updateOrder.setDeliverySn(delivery.getMail_no());
+
+        if (StringUtils.isNotBlank(delivery.getExpress_code())) {
+            FsExpress express = expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
+            if (express != null) {
+                updateOrder.setDeliveryName(express.getName());
+                updateOrder.setDeliveryCode(express.getCode());
+            } else {
+                updateOrder.setDeliveryCode(delivery.getExpress_code());
+                if (StringUtils.isNotBlank(delivery.getExpress_name())) {
+                    updateOrder.setDeliveryName(delivery.getExpress_name());
+                }
+            }
+        }
+
+        updateOrder.setDeliveryTime(new Date());
+        fsExternalOrderMapper.updateFsExternalOrder(updateOrder);
+        log.info("外部订单已发货: {}, 快递: {}, 单号: {}", localOrder.getOrderCode(), delivery.getExpress_code(), delivery.getMail_no());
+    }
+
+    @Override
+    public R auditExternalOrder(Long orderId) {
+        FsExternalOrder order = fsExternalOrderMapper.selectFsExternalOrderByOrderId(orderId);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (order.getStatus() != 1) {
+            return R.error("订单不是待审核状态,无法审核");
+        }
+        FsExternalOrder updateOrder = new FsExternalOrder();
+        updateOrder.setOrderId(orderId);
+        updateOrder.setStatus(2);
+        int result = fsExternalOrderMapper.updateFsExternalOrder(updateOrder);
+        if (result > 0) {
+            return R.ok("审核成功");
+        }
+        return R.error("审核失败");
+    }
+
+    @Override
+    public R cancelExternalOrder(Long orderId) {
+        FsExternalOrder order = fsExternalOrderMapper.selectFsExternalOrderByOrderId(orderId);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (order.getStatus() == 4) {
+            return R.error("已完成订单无法取消");
+        }
+        FsExternalOrder updateOrder = new FsExternalOrder();
+        updateOrder.setOrderId(orderId);
+        updateOrder.setStatus(-3);
+        int result = fsExternalOrderMapper.updateFsExternalOrder(updateOrder);
+        if (result > 0) {
+            return R.ok("取消订单成功");
+        }
+        return R.error("取消订单失败");
+    }
 }

+ 34 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -202,6 +202,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService {
     @Autowired
     private FsShippingTemplatesRegionScrmMapper fsShippingTemplatesRegionScrmMapper;
 
+    @Autowired
+    private com.fs.crm.service.IIntegralOrderMsgService integralOrderMsgService;
+
     //ERP 类型到服务的映射
     private Map<Integer, IErpOrderService> erpServiceMap;
 
@@ -468,6 +471,22 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService {
         }
 
         if (fsIntegralOrderMapper.insertFsIntegralOrder(order) > 0) {
+            // 发送订单消息通知
+            try {
+                String goodsName = goodsItem.get(0).getGoodsName();
+                
+                // 发送消息到总后台
+                integralOrderMsgService.sendIntegralOrderMsgToAdmin(
+                    order.getOrderId(),
+                    order.getOrderCode(),
+                    order.getUserName(),
+                    goodsName
+                );
+
+            } catch (Exception e) {
+                log.error("发送积分订单消息失败", e);
+            }
+            
             if (userCouponId != null) {
                 // 使用优惠券
                 FsUserCoupon updateUserCoupon = new FsUserCoupon();
@@ -1492,6 +1511,21 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService {
             order.setUserCouponId(userCoupon.getCouponId());
         }
         if (fsIntegralOrderMapper.insertFsIntegralOrder(order) > 0) {
+            // 发送订单消息通知
+            try {
+                String goodsName = goodsList.isEmpty() ? "积分商品" : goodsList.get(0).getGoodsName();
+                
+                // 发送消息到总后台
+                integralOrderMsgService.sendIntegralOrderMsgToAdmin(
+                    order.getOrderId(),
+                    order.getOrderCode(),
+                    order.getUserName(),
+                    goodsName
+                );
+            } catch (Exception e) {
+                log.error("发送积分订单消息失败", e);
+            }
+            
             if (order.getPayType() != 2 && totalIntegral > 0) {
                 FsUser userMap = new FsUser();
                 userMap.setUserId(user.getUserId());

+ 2 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -4735,7 +4735,7 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
     public int applyFinanceAudit(List<Long> orderIds, Integer auditType, 
                                   BigDecimal newTotalPrice, BigDecimal newPayPrice,
                                   String priceChangeReason, String voucherImages, String voucherRemark,
-                                  Long applyUserId, String applyUserName) {
+                                  Long applyUserId, String applyUserName, Integer orderType) {
         int count = 0;
         for (Long orderId : orderIds) {
             FsStoreOrder order = fsStoreOrderMapper.selectFsStoreOrderByOrderId(orderId);
@@ -4759,6 +4759,7 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             audit.setPriceChangeReason(priceChangeReason);
             audit.setVoucherImages(voucherImages);
             audit.setVoucherRemark(voucherRemark);
+            audit.setOrderType(orderType != null ? orderType : 1);
             audit.setAuditStatus(0);
             audit.setApplyUserId(applyUserId);
             audit.setApplyUserName(applyUserName);

+ 264 - 24
fs-service/src/main/java/com/fs/his/service/impl/FsStoreProductServiceImpl.java

@@ -1,6 +1,10 @@
 package com.fs.his.service.impl;
 
 import java.math.BigDecimal;
+import java.net.URL;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -40,6 +44,7 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.*;
 import com.fs.hisStore.vo.FsStoreProductListVO;
 import com.fs.live.domain.LiveGoods;
+import com.fs.system.oss.OSSFactory;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -57,6 +62,9 @@ import org.springframework.transaction.annotation.Transactional;
  */
 @Service
 public class FsStoreProductServiceImpl implements IFsStoreProductService {
+    private static final DateTimeFormatter ERP_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final int MAX_QUERY_RANGE_DAYS = 7;
+    private static final int DEFAULT_ERP_PAGE_SIZE = 100;
     @Autowired
     private FsStoreProductMapper fsStoreProductMapper;
 
@@ -890,9 +898,261 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
     }
 
     /**
-     * 批量获取商品编码 获取erp商品信息更新商品及属性等信息
-     * @return
+     * 按ERP修改时间区间同步商品,自动拆分7天窗口并处理分页。
+     * 同步字段与 updateStoreProductByErpProductCode 保持一致:
+     * 商品:productName、price、costPrice、otPrice、imgUrl、unitName、brand、prescribeSpec、prescribeFactory
+     * 规格:price、cost、otPrice、image、weight
      */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int syncProductFromErp(String modifiedBegin, String modifiedEnd, Integer pageSize) {
+        LocalDateTime begin = parseDateTime(modifiedBegin);
+        LocalDateTime end = parseDateTime(modifiedEnd);
+        if (begin.isAfter(end)) {
+            throw new ServiceException("modified_begin 不能大于 modified_end");
+        }
+
+        int actualPageSize = normalizePageSize(pageSize);
+        int totalSyncCount = 0;
+        LocalDateTime windowBegin = begin;
+
+        while (!windowBegin.isAfter(end)) {
+            LocalDateTime windowEnd = windowBegin.plusDays(MAX_QUERY_RANGE_DAYS).minusSeconds(1);
+            if (windowEnd.isAfter(end)) {
+                windowEnd = end;
+            }
+
+            totalSyncCount += syncProductWindow(windowBegin, windowEnd, actualPageSize);
+            windowBegin = windowEnd.plusSeconds(1);
+        }
+        return totalSyncCount;
+    }
+
+    private int syncProductWindow(LocalDateTime windowBegin, LocalDateTime windowEnd, int pageSize) {
+        int pageIndex = 1;
+        int syncCount = 0;
+        boolean hasNext = true;
+
+        while (hasNext) {
+            ProductQueryRequestDTO queryDTO = new ProductQueryRequestDTO();
+            queryDTO.setModifiedBegin(formatDateTime(windowBegin));
+            queryDTO.setModifiedEnd(formatDateTime(windowEnd));
+            queryDTO.setPageIndex(pageIndex);
+            queryDTO.setPageSize(pageSize);
+            queryDTO.setDateField("modified");
+
+            ProductResponseDTO response = jstErpHttpService.queryGoods(queryDTO);
+            if (response == null || response.getDatas() == null || response.getDatas().isEmpty()) {
+                break;
+            }
+
+            for (ProductResponseDTO.ProductInfo erpProduct : response.getDatas()) {
+                if (StringUtils.isBlank(erpProduct.getSkuId())) {
+                    continue;
+                }
+                syncCount += syncSingleProductFromErp(erpProduct);
+            }
+
+            hasNext = Boolean.TRUE.equals(response.getHasNext());
+            pageIndex++;
+        }
+        return syncCount;
+    }
+
+    private int syncSingleProductFromErp(ProductResponseDTO.ProductInfo erpProduct) {
+        FsStoreProductAttrValue attrQuery = new FsStoreProductAttrValue();
+        attrQuery.setBarCode(erpProduct.getSkuId());
+        List<FsStoreProductAttrValue> attrList = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueList(attrQuery);
+        if (attrList == null || attrList.isEmpty()) {
+            return insertProductByErpData(erpProduct);
+        }
+
+        int updateCount = 0;
+        for (FsStoreProductAttrValue attr : attrList) {
+            FsStoreProduct product = fsStoreProductMapper.selectFsStoreProductByProductId(attr.getProductId());
+            if (product == null) {
+                continue;
+            }
+            updateProductByErpData(product, erpProduct);
+            updateAttrValueByErpData(product,attr, erpProduct);
+            updatePackageProductJson(product.getProductId(), erpProduct);
+            updateCount++;
+        }
+        return updateCount;
+    }
+
+    private int insertProductByErpData(ProductResponseDTO.ProductInfo erpProduct) {
+        FsStoreProduct newProduct = new FsStoreProduct();
+        newProduct.setStoreId(20191853L);
+        newProduct.setBarCode(erpProduct.getSkuId());
+        newProduct.setProductName(erpProduct.getName());
+        newProduct.setPrice(erpProduct.getSalePrice());
+        newProduct.setCostPrice(erpProduct.getCostPrice());
+        newProduct.setOtPrice(erpProduct.getMarketPrice());
+        newProduct.setImgUrl(normalizeErpImageUrl(erpProduct.getPic()));
+        newProduct.setUnitName(erpProduct.getUnit());
+        newProduct.setBrand(erpProduct.getBrand());
+        newProduct.setPrescribeSpec(erpProduct.getPropertiesValue());
+        newProduct.setPrescribeFactory(erpProduct.getSupplierName());
+        newProduct.setStock(0);
+        newProduct.setSales(0);
+        newProduct.setSort(0L);
+        newProduct.setSpecType(0);
+        newProduct.setIsShow(1);
+        newProduct.setIsDisplay(1);
+        newProduct.setIsDel(0);
+        newProduct.setIsBest(0);
+        newProduct.setIsBenefit(0);
+        newProduct.setIsHot(0);
+        newProduct.setIsNew(0);
+        newProduct.setIsPostage(0);
+        newProduct.setViews(0L);
+        newProduct.setGiveIntegral(BigDecimal.ZERO);
+        newProduct.setCreateTime(DateUtils.getNowDate());
+        newProduct.setUpdateTime(DateUtils.getNowDate());
+        fsStoreProductMapper.insertFsStoreProduct(newProduct);
+
+        FsStoreProductAttrValue newAttr = new FsStoreProductAttrValue();
+        newAttr.setProductId(newProduct.getProductId());
+        newAttr.setSku(erpProduct.getName());
+        newAttr.setStock(0);
+        newAttr.setSales(0);
+        newAttr.setPrice(erpProduct.getSalePrice());
+        newAttr.setImage(newProduct.getImgUrl());
+        newAttr.setCost(erpProduct.getCostPrice());
+        newAttr.setBarCode(erpProduct.getSkuId());
+        newAttr.setOtPrice(erpProduct.getMarketPrice());
+        newAttr.setWeight(erpProduct.getWeight());
+        newAttr.setVolume(BigDecimal.ZERO);
+        newAttr.setBrokerage(BigDecimal.ZERO);
+        newAttr.setBrokerageTwo(BigDecimal.ZERO);
+        newAttr.setBrokerageThree(BigDecimal.ZERO);
+        newAttr.setDoctorBrokerage(BigDecimal.ZERO);
+        newAttr.setGiveIntegral(0);
+        fsStoreProductAttrValueMapper.insertFsStoreProductAttrValue(newAttr);
+        return 1;
+    }
+
+    private void updateProductByErpData(FsStoreProduct product, ProductResponseDTO.ProductInfo erpProduct) {
+        FsStoreProduct updateProduct = new FsStoreProduct();
+        updateProduct.setProductId(product.getProductId());
+        updateProduct.setProductName(erpProduct.getName());
+        updateProduct.setPrice(erpProduct.getSalePrice());
+        updateProduct.setCostPrice(erpProduct.getCostPrice());
+        updateProduct.setOtPrice(erpProduct.getMarketPrice());
+        updateProduct.setImgUrl(normalizeErpImageUrl(erpProduct.getPic()));
+        updateProduct.setUnitName(erpProduct.getUnit());
+        updateProduct.setBrand(erpProduct.getBrand());
+        updateProduct.setPrescribeSpec(erpProduct.getPropertiesValue());
+        updateProduct.setPrescribeFactory(erpProduct.getSupplierName());
+        fsStoreProductMapper.updateFsStoreProduct(updateProduct);
+    }
+
+    private void updateAttrValueByErpData(FsStoreProduct product,FsStoreProductAttrValue attr, ProductResponseDTO.ProductInfo erpProduct) {
+        FsStoreProductAttrValue updateAttr = new FsStoreProductAttrValue();
+        updateAttr.setId(attr.getId());
+        updateAttr.setPrice(erpProduct.getSalePrice());
+        updateAttr.setCost(erpProduct.getCostPrice());
+        updateAttr.setOtPrice(erpProduct.getMarketPrice());
+        updateAttr.setImage(product.getImgUrl());
+        updateAttr.setWeight(erpProduct.getWeight());
+        fsStoreProductAttrValueMapper.updateFsStoreProductAttrValue(updateAttr);
+    }
+
+    private LocalDateTime parseDateTime(String time) {
+        try {
+            return LocalDateTime.parse(time, ERP_TIME_FORMATTER);
+        } catch (Exception e) {
+            throw new ServiceException("时间格式错误,请使用 yyyy-MM-dd HH:mm:ss");
+        }
+    }
+
+    private String formatDateTime(LocalDateTime time) {
+        return time.truncatedTo(ChronoUnit.SECONDS).format(ERP_TIME_FORMATTER);
+    }
+
+    private int normalizePageSize(Integer pageSize) {
+        if (pageSize == null || pageSize <= 0) {
+            return DEFAULT_ERP_PAGE_SIZE;
+        }
+        return Math.min(pageSize, DEFAULT_ERP_PAGE_SIZE);
+    }
+
+    private String normalizeErpImageUrl(String imageUrl) {
+        if (StringUtils.isBlank(imageUrl)) {
+            return imageUrl;
+        }
+        if (imageUrl.contains("obs.hbhdt.top")) {
+            return imageUrl;
+        }
+        if (!imageUrl.startsWith("http://") && !imageUrl.startsWith("https://")) {
+            return imageUrl;
+        }
+        try {
+            if (OSSFactory.build() == null) {
+                return imageUrl;
+            }
+            String suffix = getImageSuffix(imageUrl);
+            return OSSFactory.build().uploadSuffix(new URL(imageUrl).openStream(), suffix);
+        } catch (Exception e) {
+            return imageUrl;
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int replaceErpImageUrls() {
+        int count = 0;
+
+        List<FsStoreProduct> products = fsStoreProductMapper.selectFsStoreProductList(new FsStoreProduct());
+        if (products != null && !products.isEmpty()) {
+            for (FsStoreProduct product : products) {
+                if (StringUtils.isBlank(product.getImgUrl()) || product.getImgUrl().contains("obs.hbhdt.top")) {
+                    continue;
+                }
+                String newImgUrl = normalizeErpImageUrl(product.getImgUrl());
+                if (!StringUtils.equals(product.getImgUrl(), newImgUrl)) {
+                    FsStoreProduct updateProduct = new FsStoreProduct();
+                    updateProduct.setProductId(product.getProductId());
+                    updateProduct.setImgUrl(newImgUrl);
+                    fsStoreProductMapper.updateFsStoreProduct(updateProduct);
+                    count++;
+                }
+            }
+        }
+
+        List<FsStoreProductAttrValue> attrValues = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueList(new FsStoreProductAttrValue());
+        if (attrValues != null && !attrValues.isEmpty()) {
+            for (FsStoreProductAttrValue attrValue : attrValues) {
+                if (StringUtils.isBlank(attrValue.getImage()) || attrValue.getImage().contains("obs.hbhdt.top")) {
+                    continue;
+                }
+                String newImage = normalizeErpImageUrl(attrValue.getImage());
+                if (!StringUtils.equals(attrValue.getImage(), newImage)) {
+                    FsStoreProductAttrValue updateAttr = new FsStoreProductAttrValue();
+                    updateAttr.setId(attrValue.getId());
+                    updateAttr.setImage(newImage);
+                    fsStoreProductAttrValueMapper.updateFsStoreProductAttrValue(updateAttr);
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+    private String getImageSuffix(String imageUrl) {
+        String url = imageUrl;
+        int queryIndex = url.indexOf('?');
+        if (queryIndex >= 0) {
+            url = url.substring(0, queryIndex);
+        }
+        int dotIndex = url.lastIndexOf('.');
+        if (dotIndex < 0) {
+            return ".jpg";
+        }
+        return url.substring(dotIndex);
+    }
+
     @Override
     public int updateStoreProductByErpProductCode() {
         List<FsStoreProduct> productList = fsStoreProductMapper.selectFsStoreProductList(new FsStoreProduct());
@@ -939,19 +1199,7 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
                         continue;
                     }
                     
-                    FsStoreProduct updateProduct = new FsStoreProduct();
-                    updateProduct.setProductId(product.getProductId());
-                    updateProduct.setProductName(erpProduct.getName());
-                    updateProduct.setPrice(erpProduct.getSalePrice());
-                    updateProduct.setCostPrice(erpProduct.getCostPrice());
-                    updateProduct.setOtPrice(erpProduct.getMarketPrice());
-                    updateProduct.setImgUrl(erpProduct.getPic());
-                    updateProduct.setUnitName(erpProduct.getUnit());
-                    updateProduct.setBrand(erpProduct.getBrand());
-                    updateProduct.setPrescribeSpec(erpProduct.getPropertiesValue());
-                    updateProduct.setPrescribeFactory(erpProduct.getSupplierName());
-                    
-                    fsStoreProductMapper.updateFsStoreProduct(updateProduct);
+                    updateProductByErpData(product, erpProduct);
                     updateCount++;
                     
                     FsStoreProductAttrValue attrQuery = new FsStoreProductAttrValue();
@@ -961,15 +1209,7 @@ public class FsStoreProductServiceImpl implements IFsStoreProductService {
                     
                     if (attrList != null && !attrList.isEmpty()) {
                         for (FsStoreProductAttrValue attr : attrList) {
-                            FsStoreProductAttrValue updateAttr = new FsStoreProductAttrValue();
-                            updateAttr.setId(attr.getId());
-                            updateAttr.setPrice(erpProduct.getSalePrice());
-                            updateAttr.setCost(erpProduct.getCostPrice());
-                            updateAttr.setOtPrice(erpProduct.getMarketPrice());
-                            updateAttr.setImage(erpProduct.getPic());
-                            updateAttr.setWeight(erpProduct.getWeight());
-                            
-                            fsStoreProductAttrValueMapper.updateFsStoreProductAttrValue(updateAttr);
+                            updateAttrValueByErpData(null,attr, erpProduct);
                         }
                     }
                     

+ 10 - 5
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -947,7 +947,6 @@ public class FsUserServiceImpl implements IFsUserService {
 
     @Override
     public FsUserSummaryCountVO userSummaryCount(Long userId) {
-        // 判断是否是管理员
         Long companyId;
         CompanyUser companyUser = companyUserMapper.selectCompanyUserById(userId);
         if (companyUser != null && companyUser.isAdmin()) {
@@ -957,8 +956,6 @@ public class FsUserServiceImpl implements IFsUserService {
             companyId = null;
         }
 
-        // 使用 CompletableFuture 异步执行两个查询
-
         Long finalUserId = userId;
         CompletableFuture<Integer> userTotalFuture = CompletableFuture.supplyAsync(() ->
                 fsUserMapper.getUserTotal(finalUserId, companyId));
@@ -966,21 +963,29 @@ public class FsUserServiceImpl implements IFsUserService {
         CompletableFuture<Integer> todayNewUserFuture = CompletableFuture.supplyAsync(() ->
                 fsUserMapper.getTodayNewUser(finalUserId, companyId));
 
+        CompletableFuture<Integer> normalUserCountFuture = CompletableFuture.supplyAsync(() ->
+                fsUserMapper.getNormalUserCount(finalUserId, companyId));
+
+        CompletableFuture<Integer> blacklistUserCountFuture = CompletableFuture.supplyAsync(() ->
+                fsUserMapper.getBlacklistUserCount(finalUserId, companyId));
+
         CompletableFuture<List<FsUserSummaryCountTagVO>> countTagList = CompletableFuture.supplyAsync(() ->
                 fsUserMapper.countTag(finalUserId, companyId));
 
-        // 等待查询结果并构建返回对象
         FsUserSummaryCountVO fsUserSummaryCountVO = new FsUserSummaryCountVO();
         try {
             fsUserSummaryCountVO.setUserTotal(userTotalFuture.get());
             fsUserSummaryCountVO.setTodayNewUser(todayNewUserFuture.get());
+            fsUserSummaryCountVO.setNormalUserCount(normalUserCountFuture.get());
+            fsUserSummaryCountVO.setBlacklistUserCount(blacklistUserCountFuture.get());
             fsUserSummaryCountVO.setTagList(countTagList.get());
 
         } catch (InterruptedException | ExecutionException e) {
             logger.error("异步查询用户统计数据失败", e);
-            // 设置默认值
             fsUserSummaryCountVO.setUserTotal(0);
             fsUserSummaryCountVO.setTodayNewUser(0);
+            fsUserSummaryCountVO.setNormalUserCount(0);
+            fsUserSummaryCountVO.setBlacklistUserCount(0);
             fsUserSummaryCountVO.setTagList(new ArrayList<>());
         }
 

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/vo/h5/FsUserSummaryCountVO.java

@@ -20,6 +20,12 @@ public class FsUserSummaryCountVO {
     @ApiModelProperty(value = "今日新增会员")
     private int todayNewUser;
 
+    @ApiModelProperty(value = "普通会员数")
+    private int normalUserCount;
+
+    @ApiModelProperty(value = "黑名单数")
+    private int blacklistUserCount;
+
     @ApiModelProperty(value = "标签统计")
     private List<FsUserSummaryCountTagVO> tagList;
 

+ 6 - 0
fs-service/src/main/java/com/fs/store/vo/h5/FsUserSummaryCountVO.java

@@ -19,6 +19,12 @@ public class FsUserSummaryCountVO {
     @ApiModelProperty(value = "今日新增会员")
     private int todayNewUser;
 
+    @ApiModelProperty(value = "普通会员数")
+    private int normalUserCount;
+
+    @ApiModelProperty(value = "黑名单数")
+    private int blacklistUserCount;
+
     @ApiModelProperty(value = "标签统计")
     private List<FsUserSummaryCountTagVO> tagList;
 

+ 1 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseComplaintRecordMapper.xml

@@ -67,6 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and cr.record_status = #{recordStatus}
             </if>
         </where>
+        group by cr.record_id
         order by cr.create_time desc
     </select>
 

+ 26 - 2
fs-service/src/main/resources/mapper/crm/CrmMsgMapper.xml

@@ -15,10 +15,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="companyUserId"    column="company_user_id"    />
         <result property="updateTime"    column="update_time"    />
         <result property="objId"    column="obj_id"    />
+        <result property="actionUrl"    column="action_url"    />
+        <result property="actionParams"    column="action_params"    />
+        <result property="platform"    column="platform"    />
+        <result property="priority"    column="priority"    />
+        <result property="expireTime"    column="expire_time"    />
     </resultMap>
 
     <sql id="selectCrmMsgVo">
-        select msg_id, msg_type, title, content, create_time, is_read, company_id, company_user_id, update_time,obj_id from crm_msg
+        select msg_id, msg_type, title, content, create_time, is_read, company_id, company_user_id, 
+               update_time, obj_id, action_url, action_params, platform, priority, expire_time 
+        from crm_msg
     </sql>
 
     <select id="selectCrmMsgList" parameterType="CrmMsg" resultMap="CrmMsgResult">
@@ -30,8 +37,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isRead != null "> and is_read = #{isRead}</if>
             <if test="companyId != null "> and company_id = #{companyId}</if>
             <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="platform != null and platform != ''"> and platform = #{platform}</if>
+            <if test="priority != null"> and priority = #{priority}</if>
         </where>
-        order by msg_id desc
+        order by priority desc, msg_id desc
     </select>
     
     <select id="selectCrmMsgById" parameterType="Long" resultMap="CrmMsgResult">
@@ -51,6 +60,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">company_user_id,</if>
             <if test="updateTime != null">update_time,</if>
             <if test="objId != null">obj_id,</if>
+            <if test="actionUrl != null">action_url,</if>
+            <if test="actionParams != null">action_params,</if>
+            <if test="platform != null">platform,</if>
+            <if test="priority != null">priority,</if>
+            <if test="expireTime != null">expire_time,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="msgType != null">#{msgType},</if>
@@ -62,6 +76,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">#{companyUserId},</if>
             <if test="updateTime != null">#{updateTime},</if>
             <if test="objId != null">#{objId},</if>
+            <if test="actionUrl != null">#{actionUrl},</if>
+            <if test="actionParams != null">#{actionParams},</if>
+            <if test="platform != null">#{platform},</if>
+            <if test="priority != null">#{priority},</if>
+            <if test="expireTime != null">#{expireTime},</if>
          </trim>
     </insert>
 
@@ -77,6 +96,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
             <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="objId != null">obj_id = #{objId},</if>
+            <if test="actionUrl != null">action_url = #{actionUrl},</if>
+            <if test="actionParams != null">action_params = #{actionParams},</if>
+            <if test="platform != null">platform = #{platform},</if>
+            <if test="priority != null">priority = #{priority},</if>
+            <if test="expireTime != null">expire_time = #{expireTime},</if>
         </trim>
         where msg_id = #{msgId}
     </update>

+ 36 - 0
fs-service/src/main/resources/mapper/his/FsAppActiveUserDailyMapper.xml

@@ -12,6 +12,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </insert>
 
+    <insert id="batchInsertWithCompany">
+        INSERT IGNORE INTO fs_app_active_user_daily (stat_date, user_id, company_id, company_user_id, create_time)
+        VALUES
+        <foreach collection="dataList" item="data" separator=",">
+            (#{statDate}, #{data.userId}, #{data.companyId}, #{data.companyUserId}, NOW())
+        </foreach>
+    </insert>
+
     <select id="selectActiveUserCount" resultType="Long">
         SELECT COUNT(DISTINCT a.user_id)
         FROM fs_app_active_user_daily a
@@ -25,6 +33,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND a.stat_date &lt;= #{endDate}
     </select>
 
+    <select id="selectActiveUserCountByCompany" resultType="Long">
+        SELECT COUNT(DISTINCT a.user_id)
+        FROM fs_app_active_user_daily a
+        INNER JOIN fs_user_company_user ucu ON a.user_id = ucu.user_id
+        INNER JOIN fs_user u ON ucu.user_id = u.user_id
+        WHERE ((u.source IS NOT NULL AND u.source != '') OR (u.login_device IS NOT NULL AND u.login_device != '') OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND ucu.status IN (0, 1)
+        AND a.stat_date >= #{startDate}
+        AND a.stat_date &lt;= #{endDate}
+        AND a.company_id = #{companyId}
+    </select>
+
+    <select id="selectActiveUserCountByCompanyUser" resultType="Long">
+        SELECT COUNT(DISTINCT a.user_id)
+        FROM fs_app_active_user_daily a
+        INNER JOIN fs_user_company_user ucu ON a.user_id = ucu.user_id
+        INNER JOIN fs_user u ON ucu.user_id = u.user_id
+        WHERE ((u.source IS NOT NULL AND u.source != '') OR (u.login_device IS NOT NULL AND u.login_device != '') OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND ucu.status IN (0, 1)
+        AND a.stat_date >= #{startDate}
+        AND a.stat_date &lt;= #{endDate}
+        AND a.company_user_id = #{companyUserId}
+    </select>
+
     <select id="selectUserIdsByDate" resultType="Long">
         SELECT DISTINCT user_id
         FROM fs_app_active_user_daily

+ 5 - 3
fs-service/src/main/resources/mapper/his/FsExternalOrderMapper.xml

@@ -192,17 +192,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             o.external_create_time AS externalCreateTime,
             o.status,
             CASE o.status
-                WHEN 1 THEN '待支付'
+                WHEN 1 THEN '待审核'
                 WHEN 2 THEN '待发货'
                 WHEN 3 THEN '待收货'
                 WHEN 4 THEN '已完成'
-                WHEN 5 THEN '已取消'
+                WHEN -3 THEN '已取消'
                 ELSE '未知'
-            END AS statusDesc
+            END AS statusDesc,
+            IF(fa.id IS NOT NULL AND fa.audit_status = 0, true, false) AS isApplyAudit
         FROM fs_external_order o
         LEFT JOIN company c ON o.company_id = c.company_id
         LEFT JOIN company_user cu ON o.company_user_id = cu.user_id
         LEFT JOIN fs_external_order_item oi ON o.order_id = oi.order_id
+        LEFT JOIN fs_store_order_finance_audit fa ON fa.order_id = o.order_id AND fa.order_type = 0 AND fa.audit_status = 0
         <where>
             <if test="orderCode != null and orderCode != ''"> AND o.order_code = #{orderCode}</if>
             <if test="userId != null"> AND o.user_id = #{userId}</if>

+ 5 - 1
fs-service/src/main/resources/mapper/his/FsStoreOrderFinanceAuditMapper.xml

@@ -21,6 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="auditRemark"         column="audit_remark"         />
         <result property="voucherImages"       column="voucher_images"       />
         <result property="voucherRemark"       column="voucher_remark"       />
+        <result property="orderType"           column="order_type"           />
         <result property="applyUserId"         column="apply_user_id"        />
         <result property="applyUserName"       column="apply_user_name"      />
         <result property="applyTime"           column="apply_time"           />
@@ -31,7 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <sql id="selectFsStoreOrderFinanceAuditVo">
         select id, order_id, order_code, audit_type, original_total_price, original_pay_price,
                new_total_price, new_pay_price, price_change_reason, audit_status, audit_user_id,
-               audit_user_name, audit_time, audit_remark, voucher_images, voucher_remark,
+               audit_user_name, audit_time, audit_remark, voucher_images, voucher_remark, order_type,
                apply_user_id, apply_user_name, apply_time, create_time, update_time
         from fs_store_order_finance_audit
     </sql>
@@ -43,6 +44,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="orderCode != null and orderCode != ''">and order_code = #{orderCode}</if>
             <if test="auditType != null">and audit_type = #{auditType}</if>
             <if test="auditStatus != null">and audit_status = #{auditStatus}</if>
+            <if test="orderType != null">and order_type = #{orderType}</if>
             <if test="applyUserId != null">and apply_user_id = #{applyUserId}</if>
         </where>
         order by create_time desc
@@ -78,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="auditRemark != null">audit_remark,</if>
             <if test="voucherImages != null">voucher_images,</if>
             <if test="voucherRemark != null">voucher_remark,</if>
+            <if test="orderType != null">order_type,</if>
             <if test="applyUserId != null">apply_user_id,</if>
             <if test="applyUserName != null">apply_user_name,</if>
             <if test="applyTime != null">apply_time,</if>
@@ -99,6 +102,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="auditRemark != null">#{auditRemark},</if>
             <if test="voucherImages != null">#{voucherImages},</if>
             <if test="voucherRemark != null">#{voucherRemark},</if>
+            <if test="orderType != null">#{orderType},</if>
             <if test="applyUserId != null">#{applyUserId},</if>
             <if test="applyUserName != null">#{applyUserName},</if>
             <if test="applyTime != null">#{applyTime},</if>

+ 37 - 7
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -868,34 +868,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="getUserNumber" resultType="com.fs.store.vo.h5.UserListCountVO">
         SELECT
             t.status,
-            COUNT(DISTINCT t.user_id) AS num
+            COUNT(*) AS num
         FROM (
             SELECT
-                fcu.status,
-                fu.user_id
+                fcu.status
             FROM fs_user fu
             INNER JOIN fs_user_company_user fcu ON fcu.user_id = fu.user_id
-            WHERE fu.is_del = 0 AND fcu.is_repeat_fans IS NOT NULL
+            WHERE fu.is_del = 0
             <if test="userId != null and userId != 0">
                 AND fcu.company_user_id = #{userId}
             </if>
             <if test="companyId != null ">
                 and fcu.company_id = #{companyId}
             </if>
+            GROUP BY fu.user_id, fcu.project_id, fcu.create_time
 
             <if test="userId != null and userId != 0 ">
             UNION ALL
             SELECT
-                fcu.status,
-                fu.user_id
+                fcu.status
             FROM fs_user fu
             INNER JOIN fs_user_company_user fcu ON fcu.user_id = fu.user_id
             INNER JOIN company_user cu ON cu.user_id = fcu.company_user_id
-            WHERE fu.is_del = 0 AND fcu.is_repeat_fans IS NOT NULL
+            WHERE fu.is_del = 0
             AND cu.parent_id = #{userId}
             <if test="companyId != null ">
                 and fcu.company_id = #{companyId}
             </if>
+            GROUP BY fu.user_id, fcu.project_id, fcu.create_time
             </if>
         ) t
         GROUP BY t.status
@@ -1006,6 +1006,36 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND to_days(ucu.create_time) = to_days(now())
     </select>
 
+    <select id="getNormalUserCount" resultType="java.lang.Integer">
+        SELECT count(fs_user.user_id) as normalUserCount
+        FROM fs_user
+        left join fs_user_company_user ucu on ucu.user_id = fs_user.user_id
+        LEFT JOIN company_user ON ucu.company_user_id = company_user.user_id
+        WHERE fs_user.is_del = 0
+        AND ucu.status = 1
+        <if test="userId != null and userId != 0 ">
+            and (ucu.company_user_id = #{userId} OR company_user.parent_id = #{userId} )
+        </if>
+        <if test="companyId != null ">
+            and ucu.company_id = #{companyId}
+        </if>
+    </select>
+
+    <select id="getBlacklistUserCount" resultType="java.lang.Integer">
+        SELECT count(fs_user.user_id) as blacklistUserCount
+        FROM fs_user
+        left join fs_user_company_user ucu on ucu.user_id = fs_user.user_id
+        LEFT JOIN company_user ON ucu.company_user_id = company_user.user_id
+        WHERE fs_user.is_del = 0
+        AND ucu.status IN (0, 2)
+        <if test="userId != null and userId != 0 ">
+            and (ucu.company_user_id = #{userId} OR company_user.parent_id = #{userId} )
+        </if>
+        <if test="companyId != null ">
+            and ucu.company_id = #{companyId}
+        </if>
+    </select>
+
     <select id="countUserSummary" resultType="com.fs.store.vo.h5.FsUserSummaryCountVO">
         SELECT (SELECT count(fs_user.user_id)
         FROM fs_user