فهرست منبع

Merge branch 'refs/heads/master' into feature_260128_mergeOrder

yuhongqi 2 روز پیش
والد
کامیت
df92404178
70فایلهای تغییر یافته به همراه2793 افزوده شده و 81 حذف شده
  1. 2 0
      DirectoryV3.xml
  2. 3 3
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  3. 2 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceDialogController.java
  4. 6 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  5. 2 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  6. 6 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java
  7. 12 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxClientController.java
  8. 7 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxDialogController.java
  9. 110 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopController.java
  10. 97 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopLogsController.java
  11. 97 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserController.java
  12. 97 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserInfoController.java
  13. 70 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  14. 7 0
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  15. 4 0
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  16. 56 25
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  17. 61 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisCommonController.java
  18. 1 36
      fs-qwhook/src/main/java/com/fs/app/controller/ApisCommonController.java
  19. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  20. 2 0
      fs-service/src/main/java/com/fs/course/param/BatchRedUpdate.java
  21. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  22. 2 1
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralOrderMapper.java
  23. 57 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  24. 11 0
      fs-service/src/main/java/com/fs/ipad/IpadSendUtils.java
  25. 0 1
      fs-service/src/main/java/com/fs/qw/service/AsyncQwAiChatSopService.java
  26. 19 0
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  27. 72 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSop.java
  28. 85 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java
  29. 49 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUser.java
  30. 65 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUserInfo.java
  31. 61 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java
  32. 64 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java
  33. 61 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java
  34. 69 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java
  35. 61 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java
  36. 69 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopService.java
  37. 61 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserInfoService.java
  38. 61 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserService.java
  39. 104 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java
  40. 161 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java
  41. 103 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserInfoServiceImpl.java
  42. 103 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java
  43. 85 0
      fs-service/src/main/java/com/fs/wxcid/ImageToBase64Util.java
  44. 1 1
      fs-service/src/main/java/com/fs/wxcid/dto/message/MsgItem.java
  45. 2 1
      fs-service/src/main/java/com/fs/wxcid/service/MessageService.java
  46. 35 1
      fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java
  47. 22 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxwSendNoticeMsgDTO.java
  48. 22 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxwSendNoticeMsgRespDTO.java
  49. 6 2
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceNew.java
  50. 4 3
      fs-service/src/main/resources/application-config-druid-cfryt.yml
  51. 1 0
      fs-service/src/main/resources/application-config-druid-ddgy.yml
  52. 1 0
      fs-service/src/main/resources/application-config-druid-gzzdy.yml
  53. 1 0
      fs-service/src/main/resources/application-config-druid-hst.yml
  54. 1 0
      fs-service/src/main/resources/application-config-druid-kyt.yml
  55. 1 0
      fs-service/src/main/resources/application-config-druid-sxjz.yml
  56. 45 0
      fs-service/src/main/resources/db/20260226-个微SOP表结构.sql
  57. 2 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceDialogMapper.xml
  58. 1 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  59. 1 0
      fs-service/src/main/resources/mapper/company/CompanyWorkflowMapper.xml
  60. 1 0
      fs-service/src/main/resources/mapper/company/CompanyWxAccountMapper.xml
  61. 1 0
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  62. 2 1
      fs-service/src/main/resources/mapper/company/CompanyWxDialogMapper.xml
  63. 1 1
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  64. 146 0
      fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml
  65. 121 0
      fs-service/src/main/resources/mapper/wx/WxSopMapper.xml
  66. 121 0
      fs-service/src/main/resources/mapper/wx/WxSopUserInfoMapper.xml
  67. 105 0
      fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml
  68. 45 0
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  69. 35 0
      fs-user-app/src/main/java/com/fs/app/param/FsUserPhoneUpdateParam.java
  70. 2 1
      fs-websocket/src/main/java/com/fs/websocket/FsWebSocketServiceApplication.java

+ 2 - 0
DirectoryV3.xml

@@ -6,4 +6,6 @@
      <tree path="/fs-admin/src/main/java/com/fs/live/controller" title="直播"/>
      <tree path="/fs-admin/src/main/java/com/fs/qw" title="企微"/>
      <tree path="/fs-ad-api" title="广告回传"/>
+     <tree path="/fs-admin" title="总后台" extension="" presentableText="" tooltipTitle="" icon="" textColor=""
+           backgroundColor=""/>
  </trees>

+ 3 - 3
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -106,7 +106,7 @@ public class FsUserController extends BaseController
         SysRole sysRole = isCheckPermission();
         for (FsUserVO fsUserVO : list) {
             if(fsUserVO.getPhone() != null&&fsUserVO.getPhone()!=""){
-                if (!(sysRole.getIsCheckPhone()==1)){
+                if (!(sysRole.getIsCheckPhone()==1) || CloudHostUtils.hasCloudHostName("叮当国医")){
                     if (fsUserVO.getPhone().length()>11){
                         fsUserVO.setPhone(decryptPhoneMk(fsUserVO.getPhone()));
                     }else {
@@ -203,7 +203,7 @@ public class FsUserController extends BaseController
         SysRole sysRole = isCheckPermission();
         for (FsUserVO fsUserVO : list) {
             if(fsUserVO.getPhone() != null&&fsUserVO.getPhone()!=""){
-                if (!(sysRole.getIsCheckPhone()==1)){
+                if (!(sysRole.getIsCheckPhone()==1) || CloudHostUtils.hasCloudHostName("叮当国医")){
                     if (fsUserVO.getPhone().length()>11){
                         fsUserVO.setPhone(decryptPhoneMk(fsUserVO.getPhone()));
                     }else {
@@ -219,7 +219,7 @@ public class FsUserController extends BaseController
         return getDataTable(list);
     }
 
-    @PreAuthorize("@ss.hasPermi('his:user:export')")
+    @PreAuthorize("@ss.hasPermi('his:user:exportProject')")
     @GetMapping("/exportListProject")
     public AjaxResult exportListProject(FsUser fsUser)
     {

+ 2 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceDialogController.java

@@ -39,6 +39,8 @@ public class CompanyVoiceDialogController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceDialog companyVoiceDialog)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyVoiceDialog> list = companyVoiceDialogService.selectCompanyVoiceDialogList(companyVoiceDialog);
         return getDataTable(list);

+ 6 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -62,6 +62,8 @@ public class CompanyVoiceRoboticController extends BaseController
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceRobotic companyVoiceRobotic){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
         return getDataTable(list);
@@ -72,6 +74,8 @@ public class CompanyVoiceRoboticController extends BaseController
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyVoiceRobotic companyVoiceRobotic){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic));
     }
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
@@ -98,6 +102,8 @@ public class CompanyVoiceRoboticController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyVoiceRobotic companyVoiceRobotic)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
         ExcelUtil<CompanyVoiceRobotic> util = new ExcelUtil<CompanyVoiceRobotic>(CompanyVoiceRobotic.class);
         return util.exportExcel(list, "robotic");

+ 2 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java

@@ -41,6 +41,8 @@ public class CompanyWorkflowController extends BaseController {
      */
     @GetMapping("/list")
     public TableDataInfo list(CompanyWorkflow fsAiWorkflow) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsAiWorkflow.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyWorkflow> list = companyWorkflowService.selectCompanyWorkflowList(fsAiWorkflow);
         return getDataTable(list);

+ 6 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java

@@ -42,6 +42,8 @@ public class CompanyWxAccountController extends BaseController
     public TableDataInfo list(CompanyWxAccount companyWxEnterpriseAccount)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxEnterpriseAccount.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxEnterpriseAccount);
         return getDataTable(list);
     }
@@ -51,6 +53,8 @@ public class CompanyWxAccountController extends BaseController
     @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyWxAccount companyWxEnterpriseAccount){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxEnterpriseAccount.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxEnterpriseAccount);
         return R.ok().put("data", list);
     }
@@ -63,6 +67,8 @@ public class CompanyWxAccountController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyWxAccount companyWxEnterpriseAccount)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxEnterpriseAccount.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxEnterpriseAccount);
         ExcelUtil<CompanyWxAccount> util = new ExcelUtil<CompanyWxAccount>(CompanyWxAccount.class);
         return util.exportExcel(list, "companyAccount");

+ 12 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxClientController.java

@@ -6,11 +6,14 @@ 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.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyWxClientService;
 import com.fs.company.vo.AddWxClientVo;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -34,6 +37,9 @@ public class CompanyWxClientController extends BaseController
     @Autowired
     private ICompanyVoiceRoboticService companyVoiceRoboticService;
 
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询添加个微信账号列表
      */
@@ -41,6 +47,8 @@ public class CompanyWxClientController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyWxClient companyWxClient){
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxClient.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxClient> list = companyWxClientService.selectCompanyWxClientListCompany(companyWxClient);
         return getDataTable(list);
     }
@@ -53,6 +61,8 @@ public class CompanyWxClientController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyWxClient companyWxClient)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxClient.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxClient> list = companyWxClientService.selectCompanyWxClientListCompany(companyWxClient);
         ExcelUtil<CompanyWxClient> util = new ExcelUtil<CompanyWxClient>(CompanyWxClient.class);
         return util.exportExcel(list, "companyClient");
@@ -110,6 +120,8 @@ public class CompanyWxClientController extends BaseController
     @GetMapping("/addWxStatistics")
     @Transactional
     public R addWxStatistics(CompanyWxClient companyWxClient){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxClient.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxClient> clients = companyWxClientService.selectCompanyWxClientListCompany(companyWxClient);
         LocalDate now = LocalDate.now();
         return R.ok()

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

@@ -40,6 +40,9 @@ public class CompanyWxDialogController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyWxDialog companyWxDialog)
     {
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyWxDialog> list = companyWxDialogService.selectCompanyWxDialogList(companyWxDialog);
         return getDataTable(list);
@@ -48,6 +51,8 @@ public class CompanyWxDialogController extends BaseController
     @PreAuthorize("@ss.hasPermi('company:wxDialog:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyWxDialog companyWxDialog){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyWxDialogService.selectCompanyWxDialogList(companyWxDialog));
     }
 
@@ -59,6 +64,8 @@ public class CompanyWxDialogController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyWxDialog companyWxDialog)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxDialog> list = companyWxDialogService.selectCompanyWxDialogList(companyWxDialog);
         ExcelUtil<CompanyWxDialog> util = new ExcelUtil<CompanyWxDialog>(CompanyWxDialog.class);
         return util.exportExcel(list, "wxDialog");

+ 110 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopController.java

@@ -0,0 +1,110 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.service.IWxSopService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 个微SOPController
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@RestController
+@RequestMapping("/wx/wxSop")
+public class WxSopController extends BaseController
+{
+    @Autowired
+    private IWxSopService wxSopService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询个微SOP列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<WxSop> list = wxSopService.selectWxSopList(wxSop);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出个微SOP列表
+     */
+    @Log(title = "个微SOP", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<WxSop> list = wxSopService.selectWxSopList(wxSop);
+        ExcelUtil<WxSop> util = new ExcelUtil<WxSop>(WxSop.class);
+        return util.exportExcel(list, "个微SOP数据");
+    }
+
+    /**
+     * 获取个微SOP详细信息(含执行账号 companyUserIds)
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopService.selectWxSopDetailById(id));
+    }
+
+    /**
+     * 新增个微SOP
+     */
+    @Log(title = "个微SOP", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(wxSopService.insertWxSop(wxSop));
+    }
+
+    /**
+     * 修改个微SOP
+     */
+    @Log(title = "个微SOP", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(wxSopService.updateWxSop(wxSop));
+    }
+
+    /**
+     * 删除个微SOP
+     */
+    @Log(title = "个微SOP", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopService.deleteWxSopByIds(ids));
+    }
+}

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopLogsController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.service.IWxSopLogsService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 个微发送记录Controller
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@RestController
+@RequestMapping("/wx/wxSopLogs")
+public class WxSopLogsController extends BaseController
+{
+    @Autowired
+    private IWxSopLogsService wxSopLogsService;
+
+    /**
+     * 查询个微发送记录列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopLogs wxSopLogs)
+    {
+        startPage();
+        List<WxSopLogs> list = wxSopLogsService.selectWxSopLogsList(wxSopLogs);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出个微发送记录列表
+     */
+    @Log(title = "个微发送记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopLogs wxSopLogs)
+    {
+        List<WxSopLogs> list = wxSopLogsService.selectWxSopLogsList(wxSopLogs);
+        ExcelUtil<WxSopLogs> util = new ExcelUtil<WxSopLogs>(WxSopLogs.class);
+        return util.exportExcel(list, "个微发送记录数据");
+    }
+
+    /**
+     * 获取个微发送记录详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopLogsService.selectWxSopLogsById(id));
+    }
+
+    /**
+     * 新增个微发送记录
+     */
+    @Log(title = "个微发送记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSopLogs wxSopLogs)
+    {
+        return toAjax(wxSopLogsService.insertWxSopLogs(wxSopLogs));
+    }
+
+    /**
+     * 修改个微发送记录
+     */
+    @Log(title = "个微发送记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSopLogs wxSopLogs)
+    {
+        return toAjax(wxSopLogsService.updateWxSopLogs(wxSopLogs));
+    }
+
+    /**
+     * 删除个微发送记录
+     */
+    @Log(title = "个微发送记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopLogsService.deleteWxSopLogsByIds(ids));
+    }
+}

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.service.IWxSopUserService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 个微营期Controller
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@RestController
+@RequestMapping("/wx/wxSopUser")
+public class WxSopUserController extends BaseController
+{
+    @Autowired
+    private IWxSopUserService wxSopUserService;
+
+    /**
+     * 查询个微营期列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopUser wxSopUser)
+    {
+        startPage();
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出个微营期列表
+     */
+    @Log(title = "个微营期", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopUser wxSopUser)
+    {
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+        ExcelUtil<WxSopUser> util = new ExcelUtil<WxSopUser>(WxSopUser.class);
+        return util.exportExcel(list, "个微营期数据");
+    }
+
+    /**
+     * 获取个微营期详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopUserService.selectWxSopUserById(id));
+    }
+
+    /**
+     * 新增个微营期
+     */
+    @Log(title = "个微营期", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSopUser wxSopUser)
+    {
+        return toAjax(wxSopUserService.insertWxSopUser(wxSopUser));
+    }
+
+    /**
+     * 修改个微营期
+     */
+    @Log(title = "个微营期", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSopUser wxSopUser)
+    {
+        return toAjax(wxSopUserService.updateWxSopUser(wxSopUser));
+    }
+
+    /**
+     * 删除个微营期
+     */
+    @Log(title = "个微营期", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopUserService.deleteWxSopUserByIds(ids));
+    }
+}

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserInfoController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.service.IWxSopUserInfoService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 个微营期详情Controller
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@RestController
+@RequestMapping("/wx/wxSopUserInfo")
+public class WxSopUserInfoController extends BaseController
+{
+    @Autowired
+    private IWxSopUserInfoService wxSopUserInfoService;
+
+    /**
+     * 查询个微营期详情列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopUserInfo wxSopUserInfo)
+    {
+        startPage();
+        List<WxSopUserInfo> list = wxSopUserInfoService.selectWxSopUserInfoList(wxSopUserInfo);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出个微营期详情列表
+     */
+    @Log(title = "个微营期详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopUserInfo wxSopUserInfo)
+    {
+        List<WxSopUserInfo> list = wxSopUserInfoService.selectWxSopUserInfoList(wxSopUserInfo);
+        ExcelUtil<WxSopUserInfo> util = new ExcelUtil<WxSopUserInfo>(WxSopUserInfo.class);
+        return util.exportExcel(list, "个微营期详情数据");
+    }
+
+    /**
+     * 获取个微营期详情详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopUserInfoService.selectWxSopUserInfoById(id));
+    }
+
+    /**
+     * 新增个微营期详情
+     */
+    @Log(title = "个微营期详情", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSopUserInfo wxSopUserInfo)
+    {
+        return toAjax(wxSopUserInfoService.insertWxSopUserInfo(wxSopUserInfo));
+    }
+
+    /**
+     * 修改个微营期详情
+     */
+    @Log(title = "个微营期详情", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSopUserInfo wxSopUserInfo)
+    {
+        return toAjax(wxSopUserInfoService.updateWxSopUserInfo(wxSopUserInfo));
+    }
+
+    /**
+     * 删除个微营期详情
+     */
+    @Log(title = "个微营期详情", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopUserInfoService.deleteWxSopUserInfoByIds(ids));
+    }
+}

+ 70 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -201,6 +201,18 @@ public class IpadSendServer {
         }
     }
 
+    /**
+     * 发送群公告
+     */
+    public void sendNotice(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
+        WxWorkResponseDTO<WxwSendNoticeMsgRespDTO> resp = ipadSendUtils.sendNotice(vo, content.getValue());
+        if (resp.getErrcode() != 0) {
+            log.debug("ID:{}-ipad接口请求返回异常:{}", vo.getId(), resp.getErrmsg());
+            content.setSendStatus(2);
+            content.setSendRemarks("发送失败:" + resp.getErrmsg());
+        }
+    }
+
     public void sendImg(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
         FileVo imgVo = FileVo.builder().url(content.getImgUrl()).build();
         imgVo.setBase(vo);
@@ -577,10 +589,63 @@ public class IpadSendServer {
         return true;
     }
 
+    /**
+     * 群公告判定消息
+     * @param qwSopLogs
+     * @param setting
+     * @param qwUser
+     * @return
+     */
+    public boolean isSendLogsGroupNotice(QwSopLogs qwSopLogs, QwSopCourseFinishTempSetting setting, QwUser qwUser){
+        // 群公告只能发给群聊
+        if(qwSopLogs.getSendType() != 6 && qwSopLogs.getSendType() != 21){
+            log.warn("群公告SOP_LOG_ID:{}, 不是群聊消息,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "群公告只能发给群聊,不发送");
+            return false;
+        }
+
+        if(qwSopLogs.getSendStatus() != 3){
+            log.info("群公告状态异常不发送:{}, LOGID: {}", qwUser.getQwUserName(), qwSopLogs.getId());
+            return false;
+        }
+        if(redisCache.getCacheObject("qw:user:id:" + qwUser.getId()) != null){
+            log.info("群公告频率异常不发送:{}", qwUser.getQwUserName());
+            return false;
+        }
+
+        boolean noSop = qwSopLogs.getSendType() != 3 && qwSopLogs.getSendType() != 7 && qwSopLogs.getSendType() != 21;
+
+        if (qwSopLogs.getExpiryTime() == null && noSop) {
+            // 作废消息
+            log.warn("群公告SOP_LOG_ID:{}, SOP任务被删除", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "SOP任务被删除");
+            return false;
+        }
+
+        LocalDateTime sendTime = DateUtil.stringToLocalDateTime(qwSopLogs.getSendTime());
+        LocalDateTime expiryDateTime;
+
+        // 判断是否过期
+        if(qwSopLogs.getSendType() == 3 || qwSopLogs.getSendType() == 7 || qwSopLogs.getSendType() == 21){
+            expiryDateTime = sendTime.plusHours(12);
+        }else{
+            expiryDateTime = sendTime.plusHours(qwSopLogs.getExpiryTime());
+        }
+
+        if (LocalDateTime.now().isAfter(expiryDateTime)) {
+            // 作废消息
+            log.warn("群公告SOP_LOG_ID:{}, 已过期,不发送", qwSopLogs.getId());
+            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "已过期,不发送");
+            return false;
+        }
+
+        return true;
+    }
+
     public void send(QwSopCourseFinishTempSetting.Setting content, QwUser qwUser, QwSopLogs qwSopLogs, Map<String, FsCoursePlaySourceConfig> miniMap, BaseVo parentVo) {
         BaseVo vo = new BaseVo();
         vo.setId(Long.parseLong(qwSopLogs.getId()));
-        vo.setRoom(qwSopLogs.getSendType() == 6);
+        vo.setRoom(qwSopLogs.getSendType() == 6 || qwSopLogs.getSendType() == 21);
         vo.setUuid(qwUser.getUid());
         vo.setExId(qwSopLogs.getExternalUserId());
         vo.setServerId(qwUser.getServerId());
@@ -589,6 +654,10 @@ public class IpadSendServer {
         vo.setQwUserId(qwUser.getId());
         try {
             content.setSendStatus(1);
+            if(qwSopLogs.getSendType() == 21 && "11".equals(content.getContentType())){
+                sendNotice(vo, content);
+                return;
+            }
             switch (content.getContentType()) {
                 case "1":
                     // 文本

+ 7 - 0
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -194,12 +194,19 @@ public class SendMsg {
             QwSopCourseFinishTempSetting setting = JSON.parseObject(qwSopLogs.getContentJson(), QwSopCourseFinishTempSetting.class);
             //直播的sendType:20单独走判断 其他的走以前的逻辑
             boolean isSendLive = Integer.valueOf(20).equals(qwSopLogs.getSendType());
+            boolean isSendGroupNotice = Integer.valueOf(21).equals(qwSopLogs.getSendType());
             if(isSendLive){
                 if (!sendServer.isSendLogsLive(qwSopLogs, setting, user)) {
                     log.info("销售:{}, 直播消息发送条件未满足:{}", user.getQwUserName(), qwSopLogs.getId());
                     continue;
                 }
             }
+            else if(isSendGroupNotice){
+                if (!sendServer.isSendLogsGroupNotice(qwSopLogs, setting, user)) {
+                    log.info("销售:{}, 群公告发送条件未满足:{}", user.getQwUserName(), qwSopLogs.getId());
+                    continue;
+                }
+            }
             else{
                 // 判断消息状态是否满足发送条件
                 if (!sendServer.isSendLogs(qwSopLogs, setting, user)) {

+ 4 - 0
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -18,6 +18,7 @@ import com.fs.course.param.FsCourseLinkMiniParam;
 import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.service.*;
 import com.fs.course.vo.FsUserCourseVideoQVO;
+import com.fs.fastGpt.domain.FastGptPushTokenTotal;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsInquiryOrderService;
 import com.fs.his.utils.qrcode.QRCodeUtils;
@@ -26,6 +27,7 @@ import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwIpadServerLog;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwRestrictionPushRecordMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.*;
 import com.fs.qwApi.domain.QwExternalContactResult;
@@ -161,6 +163,8 @@ public class CommonController {
     @Autowired
     private ISysConfigService configService;
 
+
+
     @GetMapping("/roomLinkAllow")
     public R roomLinkAllow() {
 

+ 56 - 25
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -772,7 +772,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             QwGroupChat groupChat = groupChatMap.get(logVo.getChatId());
             if (groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()) {
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null, null);
-                ruleTimeVO.setSendType(6);
+                boolean hasGroupNotice = content.getSetting() != null && content.getSetting().stream()
+                        .anyMatch(st -> "11".equals(st.getContentType()));
+                if(hasGroupNotice){
+                    ruleTimeVO.setSendType(21);
+                    log.info("检测到群公告类型,设置sendType=21, sopId:{}, chatId:{}", logVo.getSopId(), groupChat.getChatId());
+                } else {
+                    ruleTimeVO.setSendType(6);
+                }
                 ruleTimeVO.setType(2);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                         type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
@@ -945,12 +952,35 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             //直播间发送类型
             case 20:
                 handleLiveMessage(sopLogs, content, companyUserId, companyId, isGroupChat, qwUserId, groupChat, externalId, logVo, liveId);
+                break;
+            case 21:
+                handleGroupNoticeMessage(sopLogs, content, isGroupChat);
+                break;
             default:
                 log.error("未知的消息类型 {},跳过处理。", type);
                 break;
         }
     }
 
+    /**
+     * 处理群公告消息
+     * @param sopLogs 日志对象
+     * @param content 内容对象
+     * @param isGroupChat 是否为群聊
+     */
+    private void handleGroupNoticeMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content, boolean isGroupChat) {
+        // 群公告只能发给群聊
+        if (!isGroupChat) {
+            log.warn("群公告只能发给群聊,跳过处理");
+            return;
+        }
+
+        // 设置发送类型为21(群公告)
+        sopLogs.setSendType(21);
+        sopLogs.setContentJson(JSON.toJSONString(content));
+        enqueueQwSopLogs(sopLogs);
+    }
+
     private void handleVoiceMessage(QwSopLogs sopLogs, QwSopTempSetting.Content content, String companyUserId) {
         sopLogs.setContentJson(JSON.toJSONString(content));
         enqueueQwSopLogs(sopLogs);
@@ -962,14 +992,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         // 深拷贝 Content 对象,避免使用 JSON
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         if (clonedContent == null) {
-            log.error("Failed to clone content, skipping handleCourseMessage.");
+//            log.error("Failed to clone content, skipping handleCourseMessage.");
             return;
         }
 
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         List<QwSopTempSetting.Content.Setting> settingAll = new ArrayList<>();
         if (settings == null || settings.isEmpty()) {
-            log.error("Cloned content settings are empty, skipping.");
+//            log.error("Cloned content settings are empty, skipping.");
             return;
         }
         // 顺序处理每个 Setting,避免过多的并行导致线程开销
@@ -1088,7 +1118,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                             // 2. 获取系统配置
                             SysConfig luckyBagConfig = sysConfigMapper.selectConfigByConfigKey("luckyBag.config");
                             if (ObjectUtil.isEmpty(luckyBagConfig) || StringUtil.strIsNullOrEmpty(luckyBagConfig.getConfigValue())) {
-                                log.warn("福袋配置为空,设置发送状态为失败");
+//                                log.warn("福袋配置为空,设置发送状态为失败");
                                 setSopLogsStatus(sopLogs, 5L, 0L, "福袋配置不存在");
                             }
 
@@ -1097,7 +1127,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                             try {
                                 jsonObject = JSON.parseObject(luckyBagConfig.getConfigValue());
                             } catch (Exception e) {
-                                log.error("解析福袋配置JSON失败: {}", luckyBagConfig.getConfigValue(), e);
+//                                log.error("解析福袋配置JSON失败: {}", luckyBagConfig.getConfigValue(), e);
                                 setSopLogsStatus(sopLogs, 5L, 0L, "福袋配置格式错误");
                             }
 
@@ -1113,7 +1143,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                             }
 
                             if (count == null) {
-                                log.warn("周限制次数配置为空");
+//                                log.warn("周限制次数配置为空");
                                 setSopLogsStatus(sopLogs, 5L, 0L, "周限制次数配置错误");
                             }
 
@@ -1143,12 +1173,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                 Object cachedCount = redisCache.getCacheObject(countCacheKey);
                                 if (cachedCount != null && cachedCount instanceof Integer) {
                                     recordCount = (Integer) cachedCount;
-                                    log.debug("福袋计数缓存命中,userId: {},次数: {}", fsUserId, recordCount);
+//                                    log.debug("福袋计数缓存命中,userId: {},次数: {}", fsUserId, recordCount);
 
                                     // 如果只需要判断是否超限,且已超限,直接返回
                                     if (recordCount >= count) {
-                                        log.info("用户福袋次数已达上限(计数缓存), userId: {}, 当前次数: {}, 限制次数: {}",
-                                                fsUserId, recordCount, count);
+//                                        log.info("用户福袋次数已达上限(计数缓存), userId: {}, 当前次数: {}, 限制次数: {}",
+//                                                fsUserId, recordCount, count);
                                         setSopLogsStatus(sopLogs, 5L, 0L, "超过福袋发放次数");
                                         luckyBagCollectRecords = Collections.emptyList();
                                         // 可以直接返回,不需要查询完整记录
@@ -1196,7 +1226,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                             // 6. 检查次数限制
                             if (recordCount >= count) {
-                                log.info("用户福袋次数已达上限, userId: {}, 当前次数: {}, 限制次数: {}", fsUserId, recordCount, count);
+//                                log.info("用户福袋次数已达上限, userId: {}, 当前次数: {}, 限制次数: {}", fsUserId, recordCount, count);
                                 setSopLogsStatus(sopLogs, 5L, 0L, "超过福袋发放次数");
                             }
                         }
@@ -1212,7 +1242,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                         0L, 0L, qwUserId, companyUserId, companyId, cachedCourseConfig, null);
                             }
                         } catch (Exception e) {
-                            log.error("生成活动链接失败", e);
+//                            log.error("生成活动链接失败", e);
                             setSopLogsStatus(sopLogs, 5L, 0L, "生成活动链接失败");
                             return;
                         }
@@ -1241,7 +1271,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         setting.setMiniprogramTitle("福袋发放");
                         setting.setMiniprogramPage(link);
 
-                        log.info("福袋配置成功,userId: {}, appId: {}", fsUserId, finalAppId);
+//                        log.info("福袋配置成功,userId: {}, appId: {}", fsUserId, finalAppId);
 
                     } catch (Exception e) {
                         log.error("任务模板福袋发放失败", e);
@@ -1332,7 +1362,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
 
         if (config == null) {
-            log.error("CourseConfig is not loaded.");
+//            log.error("CourseConfig is not loaded.");
             return "";
         }
         FsCourseLink link = new FsCourseLink();
@@ -1388,13 +1418,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         // 深拷贝 Content 对象,避免使用 JSON
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         if (clonedContent == null) {
-            log.error("Failed to clone content, skipping handleCourseMessage.");
+//            log.error("Failed to clone content, skipping handleCourseMessage.");
             return;
         }
         clonedContent.setLiveId(liveId);
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         if (settings == null || settings.isEmpty()) {
-            log.error("Cloned content settings are empty, skipping.");
+//            log.error("Cloned content settings are empty, skipping.");
             return;
         }
 
@@ -1476,7 +1506,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         // 深拷贝 Content 对象,避免使用 JSON
         QwSopTempSetting.Content clonedContent = deepCopyContent(content);
         if (clonedContent == null) {
-            log.error("Failed to clone content, skipping handleCourseMessage.");
+//            log.error("Failed to clone content, skipping handleCourseMessage.");
             return;
         }
 
@@ -1487,7 +1517,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
         List<QwSopTempSetting.Content.Setting> settings = clonedContent.getSetting();
         if (settings == null || settings.isEmpty()) {
-            log.error("Cloned content settings are empty, skipping.");
+//            log.error("Cloned content settings are empty, skipping.");
             return;
         }
         //如果是@所有人,就添加
@@ -1603,9 +1633,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         setting.setMiniType(miniType);
                         if (!StringUtil.strIsNullOrEmpty(finalAppId)) {
                             setting.setMiniprogramAppid(finalAppId);
-                        } else {
-                            log.error("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
                         }
+//                        else {
+//                            log.error("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+//                        }
 
                     }
 
@@ -1755,8 +1786,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         st.setBusinessId(String.valueOf(businessId));
         String realLinkFull = appActivitlLink + JSON.toJSONString(courseMap);
         link.setRealLink(realLinkFull);
-        log.error("存入fs_course_link:" + registeredRealLink);
-        log.error("QwSopCourseFinishTempSetting.Setting:{}", st);
+//        log.error("存入fs_course_link:" + registeredRealLink);
+//        log.error("QwSopCourseFinishTempSetting.Setting:{}", st);
         //存短链-
         fsCourseLinkMapper.insertFsCourseLink(link);
         return link.getRealLink();
@@ -1835,7 +1866,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 return null;
             }
 
-            log.info("福袋记录添加成功 [recordId:{}, luckyBagId:{}]", recordId, content.getLuckyBagId());
+//            log.info("福袋记录添加成功 [recordId:{}, luckyBagId:{}]", recordId, content.getLuckyBagId());
             return recordId;
 
         } catch (NumberFormatException e) {
@@ -1948,7 +1979,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
 
         if (config == null) {
-            log.error("CourseConfig is not loaded.");
+//            log.error("CourseConfig is not loaded.");
             return "";
         }
 
@@ -2045,7 +2076,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
 
         if (config == null) {
-            log.error("CourseConfig is not loaded.");
+//            log.error("CourseConfig is not loaded.");
             return null;
         }
         FsCourseLink link = createFsCourseLink(corpId, sendTime, courseId, videoId, qwUserId,
@@ -2162,7 +2193,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         }
 
         if (config == null) {
-            log.error("CourseConfig is not loaded.");
+//            log.error("CourseConfig is not loaded.");
             return "";
         }
 

+ 61 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisCommonController.java

@@ -5,12 +5,14 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.fastGpt.domain.FastGptPushTokenTotal;
 import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.his.domain.FsAppVersion;
 import com.fs.his.service.IFsAppVersionService;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactCrmMapper;
+import com.fs.qw.mapper.QwRestrictionPushRecordMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.param.QwConfigSignatureParam;
 import com.fs.qw.service.IQwJsApiService;
@@ -27,6 +29,9 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.LocalDate;
+import java.util.List;
+
 
 @Api("公共接口")
 @RestController
@@ -70,6 +75,62 @@ public class ApisCommonController {
     @Autowired
     private IQwUserVideoService qwUserVideoService;
 
+
+    @Autowired
+    private QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper;
+
+    /**
+     * 统计指定时间段内的ai事件埋点
+     * @param startDate 开始日期 (格式: yyyy-MM-dd)
+     * @param endDate 结束日期 (格式: yyyy-MM-dd)
+     */
+    @GetMapping("/eventLogTotals")
+    public void eventLogTotals(String startDate, String endDate) {
+        try {
+            // 解析开始和结束日期
+            LocalDate start = LocalDate.parse(startDate);
+            LocalDate end = LocalDate.parse(endDate);
+
+            // 循环处理每一天
+            LocalDate current = start;
+            while (!current.isAfter(end)) {
+                String dateTime = current.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+                log.info("开始处理日期: {}", dateTime);
+
+                List<FastGptPushTokenTotal> fastGptPushTotalList = qwRestrictionPushRecordMapper.selectFastgptPushTokenTotal(dateTime);
+                if (fastGptPushTotalList != null && !fastGptPushTotalList.isEmpty()) {
+                    for (FastGptPushTokenTotal fastGptPushTotal : fastGptPushTotalList) {
+                        // 获取统计数据
+                        Integer type = fastGptPushTotal.getType();
+                        Long count = 0L;
+                        if(type == 7){
+                            count = fastGptPushTotal.getCount() * 450;
+                        }else{
+                            count = fastGptPushTotal.getCount() * 150;
+                        }
+                        fastGptPushTotal.setCount(count);
+                        FastGptPushTokenTotal pushTotal = qwRestrictionPushRecordMapper.selectFastGptPushTokenTotalByInfo(fastGptPushTotal);
+                        if(pushTotal == null){
+                            qwRestrictionPushRecordMapper.insertPushTokenTotal(fastGptPushTotal);
+                        }else{
+                            fastGptPushTotal.setId(pushTotal.getId());
+                            qwRestrictionPushRecordMapper.updatePushTokenTotal(fastGptPushTotal);
+                        }
+                    }
+                }
+
+                // 移动到下一天
+                current = current.plusDays(1);
+            }
+
+            log.info("时间段 {} 至 {} 的AI事件统计处理完成", startDate, endDate);
+        } catch (Exception e) {
+            log.error("处理时间段AI事件统计异常,时间范围: " + startDate + " 至 " + endDate, e);
+        }
+    }
+
+
     @PostMapping("/qwHookSendMsg")
     public R qwHookSendMsg(@RequestBody QwHookSendMsgParam param ) {
         param.setClientId(2);

+ 1 - 36
fs-qwhook/src/main/java/com/fs/app/controller/ApisCommonController.java

@@ -88,8 +88,6 @@ public class ApisCommonController {
     @Autowired
     private IFsCourseLinkService iFsCourseLinkService;
 
-    @Autowired
-    private QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper;
 
     @Autowired
     private FsUserMapper userMapper;
@@ -130,40 +128,7 @@ public class ApisCommonController {
         System.out.println(usersByPhone);
     }
 
-    @GetMapping("/sopPushTokenTotal")
-    public void sopPushTokenTotal(String dateTime) {
-
-        // 获取日期字符串(今天或昨天)
-//        String dateTime;
-//            dateTime = DateUtils.getDate();
-        log.info("开始执行sop任务token消耗统计");
-        try {
-            List<FastGptPushTokenTotal> fastGptPushTotalList = qwRestrictionPushRecordMapper.selectFastgptPushTokenTotal(dateTime);
-            if (fastGptPushTotalList != null && !fastGptPushTotalList.isEmpty()) {
-                for (FastGptPushTokenTotal fastGptPushTotal : fastGptPushTotalList) {
-                    // 获取统计数据
-                    Integer type = fastGptPushTotal.getType();
-                    Long count = 0L;
-                    if(type == 7){
-                        count = fastGptPushTotal.getCount() * 450;
-                    }else{
-                        count = fastGptPushTotal.getCount() * 150;
-                    }
-                    fastGptPushTotal.setCount(count);
-                    FastGptPushTokenTotal pushTotal = qwRestrictionPushRecordMapper.selectFastGptPushTokenTotalByInfo(fastGptPushTotal);
-                    if(pushTotal == null){
-                        qwRestrictionPushRecordMapper.insertPushTokenTotal(fastGptPushTotal);
-                    }else{
-                        fastGptPushTotal.setId(pushTotal.getId());
-                        qwRestrictionPushRecordMapper.updatePushTokenTotal(fastGptPushTotal);
-                    }
-                }
-            }
-            log.info("结束执行sop任务token消耗统计");
-        } catch (Exception e) {
-            log.error("执行sop任务token消耗统计异常", e);
-        }
-    }
+
 
     /**
      * 获取跳转微信小程序的链接地址

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

@@ -197,7 +197,7 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
 
     void insertBatchFsUserCourseVideo(@Param("collect") List<FsUserCourseVideo> collect);
 
-    void updateRedPacketMoney(@Param("videoId") Long videoId, @Param("redPacketMoney") BigDecimal redPacketMoney);
+    void updateRedPacketMoney(@Param("videoId") Long videoId, @Param("redPacketMoney") BigDecimal redPacketMoney, @Param("title") String title);
 
     /**
      * 获取选项列表

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/BatchRedUpdate.java

@@ -8,4 +8,6 @@ import java.math.BigDecimal;
 public class BatchRedUpdate {
     private Long videoId;
     private BigDecimal redPacketMoney;
+    //小节名称
+    private  String title;
 }

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

@@ -3115,7 +3115,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Override
     public void batchUpdateRed(List<BatchRedUpdate> list) {
         list.forEach(e -> {
-            fsUserCourseVideoMapper.updateRedPacketMoney(e.getVideoId(), e.getRedPacketMoney());
+            fsUserCourseVideoMapper.updateRedPacketMoney(e.getVideoId(), e.getRedPacketMoney(),e.getTitle());
         });
     }
 

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

@@ -106,7 +106,8 @@ public interface FsIntegralOrderMapper extends BaseMapper<FsIntegralOrder>
             " order by o.order_id desc "+
             "</script>"})
     List<FsIntegralOrderListUVO> selectFsIntegralOrderListUVO(@Param("maps")FsIntegralOrderListUParam param);
-    @Select("select *from fs_integral_order where order_code=#{orderCode}")
+
+    @Select("select * from fs_integral_order where order_code=#{orderCode}")
     FsIntegralOrder selectFsIntegralOrderByOrderCode(String orderCode);
 
     @Select("UPDATE fs_integral_order SET `status`=3 WHERE `status`=2 and DATE(delivery_time) < DATE_SUB(CURDATE(), INTERVAL 15 DAY)")

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

@@ -34,6 +34,9 @@ import com.fs.erp.domain.ErpOrderItem;
 import com.fs.erp.domain.ErpOrderPayment;
 import com.fs.erp.dto.ErpOrderResponse;
 import com.fs.erp.service.IErpOrderService;
+import com.fs.event.TemplateBean;
+import com.fs.event.TemplateEvent;
+import com.fs.event.TemplateListenEnum;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.dto.ErpRemarkDTO;
@@ -45,6 +48,9 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.utils.RedisCacheUtil;
 import com.fs.his.vo.*;
+import com.fs.huifuPay.domain.HuiFuRefundResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayRefundRequest;
+import com.fs.huifuPay.service.HuiFuService;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.tzBankPay.doman.PayType;
@@ -57,6 +63,7 @@ import org.redisson.api.RObjectAsync;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.interceptor.TransactionAspectSupport;
@@ -95,12 +102,22 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
 
     @Autowired
     private FsIntegralGoodsMapper fsIntegralGoodsMapper;
+
+    @Autowired
+    private  FsStorePaymentMapper storePaymentMapper;
+
     @Autowired
     private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
 
     @Autowired
     private QwUserMapper qwUserMapper;
 
+    @Autowired
+    HuiFuService huiFuService;
+
+    @Autowired
+    private ApplicationEventPublisher publisher;
+
     @Autowired
     private CompanyUserMapper companyUserMapper;
     @Autowired
@@ -870,6 +887,46 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             fsUserIntegralLogs.setStatus(0);
             fsUserIntegralLogs.setCreateTime(new Date());
             i = fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs);
+
+            //还原库存
+            if (fsIntegralOrder.getItemJson().startsWith("[") && fsIntegralOrder.getItemJson().endsWith("]")){
+                List<FsIntegralGoods> goodsItem = JSONUtil.toBean(fsIntegralOrder.getItemJson(), new TypeReference<List<FsIntegralGoods>>(){}, true);
+                goodsItem.forEach(goods -> fsIntegralGoodsMapper.addStock(goods.getGoodsId(), Objects.isNull(goods.getNum()) ? 1 : goods.getNum()));
+            } else {
+                FsIntegralGoods integralGoods = JSONUtil.toBean(fsIntegralOrder.getItemJson(), FsIntegralGoods.class);
+                fsIntegralGoodsMapper.addStock(integralGoods.getGoodsId(), Objects.isNull(integralGoods.getNum()) ? 1 : integralGoods.getNum());
+            }
+
+            //还原金额
+            List<FsStorePayment> payments = storePaymentMapper.selectFsStorePaymentByPay(6,fsIntegralOrder.getOrderId());
+            if(payments!=null&&payments.size()==1){
+                FsStorePayment payment=payments.get(0);
+                V2TradePaymentScanpayRefundRequest request = new V2TradePaymentScanpayRefundRequest();
+                request.setOrdAmt(payment.getPayMoney().toString());
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setReqSeqId("refund-"+payment.getPayCode());
+                Map<String, Object> extendInfoMap = new HashMap<>();
+                extendInfoMap.put("org_req_seq_id", "integral-"+payment.getPayCode());
+                request.setExtendInfo(extendInfoMap);
+                HuiFuRefundResult refund = huiFuService.refund(request);
+                log.info("积分退款返回结果:积分订单id:"+fsIntegralOrder.getOrderId()+refund);
+                if((refund.getResp_code().equals("00000000")||refund.getResp_code().equals("00000100"))&&(refund.getTrans_stat().equals("S")||refund.getTrans_stat().equals("P"))){
+                    FsStorePayment paymentMap=new FsStorePayment();
+                    paymentMap.setPaymentId(payment.getPaymentId());
+                    paymentMap.setStatus(-1);
+                    paymentMap.setRefundTime(DateUtils.getNowDate());
+                    paymentMap.setRefundMoney(payment.getPayMoney());
+                    storePaymentMapper.updateFsStorePayment(paymentMap);
+                    TemplateBean templateBean = TemplateBean.builder()
+                            .orderId(fsIntegralOrder.getOrderId().toString())
+                            .title("订单已取消")
+                            .remark("您的订单已取消")
+                            .uid(fsIntegralOrder.getUserId())
+                            .templateType(TemplateListenEnum.TYPE_1.getValue())
+                            .build();
+                    publisher.publishEvent(new TemplateEvent(this, templateBean));
+                }
+            }
         }
         return i;
     }

+ 11 - 0
fs-service/src/main/java/com/fs/ipad/IpadSendUtils.java

@@ -212,6 +212,17 @@ public class IpadSendUtils {
         return wxWorkService.SendAppMsg(dto, vo.getServerId());
     }
 
+    /**
+     * 发送群公告
+     */
+    public WxWorkResponseDTO<WxwSendNoticeMsgRespDTO> sendNotice(BaseVo vo, String content){
+        WxwSendNoticeMsgDTO dto = new WxwSendNoticeMsgDTO();
+        dto.setUuid(vo.getUuid());
+        dto.setRoomid(chatIds(vo));
+        dto.setMsg(content);
+        return wxWorkService.SendNotice(dto, vo.getServerId());
+    }
+
 
 
 

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

@@ -158,7 +158,6 @@ public class AsyncQwAiChatSopService {
                     sopLogs.setSendTime(sendTime);
 
                     Date expirySendTime = Date.from(expiryDateTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant());
-
                     //过滤违禁词
                     if ("1".equals(setting.getContentType())) {
                         sopUserLogsInfoService.replaceContent(setting.getContentType(), setting.getValue(), setting::setValue, words); // 替换 value

+ 19 - 0
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -803,6 +803,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 createVoiceUrl(st, companyUserId, qwSop);
                                 sopLogs.setAppSendStatus(0);
                                 break;
+                            //群公告
+                            case "11":
+                                sopLogs.setSendType(21); // 设置为群公告类型
+                                break;
                             default:
                                 break;
                         }
@@ -851,6 +855,13 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
                     List<QwSopCourseFinishTempSetting.Setting> list = JSONArray.parseArray(param.getSetting(), QwSopCourseFinishTempSetting.Setting.class);
 
+                    // 检查是否有群公告类型,如果有则设置sendType=21
+                    boolean hasGroupNotice = list.stream().anyMatch(st -> "11".equals(st.getContentType()));
+                    if(hasGroupNotice){
+                        sopLogs.setSendType(21);
+                        log.info("检测到群公告类型,设置sendType=21, sopId:{}, chatId:{}", param.getSopId(), groupChat.getChatId());
+                    }
+
                     for (QwSopCourseFinishTempSetting.Setting st : list) {
 
                         //过滤违禁词
@@ -1057,6 +1068,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 }
                                 sopLogs.setAppSendStatus(0);
                                 break;
+                            //群公告
+                            case "11":
+                                sopLogs.setSendType(21); // 设置为群公告类型
+                                break;
                         }
                     }
                     setting.setSetting(list);
@@ -1440,6 +1455,10 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                             }
                             sopLogs.setAppSendStatus(0);
                             break;
+                        //群公告(仅用于一键群发,个人不应该有群公告)
+                        case "11":
+                            log.warn("群公告不能发给个人,跳过处理,sopId:{}, externalId:{}", param.getSopId(), item.getExternalId());
+                            break;
                         default:
                             break;
 

+ 72 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSop.java

@@ -0,0 +1,72 @@
+package com.fs.wx.sop.domain;
+
+import java.time.LocalDate;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 个微SOP对象 wx_sop
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxSop extends BaseEntityTow {
+
+    /** 名称 */
+    @Excel(name = "名称")
+    private String name;
+
+    /** 筛选方式(0标签1群聊) */
+    @Excel(name = "筛选方式(0标签1群聊)")
+    private Integer filterType;
+
+    /** 选择的标签 */
+    @Excel(name = "选择的标签")
+    private String selectTags;
+
+    /** 排查的标签 */
+    @Excel(name = "排查的标签")
+    private String excludeTags;
+
+    /** 模板ID */
+    @Excel(name = "模板ID")
+    private String tempId;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 是否固定营期(0否1是) */
+    @Excel(name = "是否固定营期(0否1是)")
+    private Integer isFixed;
+
+    /** 过期时间(小时) */
+    @Excel(name = "过期时间(小时)")
+    private Integer expiryTime;
+
+    /** 营期开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期开始时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDate startTime;
+
+    /** 执行账号ID,逗号分隔(入库) */
+    @Excel(name = "执行账号ID")
+    private String accountIds;
+
+    /** 执行账号列表(不入库,用于编辑回显) */
+    @TableField(exist = false)
+    private List<Map<String, Object>> selectedQwUsers;
+
+}

+ 85 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java

@@ -0,0 +1,85 @@
+package com.fs.wx.sop.domain;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 个微发送记录对象 wx_sop_logs
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxSopLogs extends BaseEntityTow {
+
+    /** 消息类型0个人1群 */
+    @Excel(name = "消息类型0个人1群")
+    private Integer type;
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long sopId;
+
+    /** 营期ID */
+    @Excel(name = "营期ID")
+    private Long sopUserId;
+
+    /** 发送类型(字典-wx_send_type) */
+    @Excel(name = "发送类型", readConverterExp = "字=典-wx_send_type")
+    private Integer sendType;
+
+    /** 生成类型(0自动1手动) */
+    @Excel(name = "生成类型(0自动1手动)")
+    private Integer generateType;
+
+    /** 发送账号ID */
+    @Excel(name = "发送账号ID")
+    private Long accountId;
+
+    /** 发送对象ID */
+    @Excel(name = "发送对象ID")
+    private Long wxContactId;
+
+    /** 发送对象名称 */
+    @Excel(name = "发送对象名称")
+    private String wxContactName;
+
+    /** 发送群聊ID */
+    @Excel(name = "发送群聊ID")
+    private Long wxRoomId;
+
+    /** 发送群聊名称 */
+    @Excel(name = "发送群聊名称")
+    private String wxRoomName;
+
+    /** 小程序ID */
+    @Excel(name = "小程序ID")
+    private Long fsUserId;
+
+    /** 发送状态0待发送1发送成功2发送失败3消息作废 */
+    @Excel(name = "发送状态0待发送1发送成功2发送失败3消息作废")
+    private Integer sendStatus;
+
+    /** 发送备注 */
+    @Excel(name = "发送备注")
+    private String sendRemark;
+
+    /** 发送排序 */
+    @Excel(name = "发送排序")
+    private Integer sendSort;
+
+    /** 消息过期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "消息过期时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDateTime expirationTime;
+
+
+}

+ 49 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUser.java

@@ -0,0 +1,49 @@
+package com.fs.wx.sop.domain;
+
+import java.time.LocalDate;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 个微营期对象 wx_sop_user
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxSopUser extends BaseEntityTow {
+
+    /** 类型(0个人1群聊) */
+    @Excel(name = "类型(0个人1群聊)")
+    private Integer type;
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long sopId;
+
+    /** 个微账号ID */
+    @Excel(name = "个微账号ID")
+    private Long accountId;
+
+    /** 营期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDate startTime;
+
+    /** 群聊ID */
+    @Excel(name = "群聊ID")
+    private String chatId;
+
+    /** 状态(0正常1暂停) */
+    @Excel(name = "状态", readConverterExp = "0=正常1暂停")
+    private Integer status;
+
+
+}

+ 65 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUserInfo.java

@@ -0,0 +1,65 @@
+package com.fs.wx.sop.domain;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 个微营期详情对象 wx_sop_user_info
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WxSopUserInfo extends BaseEntityTow {
+
+    /** 任务ID */
+    @Excel(name = "任务ID")
+    private Long sopId;
+
+    /** 营期ID */
+    @Excel(name = "营期ID")
+    private Long sopUserId;
+
+    /** 联系人ID */
+    @Excel(name = "联系人ID")
+    private Long wxContactId;
+
+    /** 小程序ID */
+    @Excel(name = "小程序ID")
+    private Long fsUserId;
+
+    /** 是否7天都没有看课 0否 1是 */
+    @Excel(name = "是否7天都没有看课 0否 1是")
+    private Integer isDaysNotStudy;
+
+    /** 总完课天数 */
+    @Excel(name = "总完课天数")
+    private Integer finishCout;
+
+    /** 最近完课时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "最近完课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private LocalDateTime finishTime;
+
+    /** 连续完课天数 */
+    @Excel(name = "连续完课天数")
+    private Integer finishCourseDays;
+
+    /** 客户评级的等级 */
+    @Excel(name = "客户评级的等级")
+    private Integer grade;
+
+    /** 禁用状态 0 正常 1禁用 */
+    @Excel(name = "禁用状态 0 正常 1禁用")
+    private Integer status;
+
+
+}

+ 61 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java

@@ -0,0 +1,61 @@
+package com.fs.wx.sop.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.wx.sop.domain.WxSopLogs;
+
+/**
+ * 个微发送记录Mapper接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
+    /**
+     * 查询个微发送记录
+     * 
+     * @param id 个微发送记录主键
+     * @return 个微发送记录
+     */
+    WxSopLogs selectWxSopLogsById(Long id);
+
+    /**
+     * 查询个微发送记录列表
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 个微发送记录集合
+     */
+    List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
+
+    /**
+     * 新增个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    int insertWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 修改个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    int updateWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 删除个微发送记录
+     * 
+     * @param id 个微发送记录主键
+     * @return 结果
+     */
+    int deleteWxSopLogsById(Long id);
+
+    /**
+     * 批量删除个微发送记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxSopLogsByIds(Long[] ids);
+}

+ 64 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java

@@ -0,0 +1,64 @@
+package com.fs.wx.sop.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.wx.sop.domain.WxSop;
+
+/**
+ * 个微SOPMapper接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopMapper extends BaseMapper<WxSop>{
+    /**
+     * 查询个微SOP
+     * 
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    @DataSource(DataSourceType.SOP)
+    WxSop selectWxSopById(Long id);
+
+    /**
+     * 查询个微SOP列表
+     * 
+     * @param wxSop 个微SOP
+     * @return 个微SOP集合
+     */
+    List<WxSop> selectWxSopList(WxSop wxSop);
+
+    /**
+     * 新增个微SOP
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int insertWxSop(WxSop wxSop);
+
+    /**
+     * 修改个微SOP
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int updateWxSop(WxSop wxSop);
+
+    /**
+     * 删除个微SOP
+     * 
+     * @param id 个微SOP主键
+     * @return 结果
+     */
+    int deleteWxSopById(Long id);
+
+    /**
+     * 批量删除个微SOP
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxSopByIds(Long[] ids);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java

@@ -0,0 +1,61 @@
+package com.fs.wx.sop.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+
+/**
+ * 个微营期详情Mapper接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
+    /**
+     * 查询个微营期详情
+     * 
+     * @param id 个微营期详情主键
+     * @return 个微营期详情
+     */
+    WxSopUserInfo selectWxSopUserInfoById(Long id);
+
+    /**
+     * 查询个微营期详情列表
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情集合
+     */
+    List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 新增个微营期详情
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 修改个微营期详情
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 删除个微营期详情
+     * 
+     * @param id 个微营期详情主键
+     * @return 结果
+     */
+    int deleteWxSopUserInfoById(Long id);
+
+    /**
+     * 批量删除个微营期详情
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserInfoByIds(Long[] ids);
+}

+ 69 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java

@@ -0,0 +1,69 @@
+package com.fs.wx.sop.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.wx.sop.domain.WxSopUser;
+
+/**
+ * 个微营期Mapper接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
+    /**
+     * 查询个微营期
+     * 
+     * @param id 个微营期主键
+     * @return 个微营期
+     */
+    WxSopUser selectWxSopUserById(Long id);
+
+    /**
+     * 查询个微营期列表
+     * 
+     * @param wxSopUser 个微营期
+     * @return 个微营期集合
+     */
+    List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser);
+
+    /**
+     * 新增个微营期
+     * 
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int insertWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 修改个微营期
+     * 
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int updateWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 删除个微营期
+     * 
+     * @param id 个微营期主键
+     * @return 结果
+     */
+    int deleteWxSopUserById(Long id);
+
+    /**
+     * 批量删除个微营期
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserByIds(Long[] ids);
+
+    /**
+     * 根据SOP ID删除执行账号
+     * 
+     * @param sopId SOP主键
+     * @return 结果
+     */
+    int deleteBySopId(Long sopId);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java

@@ -0,0 +1,61 @@
+package com.fs.wx.sop.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wx.sop.domain.WxSopLogs;
+
+/**
+ * 个微发送记录Service接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopLogsService extends IService<WxSopLogs>{
+    /**
+     * 查询个微发送记录
+     * 
+     * @param id 个微发送记录主键
+     * @return 个微发送记录
+     */
+    WxSopLogs selectWxSopLogsById(Long id);
+
+    /**
+     * 查询个微发送记录列表
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 个微发送记录集合
+     */
+    List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
+
+    /**
+     * 新增个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    int insertWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 修改个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    int updateWxSopLogs(WxSopLogs wxSopLogs);
+
+    /**
+     * 批量删除个微发送记录
+     * 
+     * @param ids 需要删除的个微发送记录主键集合
+     * @return 结果
+     */
+    int deleteWxSopLogsByIds(Long[] ids);
+
+    /**
+     * 删除个微发送记录信息
+     * 
+     * @param id 个微发送记录主键
+     * @return 结果
+     */
+    int deleteWxSopLogsById(Long id);
+}

+ 69 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopService.java

@@ -0,0 +1,69 @@
+package com.fs.wx.sop.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wx.sop.domain.WxSop;
+
+/**
+ * 个微SOPService接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopService extends IService<WxSop>{
+    /**
+     * 查询个微SOP
+     * 
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    WxSop selectWxSopById(Long id);
+
+    /**
+     * 查询个微SOP详情(含执行账号 companyUserIds)
+     * 
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    WxSop selectWxSopDetailById(Long id);
+
+    /**
+     * 查询个微SOP列表
+     * 
+     * @param wxSop 个微SOP
+     * @return 个微SOP集合
+     */
+    List<WxSop> selectWxSopList(WxSop wxSop);
+
+    /**
+     * 新增个微SOP
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int insertWxSop(WxSop wxSop);
+
+    /**
+     * 修改个微SOP
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    int updateWxSop(WxSop wxSop);
+
+    /**
+     * 批量删除个微SOP
+     * 
+     * @param ids 需要删除的个微SOP主键集合
+     * @return 结果
+     */
+    int deleteWxSopByIds(Long[] ids);
+
+    /**
+     * 删除个微SOP信息
+     * 
+     * @param id 个微SOP主键
+     * @return 结果
+     */
+    int deleteWxSopById(Long id);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserInfoService.java

@@ -0,0 +1,61 @@
+package com.fs.wx.sop.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+
+/**
+ * 个微营期详情Service接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopUserInfoService extends IService<WxSopUserInfo>{
+    /**
+     * 查询个微营期详情
+     * 
+     * @param id 个微营期详情主键
+     * @return 个微营期详情
+     */
+    WxSopUserInfo selectWxSopUserInfoById(Long id);
+
+    /**
+     * 查询个微营期详情列表
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情集合
+     */
+    List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 新增个微营期详情
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 修改个微营期详情
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
+
+    /**
+     * 批量删除个微营期详情
+     * 
+     * @param ids 需要删除的个微营期详情主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserInfoByIds(Long[] ids);
+
+    /**
+     * 删除个微营期详情信息
+     * 
+     * @param id 个微营期详情主键
+     * @return 结果
+     */
+    int deleteWxSopUserInfoById(Long id);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserService.java

@@ -0,0 +1,61 @@
+package com.fs.wx.sop.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.wx.sop.domain.WxSopUser;
+
+/**
+ * 个微营期Service接口
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+public interface IWxSopUserService extends IService<WxSopUser>{
+    /**
+     * 查询个微营期
+     * 
+     * @param id 个微营期主键
+     * @return 个微营期
+     */
+    WxSopUser selectWxSopUserById(Long id);
+
+    /**
+     * 查询个微营期列表
+     * 
+     * @param wxSopUser 个微营期
+     * @return 个微营期集合
+     */
+    List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser);
+
+    /**
+     * 新增个微营期
+     * 
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int insertWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 修改个微营期
+     * 
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    int updateWxSopUser(WxSopUser wxSopUser);
+
+    /**
+     * 批量删除个微营期
+     * 
+     * @param ids 需要删除的个微营期主键集合
+     * @return 结果
+     */
+    int deleteWxSopUserByIds(Long[] ids);
+
+    /**
+     * 删除个微营期信息
+     * 
+     * @param id 个微营期主键
+     * @return 结果
+     */
+    int deleteWxSopUserById(Long id);
+}

+ 104 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java

@@ -0,0 +1,104 @@
+package com.fs.wx.sop.service.impl;
+
+import java.util.List;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.wx.sop.mapper.WxSopLogsMapper;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.service.IWxSopLogsService;
+
+/**
+ * 个微发送记录Service业务层处理
+ *
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs> implements IWxSopLogsService {
+
+    /**
+     * 查询个微发送记录
+     *
+     * @param id 个微发送记录主键
+     * @return 个微发送记录
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSopLogs selectWxSopLogsById(Long id)
+    {
+        return baseMapper.selectWxSopLogsById(id);
+    }
+
+    /**
+     * 查询个微发送记录列表
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 个微发送记录
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs)
+    {
+        return baseMapper.selectWxSopLogsList(wxSopLogs);
+    }
+
+    /**
+     * 新增个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSopLogs(WxSopLogs wxSopLogs)
+    {
+        wxSopLogs.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSopLogs(wxSopLogs);
+    }
+
+    /**
+     * 修改个微发送记录
+     * 
+     * @param wxSopLogs 个微发送记录
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSopLogs(WxSopLogs wxSopLogs)
+    {
+        wxSopLogs.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSopLogs(wxSopLogs);
+    }
+
+    /**
+     * 批量删除个微发送记录
+     * 
+     * @param ids 需要删除的个微发送记录主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopLogsByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopLogsByIds(ids);
+    }
+
+    /**
+     * 删除个微发送记录信息
+     * 
+     * @param id 个微发送记录主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopLogsById(Long id)
+    {
+        return baseMapper.deleteWxSopLogsById(id);
+    }
+}

+ 161 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java

@@ -0,0 +1,161 @@
+package com.fs.wx.sop.service.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.wx.sop.mapper.WxSopMapper;
+import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.service.IWxSopService;
+
+/**
+ * 个微SOPService业务层处理
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements IWxSopService {
+
+    @Autowired
+    private ICompanyWxAccountService companyWxAccountService;
+
+    /**
+     * 查询个微SOP
+     * 
+     * @param id 个微SOP主键
+     * @return 个微SOP
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSop selectWxSopById(Long id)
+    {
+        return baseMapper.selectWxSopById(id);
+    }
+
+    @Override
+    public WxSop selectWxSopDetailById(Long id)
+    {
+        WxSop wxSop = baseMapper.selectWxSopById(id);
+        if (wxSop != null) {
+            fillSelectedQwUsers(wxSop);
+        }
+        return wxSop;
+    }
+
+    /**
+     * 填充执行账号 selectedQwUsers(从 accountIds 解析并查询账号详情)
+     */
+    private void fillSelectedQwUsers(WxSop wxSop) {
+        String accountIdsStr = wxSop.getAccountIds();
+        if (StringUtils.isEmpty(accountIdsStr)) {
+            return;
+        }
+        List<Long> accountIds = Arrays.stream(accountIdsStr.split(","))
+            .map(String::trim)
+            .filter(s -> !s.isEmpty())
+            .map(Long::parseLong)
+            .collect(Collectors.toList());
+        if (accountIds.isEmpty()) {
+            return;
+        }
+        List<CompanyWxAccount> accounts = (List<CompanyWxAccount>) companyWxAccountService.listByIds(accountIds);
+        if (accounts != null) {
+            List<Map<String, Object>> selectedQwUsers = new ArrayList<>();
+            for (CompanyWxAccount acc : accounts) {
+                Map<String, Object> m = new HashMap<>();
+                m.put("id", acc.getId());
+                m.put("wxNickName", acc.getWxNickName());
+                m.put("wxNo", acc.getWxNo());
+                m.put("companyUserName", acc.getCompanyUserId() != null ? String.valueOf(acc.getCompanyUserId()) : null);
+                selectedQwUsers.add(m);
+            }
+            wxSop.setSelectedQwUsers(selectedQwUsers);
+        }
+    }
+
+    /**
+     * 查询个微SOP列表
+     * 
+     * @param wxSop 个微SOP
+     * @return 个微SOP
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSop> selectWxSopList(WxSop wxSop)
+    {
+        return baseMapper.selectWxSopList(wxSop);
+    }
+
+    /**
+     * 新增个微SOP(含执行账号保存到 wx_sop_user)
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    /**
+     * 新增个微SOP(accountIds 直接入库到 wx_sop.account_ids)
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSop(WxSop wxSop)
+    {
+        wxSop.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSop(wxSop);
+    }
+
+    /**
+     * 修改个微SOP(accountIds 直接入库到 wx_sop.account_ids)
+     * 
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSop(WxSop wxSop)
+    {
+        wxSop.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSop(wxSop);
+    }
+
+    /**
+     * 批量删除个微SOP
+     * 
+     * @param ids 需要删除的个微SOP主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopByIds(ids);
+    }
+
+    /**
+     * 删除个微SOP信息
+     * 
+     * @param id 个微SOP主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopById(Long id)
+    {
+        return baseMapper.deleteWxSopById(id);
+    }
+}

+ 103 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserInfoServiceImpl.java

@@ -0,0 +1,103 @@
+package com.fs.wx.sop.service.impl;
+
+import java.util.List;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.service.IWxSopUserInfoService;
+
+/**
+ * 个微营期详情Service业务层处理
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, WxSopUserInfo> implements IWxSopUserInfoService {
+
+    /**
+     * 查询个微营期详情
+     * 
+     * @param id 个微营期详情主键
+     * @return 个微营期详情
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSopUserInfo selectWxSopUserInfoById(Long id)
+    {
+        return baseMapper.selectWxSopUserInfoById(id);
+    }
+
+    /**
+     * 查询个微营期详情列表
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo)
+    {
+        return baseMapper.selectWxSopUserInfoList(wxSopUserInfo);
+    }
+
+    /**
+     * 新增个微营期详情
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo)
+    {
+        wxSopUserInfo.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSopUserInfo(wxSopUserInfo);
+    }
+
+    /**
+     * 修改个微营期详情
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo)
+    {
+        wxSopUserInfo.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSopUserInfo(wxSopUserInfo);
+    }
+
+    /**
+     * 批量删除个微营期详情
+     * 
+     * @param ids 需要删除的个微营期详情主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserInfoByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopUserInfoByIds(ids);
+    }
+
+    /**
+     * 删除个微营期详情信息
+     * 
+     * @param id 个微营期详情主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserInfoById(Long id)
+    {
+        return baseMapper.deleteWxSopUserInfoById(id);
+    }
+}

+ 103 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java

@@ -0,0 +1,103 @@
+package com.fs.wx.sop.service.impl;
+
+import java.util.List;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.service.IWxSopUserService;
+
+/**
+ * 个微营期Service业务层处理
+ * 
+ * @author 吴树波
+ * @date 2026-02-24
+ */
+@Service
+public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser> implements IWxSopUserService {
+
+    /**
+     * 查询个微营期
+     * 
+     * @param id 个微营期主键
+     * @return 个微营期
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public WxSopUser selectWxSopUserById(Long id)
+    {
+        return baseMapper.selectWxSopUserById(id);
+    }
+
+    /**
+     * 查询个微营期列表
+     * 
+     * @param wxSopUser 个微营期
+     * @return 个微营期
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser)
+    {
+        return baseMapper.selectWxSopUserList(wxSopUser);
+    }
+
+    /**
+     * 新增个微营期
+     * 
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int insertWxSopUser(WxSopUser wxSopUser)
+    {
+        wxSopUser.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxSopUser(wxSopUser);
+    }
+
+    /**
+     * 修改个微营期
+     * 
+     * @param wxSopUser 个微营期
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int updateWxSopUser(WxSopUser wxSopUser)
+    {
+        wxSopUser.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxSopUser(wxSopUser);
+    }
+
+    /**
+     * 批量删除个微营期
+     * 
+     * @param ids 需要删除的个微营期主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxSopUserByIds(ids);
+    }
+
+    /**
+     * 删除个微营期信息
+     * 
+     * @param id 个微营期主键
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public int deleteWxSopUserById(Long id)
+    {
+        return baseMapper.deleteWxSopUserById(id);
+    }
+}

+ 85 - 0
fs-service/src/main/java/com/fs/wxcid/ImageToBase64Util.java

@@ -0,0 +1,85 @@
+package com.fs.wxcid;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Base64;
+
+/**
+ * 网络图片转Base64工具类
+ */
+public class ImageToBase64Util {
+
+    /**
+     * 网络图片转Base64编码字符串
+     * @param imageUrl 图片的网络URL(如https://xxx.com/xxx.png)
+     * @return Base64编码字符串(不带data:image/xxx;base64,前缀)
+     * @throws Exception 异常(网络错误、图片读取失败等)
+     */
+    public static String convertImageUrlToBase64(String imageUrl) throws Exception {
+        // 1. 校验URL参数
+        if (imageUrl == null || imageUrl.trim().isEmpty()) {
+            throw new IllegalArgumentException("图片URL不能为空");
+        }
+
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        ByteArrayOutputStream outputStream = null;
+
+        try {
+            // 2. 打开URL连接
+            URL url = new URL(imageUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            // 设置连接参数(防超时、防重定向问题)
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(5000); // 连接超时5秒
+            connection.setReadTimeout(5000);    // 读取超时5秒
+            connection.setInstanceFollowRedirects(true); // 允许重定向
+
+            // 3. 校验响应码(200表示成功)
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new Exception("图片URL访问失败,响应码:" + responseCode);
+            }
+
+            // 4. 读取图片流到字节数组
+            inputStream = connection.getInputStream();
+            outputStream = new ByteArrayOutputStream();
+            byte[] buffer = new byte[1024]; // 缓冲区,每次读1KB
+            int len;
+            while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+            }
+
+            // 5. 将字节数组编码为Base64
+            byte[] imageBytes = outputStream.toByteArray();
+            return Base64.getEncoder().encodeToString(imageBytes);
+
+        } finally {
+            // 6. 关闭所有资源(避免内存泄漏)
+            if (outputStream != null) {
+                outputStream.close();
+            }
+            if (inputStream != null) {
+                inputStream.close();
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    // 测试示例
+    public static void main(String[] args) {
+        try {
+            // 替换为你的网络图片URL
+            String imageUrl = "https://czwh.obs.cn-southwest-2.myhuaweicloud.com/fs/20260225/1771999986358.png";
+            String base64Str = convertImageUrlToBase64(imageUrl);
+            System.out.println(base64Str);
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.out.println("转换失败:" + e.getMessage());
+        }
+    }
+}

+ 1 - 1
fs-service/src/main/java/com/fs/wxcid/dto/message/MsgItem.java

@@ -25,7 +25,7 @@ public class MsgItem {
      *  2 = 图片
      *  其他值请参考协议文档 */
     @JsonProperty("MsgType")
-    private Integer MsgType;
+    private Integer MsgType = 0;
 
     /** 文本消息内容 */
     @JsonProperty("TextContent")

+ 2 - 1
fs-service/src/main/java/com/fs/wxcid/service/MessageService.java

@@ -7,6 +7,7 @@ import com.fs.wxcid.dto.message.*;
 import java.util.List;
 
 public interface MessageService {
-    ApiResponseCommon<List<SendMessageResult>> sendTextMessage(Long accountId);
+    ApiResponseCommon<List<SendMessageResult>> sendTextMessage(Long accountId, String txt);
+    ApiResponseCommon<List<SendMessageResult>> sendImageMessage(Long accountId, String imgUrl);
 
 }

+ 35 - 1
fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java

@@ -2,9 +2,11 @@ package com.fs.wxcid.service.impl;
 
 import com.alibaba.fastjson.TypeReference;
 import com.fs.common.exception.CustomException;
+import com.fs.wxcid.ImageToBase64Util;
 import com.fs.wxcid.ServiceUtils;
 import com.fs.wxcid.dto.common.ApiResponseCommon;
 import com.fs.wxcid.dto.login.RequestBaseVo;
+import com.fs.wxcid.dto.message.MsgItem;
 import com.fs.wxcid.dto.message.SendMessageResult;
 import com.fs.wxcid.dto.message.SendTextMessageRequest;
 import com.fs.wxcid.service.MessageService;
@@ -22,8 +24,12 @@ public class MessageServiceImpl implements MessageService {
     private ServiceUtils serviceUtils;
 
     @Override
-    public ApiResponseCommon<List<SendMessageResult>> sendTextMessage(Long accountId) {
+    public ApiResponseCommon<List<SendMessageResult>> sendTextMessage(Long accountId, String txt) {
         SendTextMessageRequest request = new SendTextMessageRequest();
+        List<MsgItem> list = new ArrayList<>();
+        MsgItem msgItem = new MsgItem();
+        msgItem.setTextContent(txt);
+        request.setMsgItem(list);
         ApiResponseCommon<List<SendMessageResult>> response = serviceUtils.sendPost(BASE_URL + "SendTextMessage", RequestBaseVo.builder().accountId(accountId).data(request).build(), new TypeReference<ApiResponseCommon<List<SendMessageResult>>>() {});
         // 第二层:检查每条消息是否真正发送成功
         List<SendMessageResult> results = response.getData();
@@ -39,4 +45,32 @@ public class MessageServiceImpl implements MessageService {
         }
         return response;
     }
+
+    @Override
+    public ApiResponseCommon<List<SendMessageResult>> sendImageMessage(Long accountId, String imgUrl) {
+        SendTextMessageRequest request = new SendTextMessageRequest();
+        List<MsgItem> list = new ArrayList<>();
+        MsgItem msgItem = new MsgItem();
+        try {
+            msgItem.setImageContent(ImageToBase64Util.convertImageUrlToBase64(imgUrl));
+        }catch (Exception e){
+            log.error("发送消息时,图片转换base64错误", e);
+            throw new CustomException("图片消息发送失败");
+        }
+        request.setMsgItem(list);
+        ApiResponseCommon<List<SendMessageResult>> response = serviceUtils.sendPost(BASE_URL + "SendImageNewMessage", RequestBaseVo.builder().accountId(accountId).data(request).build(), new TypeReference<ApiResponseCommon<List<SendMessageResult>>>() {});
+        // 第二层:检查每条消息是否真正发送成功
+        List<SendMessageResult> results = response.getData();
+        List<String> failedRecipients = new ArrayList<>();
+        for (SendMessageResult result : results) {
+            if (!result.isSendSuccess()) {
+                failedRecipients.add(result.getToUserName());
+            }
+        }
+        if (!failedRecipients.isEmpty()) {
+            String errorMsg = "部分消息发送失败,失败接收人: " + String.join(", ", failedRecipients);
+            throw new CustomException("发送文本消息部分失败: " + errorMsg);
+        }
+        return response;
+    }
 }

+ 22 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxwSendNoticeMsgDTO.java

@@ -0,0 +1,22 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+/**
+ * 发送群公告请求DTO
+ */
+@Data
+public class WxwSendNoticeMsgDTO {
+    /**
+     * 消息的唯一标识符 (UUID)
+     */
+    private String uuid;
+    /**
+     * 群聊room_id
+     */
+    private Long roomid;
+    /**
+     * 群公告内容
+     */
+    private String msg;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxwSendNoticeMsgRespDTO.java

@@ -0,0 +1,22 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+/**
+ * 发送群公告响应DTO
+ */
+@Data
+public class WxwSendNoticeMsgRespDTO {
+    /**
+     * 数据(通常为null)
+     */
+    private Object data;
+    /**
+     * 错误码
+     */
+    private Integer errcode;
+    /**
+     * 错误信息
+     */
+    private String errmsg;
+}

+ 6 - 2
fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceNew.java

@@ -45,6 +45,10 @@ public class WxWorkServiceNew {
         redisCache.setCacheObject("serverId:" + serverId,qwIpadServer.getUrl(),2, TimeUnit.HOURS);
         return qwIpadServer.getUrl();
     }
+    public WxWorkResponseDTO<WxwSendNoticeMsgRespDTO> SendNotice(WxwSendNoticeMsgDTO param,Long serverId) {
+        String url = getUrl(serverId) + "/SendNotice";
+        return WxWorkHttpUtilNew.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxwSendNoticeMsgRespDTO>>() {}, serverId);
+    }
 
     public WxWorkResponseDTO<WxWorkInitRespDTO> init(WxWorkInitDTO param,Long serverId) {
         String url = getUrl(serverId) + "/init";
@@ -233,8 +237,8 @@ public class WxWorkServiceNew {
         String url = getUrl(serverId) + "/RoomIdToChatId";
         return WxWorkHttpUtilNew.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkChatIdToRoomIdResp>>() {}, serverId);
     }
-    
-    
+
+
     public WxWorkResponseDTO<WxSendTextAtMsgVo> sendTextAtMsgTwo(WxSendTextAtMsgTwoDTO param, Long serverId) {
         String url = getUrl(serverId) + "/SendTextAtMsgTwo";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxSendTextAtMsgVo>>() {});

+ 4 - 3
fs-service/src/main/resources/application-config-druid-cfryt.yml

@@ -42,8 +42,8 @@ wx:
       port: 6379
       timeout: 2000
     configs:
-      - appId: wxbf0cfcfbc92ccd72 # 第一个公众号的appid   //赤峰润元堂
-        secret: b08b0c6e763b5fa3c863b3dd114cd1c9 # 公众号的appsecret
+      - appId: wxc2a59fb9f32b669d # 第一个公众号的appid   //赤峰润元堂
+        secret: 246b3dc35e2ddd17a206faa265713425 # 公众号的appsecret
         token: PPKOdAlCoMO # 接口配置里的Token值
         aesKey: Eswa6VjwtVcw03qZy6Wllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
 aifabu:  #爱链接
@@ -64,7 +64,7 @@ fs :
   h5CommonApi: http://172.26.180.67:7771
   jwt:
     # 加密秘钥
-    secret: f4e2e52034348f86b6d81e581c19jeb4
+    secret: f4e2e52034348f86b6d81e581c156eb4
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken
@@ -90,6 +90,7 @@ headerImg:
 ipad:
   ipadUrl: http://ipad.cfrytjkzx.cn
   aiApi: http://49.232.181.28:3000/api
+  wxIpadUrl:
   voiceApi:
   commonApi:
 wx_miniapp_temp:

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

@@ -97,6 +97,7 @@ ipad:
   aiApi: http://49.232.181.28:3000/api
   voiceApi:
   commonApi:
+  wxIpadUrl:
 wx_miniapp_temp:
   pay_order_temp_id:
   inquiry_temp_id:

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

@@ -93,6 +93,7 @@ ipad:
   aiApi: http://
   voiceApi: http://123.207.48.104:8009
   commonApi: http://123.207.48.104:7771
+  wxIpadUrl:
 wx_miniapp_temp:
   pay_order_temp_id:
   inquiry_temp_id:

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

@@ -96,6 +96,7 @@ ipad:
   aiApi: http://49.232.181.28:3000/api
   voiceApi: http://123.207.48.104:8009
   commonApi: http://123.207.48.104:7771
+  wxIpadUrl:
 wx_miniapp_temp:
   pay_order_temp_id:
   inquiry_temp_id:

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

@@ -93,6 +93,7 @@ ipad:
   aiApi:
   voiceApi:
   commonApi:
+  wxIpadUrl:
 wx_miniapp_temp:
   pay_order_temp_id:
   inquiry_temp_id:

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

@@ -95,6 +95,7 @@ headerImg:
 ipad:
   ipadUrl: http://ipad.xintaihl.cn
   aiApi: http://49.232.181.28:3000/api
+  wxIpadUrl:
   voiceApi:
   commonApi:
 wx_miniapp_temp:

+ 45 - 0
fs-service/src/main/resources/db/20260226-个微SOP表结构.sql

@@ -0,0 +1,45 @@
+-- 个微SOP表结构(his_sop 数据库)
+-- 对应 @DataSource(DataSourceType.SOP),请在 SOP 数据源对应的数据库中执行
+
+-- 执行账号表
+CREATE TABLE IF NOT EXISTS `wx_sop_user` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `type` int(11) DEFAULT NULL COMMENT '类型(0个人1群聊)',
+  `sop_id` bigint(20) DEFAULT NULL COMMENT '任务ID',
+  `account_id` bigint(20) DEFAULT NULL COMMENT '个微账号ID',
+  `start_time` date DEFAULT NULL COMMENT '营期时间',
+  `chat_id` varchar(100) DEFAULT NULL COMMENT '群聊ID',
+  `status` int(11) DEFAULT NULL COMMENT '状态(0正常1暂停)',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  PRIMARY KEY (`id`),
+  KEY `idx_sop_id` (`sop_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个微SOP执行账号表';
+
+-- 个微SOP主表
+CREATE TABLE IF NOT EXISTS `wx_sop` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `name` varchar(100) DEFAULT NULL COMMENT '名称',
+  `filter_type` int(11) DEFAULT NULL COMMENT '筛选方式(0标签1群聊)',
+  `select_tags` varchar(500) DEFAULT NULL COMMENT '选择的标签',
+  `exclude_tags` varchar(500) DEFAULT NULL COMMENT '排查的标签',
+  `temp_id` varchar(50) DEFAULT NULL COMMENT '模板ID',
+  `company_id` bigint(20) DEFAULT NULL COMMENT '公司ID',
+  `is_fixed` int(11) DEFAULT NULL COMMENT '是否固定营期(0否1是)',
+  `expiry_time` int(11) DEFAULT NULL COMMENT '过期时间(小时)',
+  `start_time` date DEFAULT NULL COMMENT '营期开始时间',
+  `account_ids` varchar(500) DEFAULT NULL COMMENT '执行账号ID,逗号分隔',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  PRIMARY KEY (`id`),
+  KEY `idx_company_id` (`company_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个微SOP表';
+
+-- 若 wx_sop 表已存在且无 account_ids 字段,可执行:
+-- ALTER TABLE wx_sop ADD COLUMN account_ids varchar(500) DEFAULT NULL COMMENT '执行账号ID,逗号分隔' AFTER start_time;

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

@@ -21,7 +21,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCompanyVoiceDialogList" parameterType="CompanyVoiceDialog" resultMap="CompanyVoiceDialogResult">
         <include refid="selectCompanyVoiceDialogVo"/>
-        <where>  
+        <where>
+            <if test="companyId != null"> and company_id = #{companyId} </if>
             <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
             <if test="resultName != null  and resultName != ''"> and result_name like concat('%', #{resultName}, '%')</if>
             <if test="resultId != null "> and result_id = #{resultId}</if>

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

@@ -61,6 +61,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_user u on a.create_user = u.user_id
         inner join company_dept d on u.dept_id = d.dept_id
         <where>
+            <if test="companyId != null"> and a.company_id = #{companyId}</if>
             <if test="name != null  and name != ''"> and a.name like concat('%', #{name}, '%')</if>
             <if test="taskName != null  and taskName != ''"> and a.task_name like concat('%', #{taskName}, '%')</if>
             <if test="taskId != null "> and a.task_id = #{taskId}</if>

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

@@ -53,6 +53,7 @@
             <if test="workflowType != null">and workflow_type = #{workflowType}</if>
             <if test="status != null">and status = #{status}</if>
             <if test="companyUserId != null">and company_user_id = #{companyUserId}</if>
+            <if test="companyId != null"> and company_id = #{companyId} </if>
         </where>
         order by create_time desc
     </select>

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

@@ -34,6 +34,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_user u on a.company_user_id = u.user_id
         inner join company_dept d on u.dept_id = d.dept_id
         <where>
+            <if test="companyId != null"> and a.company_id = #{companyId} </if>
             <if test="wxNickName != null  and wxNickName != ''"> and a.wx_nick_name like concat( #{wxNickName}, '%')</if>
             <if test="phone != null  and phone != ''"> and a.phone like concat( #{phone}, '%')</if>
             <if test="wxNo != null  and wxNo != ''"> and a.wx_no = #{wxNo}</if>

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

@@ -55,6 +55,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_wx_dialog c on a.dialog_id = c.id
         inner join company_voice_robotic robotic on a.robotic_id = robotic.id
         <where>
+            <if test="companyId != null"> and b.company_id = #{companyId}</if>
             <if test="roboticId != null "> and a.robotic_id = #{roboticId}</if>
             <if test="roboticWxId != null "> and b.id = #{roboticWxId}</if>
             <if test="customerId != null "> and a.customer_id = #{customerId}</if>

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

@@ -17,7 +17,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectCompanyWxDialogList" parameterType="CompanyWxDialog" resultMap="CompanyWxDialogResult">
         <include refid="selectCompanyWxDialogVo"/>
-        <where>  
+        <where>
+            <if test="companyId != null"> and company_id = #{companyId}</if>
             <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
             <if test="templateDetails != null  and templateDetails != ''"> and template_details = #{templateDetails}</if>
         </where>

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

@@ -400,7 +400,7 @@
     </select>
 
     <update id="updateRedPacketMoney">
-        update fs_user_course_video set red_packet_money = #{redPacketMoney} where video_id = #{videoId}
+        update fs_user_course_video set red_packet_money = #{redPacketMoney},title = #{title} where video_id = #{videoId}
     </update>
 
     <update id="batchUpdateByVideoId">

+ 146 - 0
fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml

@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopLogsMapper">
+    
+    <resultMap type="WxSopLogs" id="WxSopLogsResult">
+        <result property="id"    column="id"    />
+        <result property="type"    column="type"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopUserId"    column="sop_user_id"    />
+        <result property="sendType"    column="send_type"    />
+        <result property="generateType"    column="generate_type"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="wxContactName"    column="wx_contact_name"    />
+        <result property="wxRoomId"    column="wx_room_id"    />
+        <result property="wxRoomName"    column="wx_room_name"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="sendStatus"    column="send_status"    />
+        <result property="sendRemark"    column="send_remark"    />
+        <result property="sendSort"    column="send_sort"    />
+        <result property="expirationTime"    column="expiration_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopLogsVo">
+        select id, type, sop_id, sop_user_id, send_type, generate_type, account_id, wx_contact_id, wx_contact_name, wx_room_id, wx_room_name, fs_user_id, send_status, send_remark, send_sort, expiration_time, create_time, create_by, update_time, update_by, remark from wx_sop_logs
+    </sql>
+
+    <select id="selectWxSopLogsList" parameterType="WxSopLogs" resultMap="WxSopLogsResult">
+        <include refid="selectWxSopLogsVo"/>
+        <where>  
+            <if test="type != null "> and type = #{type}</if>
+            <if test="sopId != null "> and sop_id = #{sopId}</if>
+            <if test="sopUserId != null "> and sop_user_id = #{sopUserId}</if>
+            <if test="sendType != null "> and send_type = #{sendType}</if>
+            <if test="generateType != null "> and generate_type = #{generateType}</if>
+            <if test="accountId != null "> and account_id = #{accountId}</if>
+            <if test="wxContactId != null "> and wx_contact_id = #{wxContactId}</if>
+            <if test="wxContactName != null  and wxContactName != ''"> and wx_contact_name like concat('%', #{wxContactName}, '%')</if>
+            <if test="wxRoomId != null "> and wx_room_id = #{wxRoomId}</if>
+            <if test="wxRoomName != null  and wxRoomName != ''"> and wx_room_name like concat('%', #{wxRoomName}, '%')</if>
+            <if test="fsUserId != null "> and fs_user_id = #{fsUserId}</if>
+            <if test="sendStatus != null "> and send_status = #{sendStatus}</if>
+            <if test="sendRemark != null  and sendRemark != ''"> and send_remark = #{sendRemark}</if>
+            <if test="sendSort != null "> and send_sort = #{sendSort}</if>
+            <if test="expirationTime != null "> and expiration_time = #{expirationTime}</if>
+        </where>
+    </select>
+    
+    <select id="selectWxSopLogsById" parameterType="Long" resultMap="WxSopLogsResult">
+        <include refid="selectWxSopLogsVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertWxSopLogs" parameterType="WxSopLogs" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop_logs
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="type != null">type,</if>
+            <if test="sopId != null">sop_id,</if>
+            <if test="sopUserId != null">sop_user_id,</if>
+            <if test="sendType != null">send_type,</if>
+            <if test="generateType != null">generate_type,</if>
+            <if test="accountId != null">account_id,</if>
+            <if test="wxContactId != null">wx_contact_id,</if>
+            <if test="wxContactName != null">wx_contact_name,</if>
+            <if test="wxRoomId != null">wx_room_id,</if>
+            <if test="wxRoomName != null">wx_room_name,</if>
+            <if test="fsUserId != null">fs_user_id,</if>
+            <if test="sendStatus != null">send_status,</if>
+            <if test="sendRemark != null">send_remark,</if>
+            <if test="sendSort != null">send_sort,</if>
+            <if test="expirationTime != null">expiration_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="type != null">#{type},</if>
+            <if test="sopId != null">#{sopId},</if>
+            <if test="sopUserId != null">#{sopUserId},</if>
+            <if test="sendType != null">#{sendType},</if>
+            <if test="generateType != null">#{generateType},</if>
+            <if test="accountId != null">#{accountId},</if>
+            <if test="wxContactId != null">#{wxContactId},</if>
+            <if test="wxContactName != null">#{wxContactName},</if>
+            <if test="wxRoomId != null">#{wxRoomId},</if>
+            <if test="wxRoomName != null">#{wxRoomName},</if>
+            <if test="fsUserId != null">#{fsUserId},</if>
+            <if test="sendStatus != null">#{sendStatus},</if>
+            <if test="sendRemark != null">#{sendRemark},</if>
+            <if test="sendSort != null">#{sendSort},</if>
+            <if test="expirationTime != null">#{expirationTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSopLogs" parameterType="WxSopLogs">
+        update wx_sop_logs
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="type != null">type = #{type},</if>
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="sopUserId != null">sop_user_id = #{sopUserId},</if>
+            <if test="sendType != null">send_type = #{sendType},</if>
+            <if test="generateType != null">generate_type = #{generateType},</if>
+            <if test="accountId != null">account_id = #{accountId},</if>
+            <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
+            <if test="wxContactName != null">wx_contact_name = #{wxContactName},</if>
+            <if test="wxRoomId != null">wx_room_id = #{wxRoomId},</if>
+            <if test="wxRoomName != null">wx_room_name = #{wxRoomName},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="sendStatus != null">send_status = #{sendStatus},</if>
+            <if test="sendRemark != null">send_remark = #{sendRemark},</if>
+            <if test="sendSort != null">send_sort = #{sendSort},</if>
+            <if test="expirationTime != null">expiration_time = #{expirationTime},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopLogsById" parameterType="Long">
+        delete from wx_sop_logs where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopLogsByIds" parameterType="String">
+        delete from wx_sop_logs where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 121 - 0
fs-service/src/main/resources/mapper/wx/WxSopMapper.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopMapper">
+    
+    <resultMap type="WxSop" id="WxSopResult">
+        <result property="id"    column="id"    />
+        <result property="name"    column="name"    />
+        <result property="filterType"    column="filter_type"    />
+        <result property="selectTags"    column="select_tags"    />
+        <result property="excludeTags"    column="exclude_tags"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="isFixed"    column="is_fixed"    />
+        <result property="expiryTime"    column="expiry_time"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="accountIds"    column="account_ids"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopVo">
+        select id, name, filter_type, select_tags, exclude_tags, temp_id, company_id, is_fixed, expiry_time, start_time, account_ids, create_time, create_by, update_time, update_by, remark from wx_sop
+    </sql>
+
+    <select id="selectWxSopList" parameterType="WxSop" resultMap="WxSopResult">
+        <include refid="selectWxSopVo"/>
+        <where>  
+            <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="filterType != null "> and filter_type = #{filterType}</if>
+            <if test="selectTags != null  and selectTags != ''"> and select_tags = #{selectTags}</if>
+            <if test="excludeTags != null  and excludeTags != ''"> and exclude_tags = #{excludeTags}</if>
+            <if test="tempId != null  and tempId != ''"> and temp_id = #{tempId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="isFixed != null "> and is_fixed = #{isFixed}</if>
+            <if test="expiryTime != null "> and expiry_time = #{expiryTime}</if>
+            <if test="startTime != null "> and start_time = #{startTime}</if>
+            <if test="accountIds != null  and accountIds != ''"> and account_ids = #{accountIds}</if>
+        </where>
+    </select>
+    
+    <select id="selectWxSopById" parameterType="Long" resultMap="WxSopResult">
+        <include refid="selectWxSopVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertWxSop" parameterType="WxSop" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="name != null">name,</if>
+            <if test="filterType != null">filter_type,</if>
+            <if test="selectTags != null">select_tags,</if>
+            <if test="excludeTags != null">exclude_tags,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="isFixed != null">is_fixed,</if>
+            <if test="expiryTime != null">expiry_time,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="accountIds != null">account_ids,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="name != null">#{name},</if>
+            <if test="filterType != null">#{filterType},</if>
+            <if test="selectTags != null">#{selectTags},</if>
+            <if test="excludeTags != null">#{excludeTags},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="isFixed != null">#{isFixed},</if>
+            <if test="expiryTime != null">#{expiryTime},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="accountIds != null">#{accountIds},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSop" parameterType="WxSop">
+        update wx_sop
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="name != null">name = #{name},</if>
+            <if test="filterType != null">filter_type = #{filterType},</if>
+            <if test="selectTags != null">select_tags = #{selectTags},</if>
+            <if test="excludeTags != null">exclude_tags = #{excludeTags},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="isFixed != null">is_fixed = #{isFixed},</if>
+            <if test="expiryTime != null">expiry_time = #{expiryTime},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="accountIds != null">account_ids = #{accountIds},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopById" parameterType="Long">
+        delete from wx_sop where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopByIds" parameterType="String">
+        delete from wx_sop where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 121 - 0
fs-service/src/main/resources/mapper/wx/WxSopUserInfoMapper.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopUserInfoMapper">
+    
+    <resultMap type="WxSopUserInfo" id="WxSopUserInfoResult">
+        <result property="id"    column="id"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopUserId"    column="sop_user_id"    />
+        <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="isDaysNotStudy"    column="is_days_not_study"    />
+        <result property="finishCout"    column="finish_cout"    />
+        <result property="finishTime"    column="finish_time"    />
+        <result property="finishCourseDays"    column="finish_course_days"    />
+        <result property="grade"    column="grade"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopUserInfoVo">
+        select id, sop_id, sop_user_id, wx_contact_id, fs_user_id, is_days_not_study, finish_cout, finish_time, finish_course_days, grade, status, create_time, create_by, update_time, update_by, remark from wx_sop_user_info
+    </sql>
+
+    <select id="selectWxSopUserInfoList" parameterType="WxSopUserInfo" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserInfoVo"/>
+        <where>  
+            <if test="sopId != null "> and sop_id = #{sopId}</if>
+            <if test="sopUserId != null "> and sop_user_id = #{sopUserId}</if>
+            <if test="wxContactId != null "> and wx_contact_id = #{wxContactId}</if>
+            <if test="fsUserId != null "> and fs_user_id = #{fsUserId}</if>
+            <if test="isDaysNotStudy != null "> and is_days_not_study = #{isDaysNotStudy}</if>
+            <if test="finishCout != null "> and finish_cout = #{finishCout}</if>
+            <if test="finishTime != null "> and finish_time = #{finishTime}</if>
+            <if test="finishCourseDays != null "> and finish_course_days = #{finishCourseDays}</if>
+            <if test="grade != null "> and grade = #{grade}</if>
+            <if test="status != null "> and status = #{status}</if>
+        </where>
+    </select>
+    
+    <select id="selectWxSopUserInfoById" parameterType="Long" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserInfoVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertWxSopUserInfo" parameterType="WxSopUserInfo" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop_user_info
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="sopId != null">sop_id,</if>
+            <if test="sopUserId != null">sop_user_id,</if>
+            <if test="wxContactId != null">wx_contact_id,</if>
+            <if test="fsUserId != null">fs_user_id,</if>
+            <if test="isDaysNotStudy != null">is_days_not_study,</if>
+            <if test="finishCout != null">finish_cout,</if>
+            <if test="finishTime != null">finish_time,</if>
+            <if test="finishCourseDays != null">finish_course_days,</if>
+            <if test="grade != null">grade,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="sopId != null">#{sopId},</if>
+            <if test="sopUserId != null">#{sopUserId},</if>
+            <if test="wxContactId != null">#{wxContactId},</if>
+            <if test="fsUserId != null">#{fsUserId},</if>
+            <if test="isDaysNotStudy != null">#{isDaysNotStudy},</if>
+            <if test="finishCout != null">#{finishCout},</if>
+            <if test="finishTime != null">#{finishTime},</if>
+            <if test="finishCourseDays != null">#{finishCourseDays},</if>
+            <if test="grade != null">#{grade},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSopUserInfo" parameterType="WxSopUserInfo">
+        update wx_sop_user_info
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="sopUserId != null">sop_user_id = #{sopUserId},</if>
+            <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="isDaysNotStudy != null">is_days_not_study = #{isDaysNotStudy},</if>
+            <if test="finishCout != null">finish_cout = #{finishCout},</if>
+            <if test="finishTime != null">finish_time = #{finishTime},</if>
+            <if test="finishCourseDays != null">finish_course_days = #{finishCourseDays},</if>
+            <if test="grade != null">grade = #{grade},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopUserInfoById" parameterType="Long">
+        delete from wx_sop_user_info where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopUserInfoByIds" parameterType="String">
+        delete from wx_sop_user_info where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 105 - 0
fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopUserMapper">
+    
+    <resultMap type="WxSopUser" id="WxSopUserResult">
+        <result property="id"    column="id"    />
+        <result property="type"    column="type"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="chatId"    column="chat_id"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopUserVo">
+        select id, type, sop_id, account_id, start_time, chat_id, status, create_time, create_by, update_time, update_by, remark from wx_sop_user
+    </sql>
+
+    <select id="selectWxSopUserList" parameterType="WxSopUser" resultMap="WxSopUserResult">
+        <include refid="selectWxSopUserVo"/>
+        <where>  
+            <if test="type != null "> and type = #{type}</if>
+            <if test="sopId != null "> and sop_id = #{sopId}</if>
+            <if test="accountId != null "> and account_id = #{accountId}</if>
+            <if test="startTime != null "> and start_time = #{startTime}</if>
+            <if test="chatId != null  and chatId != ''"> and chat_id = #{chatId}</if>
+            <if test="status != null "> and status = #{status}</if>
+        </where>
+    </select>
+    
+    <select id="selectWxSopUserById" parameterType="Long" resultMap="WxSopUserResult">
+        <include refid="selectWxSopUserVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertWxSopUser" parameterType="WxSopUser" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop_user
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="type != null">type,</if>
+            <if test="sopId != null">sop_id,</if>
+            <if test="accountId != null">account_id,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="chatId != null">chat_id,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="type != null">#{type},</if>
+            <if test="sopId != null">#{sopId},</if>
+            <if test="accountId != null">#{accountId},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="chatId != null">#{chatId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSopUser" parameterType="WxSopUser">
+        update wx_sop_user
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="type != null">type = #{type},</if>
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="accountId != null">account_id = #{accountId},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="chatId != null">chat_id = #{chatId},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopUserById" parameterType="Long">
+        delete from wx_sop_user where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopUserByIds" parameterType="String">
+        delete from wx_sop_user where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteBySopId" parameterType="Long">
+        delete from wx_sop_user where sop_id = #{sopId}
+    </delete>
+</mapper>

+ 45 - 0
fs-user-app/src/main/java/com/fs/app/controller/UserController.java

@@ -1,16 +1,21 @@
 package com.fs.app.controller;
 
 
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
+import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
 import cn.hutool.core.lang.Validator;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.app.annotation.Login;
 import com.fs.app.param.FsDoctorRegisterParam;
 import com.fs.app.param.FsUserEditParam;
+import com.fs.app.param.FsUserPhoneUpdateParam;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.sign.Md5Utils;
+import com.fs.core.config.WxMaConfiguration;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.his.domain.FsDoctor;
 import com.fs.his.domain.FsUser;
@@ -35,6 +40,7 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxErrorException;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -365,4 +371,43 @@ public class UserController extends  AppBaseController {
     public R removeUser(){
         return userService.removeUser(Long.parseLong(getUserId()));
     }
+
+//    @Login
+    @ApiOperation("小程序用户单独授权手机号,然后将手机号更新")
+    @PostMapping("/ma/authUserPhone")
+    public R updateUserPhone(@RequestBody @Valid FsUserPhoneUpdateParam param){
+        log.info("【小程序用户单独授权手机号】:{}",param);
+
+        if (StringUtils.isEmpty(param.getCode())) {
+            return R.error("code不存在");
+        }
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("appId不能为空");
+        }
+        if (StringUtils.isEmpty(param.getUserId())) {
+            return R.error("appId不能为空");
+        }
+        final WxMaService wxService = WxMaConfiguration.getMaService(param.getAppId());
+        try {
+            WxMaJscode2SessionResult session = wxService.getUserService().getSessionInfo(param.getCode());
+            log.info(session.getSessionKey());
+            // 解密
+            WxMaPhoneNumberInfo phoneNoInfo = wxService.getUserService().getPhoneNoInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
+            String phoneNumber = phoneNoInfo.getPhoneNumber();
+
+            // 修改用户
+            FsUser user = fsUserService.selectFsUserById(Long.parseLong(param.getUserId()));
+            if(user != null){
+                user.setPhone(phoneNumber);
+                if(userService.updateFsUser(user)>0) {
+                    return R.ok("授权更新成功").put("user",user);
+                } else {
+                    return R.error("授权更新失败");
+                }
+            }
+        } catch (WxErrorException e) {
+            return R.error("授权失败,"+e.getMessage());
+        }
+        return R.error("授权失败,请联系管理员");
+    }
 }

+ 35 - 0
fs-user-app/src/main/java/com/fs/app/param/FsUserPhoneUpdateParam.java

@@ -0,0 +1,35 @@
+package com.fs.app.param;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author caolq
+ * 小程序用户单独授权手机号参数
+ */
+@Data
+public class FsUserPhoneUpdateParam implements Serializable {
+
+    @NotBlank(message = "code不能为空")
+    @ApiModelProperty(value = "小程序登陆code")
+    private String code;
+
+    @ApiModelProperty(value = "小程序完整用户信息的加密数据")
+    private String encryptedData;
+
+    @ApiModelProperty(value = "小程序加密算法的初始向量")
+    private String iv;
+
+    @NotBlank(message = "小程序appid不能为空")
+    @ApiModelProperty(value = "小程序appid")
+    private String appId;
+
+    @NotBlank(message = "userId不能为空")
+    @ApiModelProperty(value = "当前用户id")
+    private String userId;
+
+}

+ 2 - 1
fs-websocket/src/main/java/com/fs/websocket/FsWebSocketServiceApplication.java

@@ -15,7 +15,8 @@ import org.springframework.context.annotation.FilterType;
 @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
 @ComponentScan(basePackages = {"com.fs"}, excludeFilters = {
         @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {com.fs.common.utils.RedisUtil.class,
-                com.fs.common.core.redis.RedisCache.class, com.fs.common.core.redis.RedisCacheT.class})
+                com.fs.common.core.redis.RedisCache.class, com.fs.common.core.redis.RedisCacheT.class}),
+        @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.fs.common.core.redis.service.*")
         })
 public class FsWebSocketServiceApplication extends SpringBootServletInitializer {