wjj преди 20 часа
родител
ревизия
af8267536c
променени са 66 файла, в които са добавени 2484 реда и са изтрити 285 реда
  1. 229 0
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  2. 2 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  3. 99 6
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  4. 82 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLog.java
  5. 4 2
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallBlacklistMapper.java
  6. 71 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogMapper.java
  7. 4 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java
  8. 74 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogService.java
  9. 9 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java
  10. 2 1
      fs-service/src/main/java/com/fs/company/service/ICompanyWxDialogService.java
  11. 2 2
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallBlacklistServiceImpl.java
  12. 136 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogServiceImpl.java
  13. 45 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  14. 33 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java
  15. 1 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  16. 82 0
      fs-service/src/main/java/com/fs/company/util/ObjectPlaceholderResolver.java
  17. 1 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWxClient4WorkFlowVO.java
  18. 17 0
      fs-service/src/main/java/com/fs/company/vo/SendMsgVo.java
  19. 120 0
      fs-service/src/main/java/com/fs/course/config/RedisKeyScanner.java
  20. 73 0
      fs-service/src/main/java/com/fs/course/config/WxConfig.java
  21. 2 0
      fs-service/src/main/java/com/fs/qw/domain/QwUser.java
  22. 11 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  23. 38 0
      fs-service/src/main/java/com/fs/wxcid/dto/callback/WxCallbackVo.java
  24. 16 0
      fs-service/src/main/java/com/fs/wxcid/dto/friend/AddContactParam.java
  25. 14 0
      fs-service/src/main/java/com/fs/wxcid/dto/friend/SearchContactParam.java
  26. 86 0
      fs-service/src/main/java/com/fs/wxcid/dto/friend/SearchContactResponse.java
  27. 19 0
      fs-service/src/main/java/com/fs/wxcid/dto/friend/VerifyUserParam.java
  28. 74 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/CdnUploadVideoResult.java
  29. 59 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/ChatSendRet.java
  30. 22 15
      fs-service/src/main/java/com/fs/wxcid/dto/message/RevokeMsgRequest.java
  31. 25 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/RevokeMsgResult.java
  32. 15 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendImageMessageParam.java
  33. 40 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendImageMessageResult.java
  34. 46 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendImageResponse.java
  35. 35 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendMessageResult.java
  36. 32 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendResponse.java
  37. 14 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendTextMessageParam.java
  38. 27 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageParam.java
  39. 20 0
      fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageRequest.java
  40. 9 6
      fs-service/src/main/java/com/fs/wxcid/service/FriendService.java
  41. 66 0
      fs-service/src/main/java/com/fs/wxcid/service/IWxMsgLogService.java
  42. 6 24
      fs-service/src/main/java/com/fs/wxcid/service/MessageService.java
  43. 61 4
      fs-service/src/main/java/com/fs/wxcid/service/impl/FriendServiceImpl.java
  44. 102 147
      fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java
  45. 116 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/WxMsgLogServiceImpl.java
  46. 26 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxAddSearchDTO.java
  47. 21 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxSearchContactDTO.java
  48. 35 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxSearchContactResp.java
  49. 15 0
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java
  50. 14 0
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java
  51. 3 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallBlacklistMapper.xml
  52. 98 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogMapper.xml
  53. 12 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  54. 1 1
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  55. 0 1
      fs-wx-api/src/main/java/com/fs/app/controller/AppBaseController.java
  56. 43 0
      fs-wx-api/src/main/java/com/fs/app/controller/CallBackController.java
  57. 3 36
      fs-wx-api/src/main/java/com/fs/app/controller/CommonController.java
  58. 147 0
      fs-wx-api/src/main/java/com/fs/app/controller/WebscoketServer.java
  59. 0 1
      fs-wx-api/src/main/java/com/fs/app/param/CompanyWxListParam.java
  60. 0 1
      fs-wx-api/src/main/java/com/fs/app/websocket/bean/MsgBean.java
  61. 1 32
      fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java
  62. 0 1
      fs-wx-api/src/main/java/com/fs/framework/aspectj/LogAspect.java
  63. 1 1
      fs-wx-api/src/main/java/com/fs/framework/config/DruidConfig.java
  64. 0 2
      fs-wx-api/src/main/java/com/fs/framework/config/MyBatisConfig.java
  65. 0 1
      fs-wx-api/src/main/java/com/fs/framework/config/SwaggerConfig.java
  66. 23 0
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

+ 229 - 0
fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java

@@ -0,0 +1,229 @@
+package com.fs.company.controller.company;
+
+import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.aicall.service.ICcLlmAgentAccountService;
+import com.fs.aicall.service.ICompanyBindAiModelService;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.*;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/6 14:28
+ * @description EasyCallCenter365 外呼管理控制器
+ * <p>
+ * 所有接口均需要登录态,通过 TokenService 获取当前登录用户的 companyId。
+ * 接口地址前缀:/company/easyCall
+ * 对应三方服务器:http://129.28.164.235:8899
+ */
+@Api(tags = "EasyCallCenter365外呼管理")
+@RestController
+@RequestMapping("/company/easyCall")
+public class EasyCallController extends BaseController {
+
+    @Autowired
+    private IEasyCallService easyCallService;
+
+    /** 用于获取当前登录用户信息,从中取出 companyId */
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ICompanyBindAiModelService companyBindAiModelService;
+
+    @Autowired
+    private ICcLlmAgentAccountService ccLlmAgentAccountService;
+
+    // =================== 基础数据查询 ===================
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/list")
+    public R getGatewayList() {
+        // 获取当前登录用户的公司ID(当前仅用于传递,未涉及多公司隔离)
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取大模型配置列表
+     * 创建 AI 外呼任务时需要选择大模型配置,决定机器人论某能力和话术
+     */
+    @ApiOperation("获取大模型配置列表")
+    @GetMapping("/llmAccount/list")
+    public R getLlmAccountList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        CcLlmAgentAccount ccLlmAgentAccount = new CcLlmAgentAccount();
+        if (companyId != null) {
+            List<Long> modelIds = companyBindAiModelService.selectModelIdsByCompanyId(companyId);
+            if (!modelIds.isEmpty()) {
+                ccLlmAgentAccount.setModelIds(modelIds);
+            } else {
+                return R.ok().put("data", new ArrayList<>());
+            }
+        }
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+//        List<EasyCallLlmAccountVO> list = easyCallService.getLlmAccountList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取音色列表
+     * AI 外呼任务用于配置语音合成声音风格,如普通话男声、女声等
+     */
+    @ApiOperation("获取音色列表")
+    @GetMapping("/voiceCode/list")
+    public R getVoiceCodeList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallVoiceCodeVO> list = easyCallService.getVoiceCodeList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取技能组列表
+     * 技能组也叫业务组,AI 外呼需要转人工时用于指定转入哪个客服组
+     */
+    @ApiOperation("获取技能组列表")
+    @GetMapping("/busiGroup/list")
+    public R getBusiGroupList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<EasyCallBusiGroupVO> list = easyCallService.getBusiGroupList(companyId);
+        return R.ok().put("data", list);
+    }
+
+    // =================== 任务管理 ===================
+
+    /**
+     * 分页查询外呼任务列表
+     * 支持按任务ID、任务名称、创建时间范围进行过滤,同时返回拨打统计
+     */
+    @ApiOperation("任务列表查询")
+    @PostMapping("/task/list")
+    public R getTaskList(@RequestBody EasyCallTaskQueryParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallPageResult<EasyCallTaskVO> result = easyCallService.getTaskList(param, companyId);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 分页查询通话记录
+     * 支持按号码、拨打时间、通话时长、通话状态等多条件过滤
+     * callType:01-呼入,02-AI外呼,03-人工外呼,三种类型不支持混合查询
+     */
+    @ApiOperation("通话记录查询")
+    @PostMapping("/record/list")
+    public R getRecordList(@RequestBody EasyCallRecordQueryParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallPageResult<EasyCallRecordVO> result = easyCallService.getRecordList(param, companyId);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 创建外呼任务
+     * 创建成功后返回包含 batchId 的任务对象,后续操作(启动/追加名单)均需要该 batchId
+     */
+    @ApiOperation("创建外呼任务")
+    @PostMapping("/task/create")
+    public R createTask(@RequestBody EasyCallCreateTaskParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        EasyCallTaskVO task = easyCallService.createTask(param, companyId);
+        return R.ok().put("data", task);
+    }
+
+    /**
+     * 启动外呼任务
+     * 任务创建后默认处于待机状态,调用此接口才会开始对名单中的号码拨打
+     * @param batchId 任务ID,来自创建任务或任务列表查询返回的 batchId
+     */
+    @ApiOperation("启动外呼任务")
+    @GetMapping("/task/start")
+    public R startTask(@RequestParam Long batchId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.startTask(batchId, companyId);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 停止外呼任务
+     * 停止后任务不再拨打,可重新调用启动接口继续运行
+     * @param batchId 任务ID
+     */
+    @ApiOperation("停止外呼任务")
+    @GetMapping("/task/stop")
+    public R stopTask(@RequestParam Long batchId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.stopTask(batchId, companyId);
+        return R.ok("操作成功");
+    }
+
+    // =================== 名单管理 ===================
+
+    /**
+     * AI 外呼专用追加名单
+     * 仅支持 AI 外呼任务,phoneList 为纯手机号字符串列表
+     * 任务启动后也可以继续追加,实现动态补充
+     */
+    @ApiOperation("AI外呼追加名单")
+    @PostMapping("/callList/addAi")
+    public R addAiCallList(@RequestBody EasyCallAddCallListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.addAiCallList(param, companyId);
+        return R.ok("操作成功");
+    }
+
+    /**
+     * 通用追加名单
+     * 同时支持 AI 外呼和通知提醒任务
+     * 通知提醒任务必须在每个号码条目中填写 noticeContent(具体提醒话术)
+     * bizJson 可传入需要传递给机器人的业务数据小弹口
+     */
+    @ApiOperation("通用追加名单")
+    @PostMapping("/callList/addCommon")
+    public R addCommonCallList(@RequestBody EasyCallCommonAddCallListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        easyCallService.addCommonCallList(param, companyId);
+        return R.ok("操作成功");
+    }
+
+    // =================== 录音相关 ===================
+
+    /**
+     * 将通话记录中的录音相对路径拼接为可直接访问的完整 URL
+     * 录音查询接口返回的 wavFileUrl 属于相对路径,需要拼接 baseUrl 才能下载
+     * @param wavFileUrl 通话记录中返回的 wavFileUrl 字段
+     */
+    @ApiOperation("获取录音文件完整访问url")
+    @GetMapping("/record/fileUrl")
+    public R getRecordFileUrl(@RequestParam String wavFileUrl) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        String fullUrl = easyCallService.getRecordFileUrl(wavFileUrl, companyId);
+        return R.ok().put("data", fullUrl);
+    }
+}

+ 2 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -111,6 +111,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/profile/**"
                 ).permitAll()
                 .antMatchers("/test").anonymous()
+                .antMatchers("/company/companyVoiceRobotic/callerResult").anonymous()
+                .antMatchers("/company/companyVoiceRobotic/callerResult4EasyCall").anonymous()
                 .antMatchers("**/callerResult").anonymous()
                 .antMatchers("/qw/getJsapiTicket/**").anonymous()
                 .antMatchers("/msg/**").anonymous()

+ 99 - 6
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -32,15 +32,18 @@ import com.fs.his.mapper.FsPackageOrderMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.vo.FsPackageOrderVO;
 import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
 import com.fs.qw.domain.QwAcquisitionSendMsgLog;
 import com.fs.qw.domain.QwCourseLinkSendMsgLog;
 import com.fs.qw.domain.QwSopSmsLogs;
 import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.mapper.QwAcquisitionLinkInfoMapper;
 import com.fs.qw.mapper.QwAcquisitionSendMsgLogMapper;
 import com.fs.qw.mapper.QwCourseLinkSendMsgLogMapper;
 import com.fs.qw.mapper.QwSopSmsLogsMapper;
 import com.fs.qw.service.IQwSopSmsLogsService;
 import com.fs.qw.strategy.SmsLogStrategyManager;
+import com.fs.qw.utils.UniqueStringUtil;
 import com.fs.sms.domain.SendSmsReturn;
 import com.fs.sms.service.impl.SmsTServiceImpl;
 import com.fs.sop.domain.QwSopLogs;
@@ -59,8 +62,13 @@ import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
 
 @Service
 @Slf4j
@@ -116,6 +124,16 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private CompanyVoiceRoboticCallBlacklistServiceImpl companyVoiceRoboticCallBlacklistService;
 
+    private static final String  LINK_DOMAIN = "https://c.ysyd.top/";
+
+    private static final String  LINK_SUFFIX = "?customer_channel=up:";
+
+    @Autowired
+    private QwAcquisitionLinkInfoMapper qwAcquisitionLinkInfoMapper;
+
+    //获客链接短信模板code
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
+
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -1097,8 +1115,9 @@ public class SmsServiceImpl implements ISmsService
         companyVoiceRoboticCallBlacklistCheckParam.setCompanyId(param.getCompanyId());
         companyVoiceRoboticCallBlacklistCheckParam.setBusinessType(BusinessTypeEnum.SMS.getCode());
         companyVoiceRoboticCallBlacklistCheckParam.setTargetValue(companyUser.getPhonenumber());
+        companyVoiceRoboticCallBlacklistCheckParam.setTargetType(1);
         CompanyVoiceRoboticCallBlacklistCheckVO companyVoiceRoboticCallBlacklistCheckVO = companyVoiceRoboticCallBlacklistService.checkBlacklist(companyVoiceRoboticCallBlacklistCheckParam);
-        if (companyVoiceRoboticCallBlacklistCheckVO.getPass()){
+        if (!companyVoiceRoboticCallBlacklistCheckVO.getPass()){
             throw new RuntimeException("黑名单校验未通过");
         }
         for(Long id:param.getCustomerIds()){
@@ -1117,6 +1136,16 @@ public class SmsServiceImpl implements ISmsService
             if(StringUtils.isNotEmpty(param.getSenderName())){
                 content=content.replace("${sms.senderName}",param.getSenderName());
             }
+            if (param.getTempCode()!=null &&SMS_LINK_TEMPLATE_CODE.equals(temp.getTempCode())){
+                String randomStr = generateUniqueRandomStr();
+                String replaceText=LINK_DOMAIN+randomStr;
+                content = content.replace("${sms.friendLink}",replaceText);
+                //添加获客链接记录
+                addAcquisitionLinkInfo(null,crmCustomer.getMobile(),param.getCardUrl(),randomStr, param.getCompanyUserId());
+            }
+            if (param.getTempCode()!=null &&"看课发送短信模板".equals(temp.getTempCode())){
+                content=content.replace("${sms.courseUrl}",param.getTempCode());
+            }
 
             String urls= null;
             // 通知类的不加 退订回T 只有营销类的加
@@ -1125,12 +1154,17 @@ public class SmsServiceImpl implements ISmsService
             FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
             if (sms.getType().equals("rf")){
                 try {
-                    if(temp.getTempType().equals(1)){
-                        urls = sms.getRfUrl1()+"sms?action=send&account="+sms.getRfAccount1()+"&password="+sms.getRfPassword1()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content, "UTF-8")+"&extno="+sms.getRfCode1()+"&rt=json";
-                    }
-                    else if(temp.getTempType().equals(2)){
-                        urls = sms.getRfUrl2()+"sms?action=send&account="+sms.getRfAccount2()+"&password="+sms.getRfPassword2()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content+"拒收请回复R", "UTF-8")+"&extno="+sms.getRfCode2()+"&rt=json";
+                    if (temp.getTempType().equals(2) && SMS_LINK_TEMPLATE_CODE.equals(temp.getTempCode())){
+                        urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + crmCustomer.getMobile() + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+                    } else {
+                        if(temp.getTempType().equals(1)){
+                            urls = sms.getRfUrl1()+"sms?action=send&account="+sms.getRfAccount1()+"&password="+sms.getRfPassword1()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content, "UTF-8")+"&extno="+sms.getRfCode1()+"&rt=json";
+                        }
+                        else if(temp.getTempType().equals(2)){
+                            urls = sms.getRfUrl2()+"sms?action=send&account="+sms.getRfAccount2()+"&password="+sms.getRfPassword2()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content+"拒收请回复R", "UTF-8")+"&extno="+sms.getRfCode2()+"&rt=json";
+                        }
                     }
+
                 } catch (UnsupportedEncodingException e) {
                     e.printStackTrace();
                 }
@@ -1215,4 +1249,63 @@ public class SmsServiceImpl implements ISmsService
             }
         }
     }
+
+    /**
+     * 添加链接生成记录
+     * */
+    public int addAcquisitionLinkInfo(Long qwAcquisitionAssistantId,String originalPhone,String originalLink,String randomStr,Long createBy){
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setCreateBy(createBy);
+        qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
+        qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
+        //加密手机号
+        String phonePlus = encryptPhone(originalPhone);
+        String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        // ========== 缓存URL,便于后续通过randomStr访问 ==========
+//        try {
+//            String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
+//            Integer cacheExpire = 2; // 默认缓存2天
+//            redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
+//            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
+//        } catch (Exception e) {
+//            // 缓存失败不影响主流程,但需要记录日志
+//            log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
+//        }
+        return addResult;
+    }
+    /**
+     * 生成唯一的页面参数
+     */
+    private String generateUniqueRandomStr() {
+        // 获取所有已存在的pageParam(只取需要的字段)
+        List<String> existingParams = qwAcquisitionLinkInfoMapper.selectAllRandomStr();
+        //使用Set,提高查找效率 O(1)
+        Set<String> paramSet = new HashSet<>(existingParams);
+
+        int maxAttempts = 10; // 设置最大尝试次数
+        int attempt = 0;
+
+        while (attempt < maxAttempts) {
+            // 生成7位随机码
+            String candidate = UniqueStringUtil.generateTimeBasedUnique(7);
+
+            // 使用Set的contains方法,O(1)复杂度
+            if (!paramSet.contains(candidate)) {
+                log.debug("生成页面参数成功: {}, 尝试次数: {}", candidate, attempt + 1);
+                return candidate;
+            }
+
+            attempt++;
+            log.debug("页面参数 {} 已存在,重新生成,第{}次尝试", candidate, attempt);
+        }
+
+        // 如果多次尝试都失败,使用+1随机数方案
+        String finalParam = UniqueStringUtil.generateTimeBasedUnique(8);
+        log.warn("多次尝试后使用7位参数: {}", finalParam);
+        return finalParam;
+    }
 }

+ 82 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLog.java

@@ -0,0 +1,82 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 调用日志对象 company_voice_robotic_call_log
+ *
+ * @author fs
+ * @date 2026-01-13
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyVoiceRoboticCallLog extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long logId;
+
+    /** 任务id */
+    @Excel(name = "任务id")
+    private Long roboticId;
+
+    /**
+     * caller_id  打电话&发短信
+     * */
+    @Excel(name = "caller_id")
+    private Long callerId;
+
+    /**
+     * wx_client_id 加微
+     */
+    @Excel(name = "wx_client_id")
+    private Long wxClientId;
+
+    /** 调用方法 */
+    @Excel(name = "调用方法")
+    private String runFunction;
+
+    /** 记录调用时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "记录调用时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date runTime;
+
+    /** 调用参数 */
+    @Excel(name = "调用参数")
+    private String runParam;
+
+    /** 执行结果 */
+    @Excel(name = "执行结果")
+    private String result;
+
+    /** 执行状态:1、执行中,2、执行成功,3、执行失败 */
+    @Excel(name = "执行状态:1、执行中,2、执行成功,3、执行失败")
+    private Integer status;
+
+
+    public static CompanyVoiceRoboticCallLog initCallLogByType(String type, String runParam, Long keyId, Long taskId) {
+        CompanyVoiceRoboticCallLog log = new CompanyVoiceRoboticCallLog();
+
+        switch (type) {
+            case Constants.SEND_MSG:
+            case Constants.CELL_PHONE:
+                log.callerId = keyId;
+                break;
+            case Constants.ADD_WX:
+                log.wxClientId = keyId;
+                break;
+        }
+        log.runParam = runParam;
+        log.roboticId = taskId;
+        log.runFunction = type;
+        log.runTime = new Date();
+        return log;
+    }
+
+}

+ 4 - 2
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallBlacklistMapper.java

@@ -78,13 +78,15 @@ public interface CompanyVoiceRoboticCallBlacklistMapper
      */
     CompanyVoiceRoboticCallBlacklist selectActiveByTarget(@Param("companyId") Long companyId,
                                                           @Param("businessType") String businessType,
-                                                          @Param("targetValue") String targetValue);
+                                                          @Param("targetValue") String targetValue,
+                                                          @Param("targetType")Integer targetType);
 
     /**
      * 查询全局黑名单
      */
     CompanyVoiceRoboticCallBlacklist selectGlobalActiveByTarget(@Param("businessType") String businessType,
-                                                                @Param("targetValue") String targetValue);
+                                                                @Param("targetValue") String targetValue,
+                                                                    @Param("targetType") Integer targetType);
 
 
     int updateStatusById(CompanyVoiceRoboticCallBlacklist entity);

+ 71 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogMapper.java

@@ -0,0 +1,71 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyVoiceRoboticCallLog;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 调用日志Mapper接口
+ * 
+ * @author fs
+ * @date 2026-01-13
+ */
+public interface CompanyVoiceRoboticCallLogMapper extends BaseMapper<CompanyVoiceRoboticCallLog>{
+    /**
+     * 查询调用日志
+     * 
+     * @param logId 调用日志主键
+     * @return 调用日志
+     */
+    CompanyVoiceRoboticCallLog selectCompanyVoiceRoboticCallLogByLogId(Long logId);
+
+    /**
+     * 查询调用日志列表
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 调用日志集合
+     */
+    List<CompanyVoiceRoboticCallLog> selectCompanyVoiceRoboticCallLogList(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    /**
+     * 新增调用日志
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 结果
+     */
+    int insertCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    /**
+     * 修改调用日志
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 结果
+     */
+    int updateCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    /**
+     * 删除调用日志
+     * 
+     * @param logId 调用日志主键
+     * @return 结果
+     */
+    int deleteCompanyVoiceRoboticCallLogByLogId(Long logId);
+
+    /**
+     * 批量删除调用日志
+     * 
+     * @param logIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanyVoiceRoboticCallLogByLogIds(Long[] logIds);
+
+    /**
+     * 查询待回调写入日志数据
+     * @param callees
+     * @return
+     */
+    CompanyVoiceRoboticCallLog selectNoResultLogByCallees(@Param("callees") CompanyVoiceRoboticCallees callees);
+}

+ 4 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java

@@ -72,4 +72,8 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
     List<DictVO> getDictDataList(@Param("dictType") String dictType);
 
     int finishAddWxRobotic(@Param("collect") List<Long> collect);
+
+    void finishRobotic(@Param("id") Long id);
+
+    List<CompanyVoiceRobotic> selectSceneTaskByCompanyIdAndType(@Param("companyId") Long companyId, @Param("sceneType") Integer sceneType);
 }

+ 74 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogService.java

@@ -0,0 +1,74 @@
+package com.fs.company.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aicall.domain.apiresult.PushIIntentionResult;
+import com.fs.company.domain.CompanyVoiceRoboticCallLog;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+
+import java.util.List;
+
+/**
+ * 调用日志Service接口
+ * 
+ * @author fs
+ * @date 2026-01-13
+ */
+public interface ICompanyVoiceRoboticCallLogService extends IService<CompanyVoiceRoboticCallLog>{
+    /**
+     * 查询调用日志
+     * 
+     * @param logId 调用日志主键
+     * @return 调用日志
+     */
+    CompanyVoiceRoboticCallLog selectCompanyVoiceRoboticCallLogByLogId(Long logId);
+
+    /**
+     * 查询调用日志列表
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 调用日志集合
+     */
+    List<CompanyVoiceRoboticCallLog> selectCompanyVoiceRoboticCallLogList(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    /**
+     * 新增调用日志
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 结果
+     */
+    int insertCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    /**
+     * 修改调用日志
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 结果
+     */
+    int updateCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    /**
+     * 批量删除调用日志
+     * 
+     * @param logIds 需要删除的调用日志主键集合
+     * @return 结果
+     */
+    int deleteCompanyVoiceRoboticCallLogByLogIds(Long[] logIds);
+
+    /**
+     * 删除调用日志信息
+     * 
+     * @param logId 调用日志主键
+     * @return 结果
+     */
+    int deleteCompanyVoiceRoboticCallLogByLogId(Long logId);
+
+    /**
+     * 异步写入日志
+     * @param companyVoiceRoboticCallLog
+     */
+    void asyncInsertCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog);
+
+    void asyncInsertCompanyVoiceRoboticCallLogBatch(List<CompanyVoiceRoboticCallLog> list);
+
+    void asyncHandleCalleeCallBackResult(PushIIntentionResult result, CompanyVoiceRoboticCallees callees);
+}

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java

@@ -3,6 +3,7 @@ package com.fs.company.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.AddWxResultVo;
+import com.fs.company.vo.CompanyWxClient4WorkFlowVO;
 
 import java.util.List;
 
@@ -69,4 +70,12 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient>
 
     void addWxTrueResult(AddWxResultVo vo);
 
+    List<CompanyWxClient> getAddWxList(List<Long> accountIdList,Integer isWeCom);
+
+    List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList, Integer cidGroupId);
+
+    List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList,Integer isWeCom);
+
+    List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(List<Long> accountIdList);
+
 }

+ 2 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyWxDialogService.java

@@ -1,5 +1,6 @@
 package com.fs.company.service;
 
+import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyWxDialog;
 
 import java.util.List;
@@ -10,7 +11,7 @@ import java.util.List;
  * @author fs
  * @date 2024-12-06
  */
-public interface ICompanyWxDialogService 
+public interface ICompanyWxDialogService extends IService<CompanyWxDialog>
 {
     /**
      * 查询添加微信话术

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallBlacklistServiceImpl.java

@@ -137,7 +137,7 @@ public class CompanyVoiceRoboticCallBlacklistServiceImpl implements ICompanyVoic
                     companyVoiceRoboticCallBlacklistMapper.selectActiveByTarget(
                             param.getCompanyId(),
                             param.getBusinessType(),
-                            targetValue
+                            targetValue,param.getTargetType()
                     );
 
             if (companyHit != null) {
@@ -169,7 +169,7 @@ public class CompanyVoiceRoboticCallBlacklistServiceImpl implements ICompanyVoic
 //                buildReason(hit)
 //        );
         // 2. 查全局黑名单
-        CompanyVoiceRoboticCallBlacklist globalHit = companyVoiceRoboticCallBlacklistMapper.selectGlobalActiveByTarget(param.getBusinessType(),targetValue);
+        CompanyVoiceRoboticCallBlacklist globalHit = companyVoiceRoboticCallBlacklistMapper.selectGlobalActiveByTarget(param.getBusinessType(),targetValue,param.getTargetType());
 
         if (globalHit != null) {
             return CompanyVoiceRoboticCallBlacklistCheckVO.reject(

+ 136 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogServiceImpl.java

@@ -0,0 +1,136 @@
+package com.fs.company.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aicall.domain.apiresult.PushIIntentionResult;
+import com.fs.company.domain.CompanyVoiceRoboticCallLog;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogMapper;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 调用日志Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-01-13
+ */
+@Service
+@Slf4j
+public class CompanyVoiceRoboticCallLogServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogMapper, CompanyVoiceRoboticCallLog> implements ICompanyVoiceRoboticCallLogService {
+
+
+    @Autowired
+    CompanyVoiceRoboticCallLogMapper companyVoiceRoboticCallLogMapper;
+
+    /**
+     * 查询调用日志
+     * 
+     * @param logId 调用日志主键
+     * @return 调用日志
+     */
+    @Override
+    public CompanyVoiceRoboticCallLog selectCompanyVoiceRoboticCallLogByLogId(Long logId)
+    {
+        return baseMapper.selectCompanyVoiceRoboticCallLogByLogId(logId);
+    }
+
+    /**
+     * 查询调用日志列表
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 调用日志
+     */
+    @Override
+    public List<CompanyVoiceRoboticCallLog> selectCompanyVoiceRoboticCallLogList(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog)
+    {
+        return baseMapper.selectCompanyVoiceRoboticCallLogList(companyVoiceRoboticCallLog);
+    }
+
+    /**
+     * 新增调用日志
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog)
+    {
+        return baseMapper.insertCompanyVoiceRoboticCallLog(companyVoiceRoboticCallLog);
+    }
+
+    /**
+     * 修改调用日志
+     * 
+     * @param companyVoiceRoboticCallLog 调用日志
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog)
+    {
+        return baseMapper.updateCompanyVoiceRoboticCallLog(companyVoiceRoboticCallLog);
+    }
+
+    /**
+     * 批量删除调用日志
+     * 
+     * @param logIds 需要删除的调用日志主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyVoiceRoboticCallLogByLogIds(Long[] logIds)
+    {
+        return baseMapper.deleteCompanyVoiceRoboticCallLogByLogIds(logIds);
+    }
+
+    /**
+     * 删除调用日志信息
+     * 
+     * @param logId 调用日志主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyVoiceRoboticCallLogByLogId(Long logId)
+    {
+        return baseMapper.deleteCompanyVoiceRoboticCallLogByLogId(logId);
+    }
+
+    @Async("callLogExcutor")
+    public void asyncInsertCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog) {
+        try{
+            companyVoiceRoboticCallLog.setCreateTime(new Date());
+            baseMapper.insertCompanyVoiceRoboticCallLog(companyVoiceRoboticCallLog);
+        } catch (Exception e) {
+            log.error("记录任务执行日志失败:失败数据:{}",companyVoiceRoboticCallLog, e);
+        }
+    }
+    @Async("callLogExcutor")
+    public void asyncInsertCompanyVoiceRoboticCallLogBatch(List<CompanyVoiceRoboticCallLog> list) {
+        try{
+            list.stream().forEach(i->i.setCreateTime(new Date()));
+            this.saveBatch(list);
+        } catch (Exception e) {
+            log.error("批量记录任务执行日志失败:失败数据:{}",list, e);
+        }
+    }
+
+
+    @Async("callLogExcutor")
+    public void asyncHandleCalleeCallBackResult(PushIIntentionResult result, CompanyVoiceRoboticCallees callees) {
+        try {
+            CompanyVoiceRoboticCallLog companyVoiceRoboticCallLog = companyVoiceRoboticCallLogMapper.selectNoResultLogByCallees(callees);
+            companyVoiceRoboticCallLog.setStatus(2);
+            companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
+            baseMapper.updateCompanyVoiceRoboticCallLog(companyVoiceRoboticCallLog);
+        } catch (Exception ex) {
+            log.error("处理回调结果异常:{}", result, ex);
+        }
+    }
+
+}

+ 45 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -35,9 +35,13 @@ import com.fs.enums.TaskTypeEnum;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.impl.QwExternalContactServiceImpl;
+import com.fs.qwApi.domain.QwLinkCreateResult;
+import com.fs.qwApi.param.QwLinkCreateParam;
+import com.fs.qwApi.service.QwApiService;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.impl.SysDictTypeServiceImpl;
+import com.fs.voice.utils.StringUtil;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.RequiredArgsConstructor;
@@ -120,6 +124,8 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     /** 每次重试等待时长(毫秒) */
     private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
 
+    private final QwApiService qwApiService;
+
     /**
      * 查询机器人外呼任务
      *
@@ -457,6 +463,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             CompanyWxClient wxClient = companyWxClientMapper.selectOneByRoboticIdAndUserId(roboticId, callees.getUserId());
             CompanyVoiceRoboticWx wx = companyVoiceRoboticWxService.getById(wxClient.getRoboticWxId());
 
+
             CompanyWxAccount wxAccount = new CompanyWxAccount();
             if (wxClient.getIsWeCom() == 2) {
                 QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedisForId(String.valueOf(wxClient.getAccountId()));
@@ -473,6 +480,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 CompanySms sms = companySmsService.selectCompanySmsByCompanyId(wxAccount.getCompanyId());
                 if (sms != null) {
                     if (sms.getRemainSmsCount() > 0) {
+
                         SmsSendBatchParam smsSendBatchParam = new SmsSendBatchParam();
                         smsSendBatchParam.setCompanyId(wxAccount.getCompanyId());
                         smsSendBatchParam.setCompanyUserId(wxAccount.getCompanyUserId());
@@ -492,6 +500,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                                 wxAccount.getCompanyUserId(),
                                 temp.getTempId()
                         );
+                        if ("获客链接短信模板".equals(temp.getTempCode())) {
+                            QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedisForId(String.valueOf(wxClient.getAccountId()));
+                            String linkUrl = getLinkUrl(qwUserByRedis);
+                            smsSendBatchParam.setCardUrl(linkUrl);
+                        }
                         addLog.setStatus(1);
                         try {
                             int smsContentLen = getSmsContentLen(smsSendBatchParam);
@@ -1650,4 +1663,36 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return vo;
         }).collect(Collectors.toList());
     }
+
+    /**
+     * 获取获客链接
+     */
+    private String getLinkUrl(QwUser qwUser){
+
+        String link = redisCache2.getCacheObject("customerLink:"+qwUser.getId());
+        if (link!=null && !StringUtil.strIsNullOrEmpty(link)){
+            return link;
+        }
+
+        //获取获客链接
+        QwLinkCreateParam createParam=new QwLinkCreateParam();
+        createParam.setLink_name(qwUser.getQwUserName()+"的获客链接");
+
+        QwLinkCreateParam.Range range=new QwLinkCreateParam.Range();
+        range.setUser_list(Collections.singletonList(qwUser.getQwUserId()));
+        createParam.setRange(range);
+
+        QwLinkCreateResult result = qwApiService.linkCreate(createParam, qwUser.getCorpId());
+
+        if (result.getErrcode()==0){
+
+            redisCache2.setCacheObject("customerLink:"+qwUser.getId(),result.getUrl());
+
+            return  result.getUrl();
+        }else {
+            return null;
+        }
+
+
+    }
 }

+ 33 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java

@@ -12,8 +12,11 @@ import com.fs.company.mapper.CompanyVoiceRoboticWxMapper;
 import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.service.ICompanyWxClientService;
 import com.fs.company.vo.AddWxResultVo;
+import com.fs.company.vo.CompanyWxClient4WorkFlowVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
+import com.fs.enums.ExecutionStatusEnum;
+import com.fs.enums.NodeTypeEnum;
 import com.fs.wxUser.domain.CompanyWxUser;
 import com.fs.wxUser.mapper.CompanyWxUserMapper;
 import lombok.AllArgsConstructor;
@@ -223,4 +226,34 @@ public class CompanyWxClientServiceImpl extends ServiceImpl<CompanyWxClientMappe
             companyWxUserMapper.updateCompanyWxUser(companyWx);
         }
     }
+
+    @Override
+    public List<CompanyWxClient> getAddWxList(List<Long> accountIdList,Integer isWeCom) {
+        return baseMapper.getAddWxList(accountIdList,isWeCom);
+    }
+
+    /**
+     * 获取添加微信列表 工作流用
+     * @param accountIdList
+     * @return
+     */
+    @Override
+    public  List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList, Integer cidGroupId){
+        return baseMapper.getAddWxList4Workflow(accountIdList, ExecutionStatusEnum.PAUSED.getValue(), NodeTypeEnum.AI_ADD_WX_TASK.getValue(),cidGroupId);
+    }
+
+    @Override
+    public List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList, Integer isWeCom) {
+        return baseMapper.getQwAddWxList(accountIdList,isWeCom);
+    }
+
+    /**
+     * 获取添加微信列表 工作流用
+     * @param accountIdList
+     * @return
+     */
+    @Override
+    public  List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(List<Long> accountIdList){
+        return baseMapper.getQwAddWxList4Workflow(accountIdList, ExecutionStatusEnum.WAITING.getValue(), NodeTypeEnum.AI_QW_ADD_WX_TASK.getValue());
+    }
 }

+ 1 - 0
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -286,6 +286,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         companyVoiceRoboticCallBlacklistCheckParam.setCompanyId(crmCustomer.getCompanyId());
         companyVoiceRoboticCallBlacklistCheckParam.setBusinessType(BusinessTypeEnum.SMS.getCode());
         companyVoiceRoboticCallBlacklistCheckParam.setTargetValue(callees.getPhone());
+        companyVoiceRoboticCallBlacklistCheckParam.setTargetType(1);
         CompanyVoiceRoboticCallBlacklistCheckVO companyVoiceRoboticCallBlacklistCheckVO = companyVoiceRoboticCallBlacklistService.checkBlacklist(companyVoiceRoboticCallBlacklistCheckParam);
         if (!companyVoiceRoboticCallBlacklistCheckVO.getPass()){
             log.error("workflowCallPhoneOne4EasyCall: 被叫人黑名单校验未通过 - userId: {}", callees.getUserId());

+ 82 - 0
fs-service/src/main/java/com/fs/company/util/ObjectPlaceholderResolver.java

@@ -0,0 +1,82 @@
+package com.fs.company.util;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 基于Java对象的文本占位符替换工具类
+ * 支持替换 ${属性名} 或 ${属性名:默认值} 格式的占位符
+ */
+@Component
+public class ObjectPlaceholderResolver {
+
+    // 正则匹配 ${xxx} 或 ${xxx:默认值} 格式的占位符
+    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}:]+)(?::([^}]*))?\\}");
+
+    /**
+     * 替换文本中的占位符为指定对象的属性值
+     * @param targetObject 提供属性值的目标对象(不能为null)
+     * @param text 包含占位符的原始文本
+     * @return 替换后的文本
+     */
+    public String resolvePlaceholders(Object targetObject, String text) {
+        // 容错:对象为空或文本为空,直接返回原文本
+        if (targetObject == null || text == null || text.isEmpty()) {
+            return text;
+        }
+
+        Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
+        StringBuffer result = new StringBuffer();
+
+        while (matcher.find()) {
+            // 提取占位符中的属性名(如 ${name} → name)
+            String propName = matcher.group(1);
+            // 提取默认值(如 ${hobby:打篮球} → 打篮球)
+            String defaultValue = matcher.group(2);
+
+            // 核心:通过反射获取对象的属性值
+            Object propValue = getPropertyValue(targetObject, propName);
+
+            // 确定最终替换值:有属性值→用属性值 → 无则用默认值 → 都无则保留原占位符
+            String replaceValue = null;
+            if (propValue != null) {
+                replaceValue = propValue.toString();
+            } else if (defaultValue != null) {
+                replaceValue = defaultValue;
+            } else {
+                replaceValue = matcher.group(0); // 保留原占位符
+            }
+
+            // 替换(处理特殊字符,避免替换失败)
+            matcher.appendReplacement(result, Matcher.quoteReplacement(replaceValue));
+        }
+        matcher.appendTail(result);
+
+        return result.toString();
+    }
+
+    /**
+     * 反射获取对象的属性值(通过Getter方法)
+     * 规则:属性名name → Getter方法getName()
+     * @param obj 目标对象
+     * @param propName 属性名
+     * @return 属性值(不存在则返回null)
+     */
+    private Object getPropertyValue(Object obj, String propName) {
+        try {
+            // 拼接Getter方法名:首字母大写,比如 name → getName
+            String methodName = "get" + propName.substring(0, 1).toUpperCase() + propName.substring(1);
+            // 获取Getter方法
+            Method getterMethod = obj.getClass().getMethod(methodName);
+            // 执行方法获取属性值(设置可访问,兼容私有方法)
+            getterMethod.setAccessible(true);
+            return getterMethod.invoke(obj);
+        } catch (Exception e) {
+            // 捕获所有反射异常(方法不存在、执行失败等),返回null
+            return null;
+        }
+    }
+}

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

@@ -95,4 +95,5 @@ public class CompanyWxClient4WorkFlowVO extends BaseEntityTow {
     * 投流 id
     */
     private String traceId;
+    private String nodeKey;
 }

+ 17 - 0
fs-service/src/main/java/com/fs/company/vo/SendMsgVo.java

@@ -0,0 +1,17 @@
+package com.fs.company.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SendMsgVo {
+
+    private Integer txt;
+    private Integer img;
+
+}

+ 120 - 0
fs-service/src/main/java/com/fs/course/config/RedisKeyScanner.java

@@ -0,0 +1,120 @@
+package com.fs.course.config;
+
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@Component
+public class RedisKeyScanner {
+
+    private final StringRedisTemplate redisTemplate;
+
+    public RedisKeyScanner(StringRedisTemplate redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    public Set<String> scan(String pattern) {
+        Set<String> keys = new HashSet<>();
+
+        // 这里指定 match + count
+        ScanOptions options = ScanOptions.scanOptions()
+                .match(pattern)
+                .count(1000)
+                .build();
+
+        // 使用 RedisConnection 提供的 scan
+        redisTemplate.execute((RedisConnection connection) -> {
+            try (Cursor<byte[]> cursor = connection.scan(options)) {
+                RedisSerializer<String> keySerializer = redisTemplate.getStringSerializer();
+                cursor.forEachRemaining(item -> keys.add(keySerializer.deserialize(item)));
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            return null;
+        });
+
+        return keys;
+    }
+
+    public Set<String> scanKeys(String pattern) {
+        Set<String> keys = new HashSet<>();
+        ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();
+
+        redisTemplate.execute((RedisConnection connection) -> {
+            try (Cursor<byte[]> cursor = connection.scan(options)) {
+                while (cursor.hasNext()) {
+                    byte[] rawKey = cursor.next();
+                    String key = (String) redisTemplate.getKeySerializer().deserialize(rawKey);
+                    keys.add(key);
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("Error during scan", e);
+            }
+            return null;
+        });
+
+        return keys;
+    }
+
+    /**
+     * 使用 SCAN 替代 KEYS,避免阻塞 Redis
+     *
+     * @param pattern key 匹配模式,例如 "h5user:watch:duration:*"
+     * @return 匹配到的 key 集合
+     */
+    public Collection<String> scanPatternKeys(final String pattern) {
+        Set<String> keys = new HashSet<>();
+
+        // 每次扫描的数量,可根据实际情况调整(越大速度越快,但对 CPU 影响越大)
+        ScanOptions options = ScanOptions.scanOptions()
+                .match(pattern)
+                .count(1000)
+                .build();
+
+        // 使用 redisTemplate 的 execute 保证连接获取 & 释放正确
+        redisTemplate.execute((RedisConnection connection) -> {
+            try (Cursor<byte[]> cursor = connection.scan(options)) {
+                RedisSerializer<String> keySerializer = redisTemplate.getStringSerializer();
+                while (cursor.hasNext()) {
+                    keys.add(keySerializer.deserialize(cursor.next()));
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("Error during redis SCAN", e);
+            }
+            return null;
+        });
+
+        return keys;
+    }
+
+    public Set<String> scanMatchKey(String pattern) {
+        return (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
+            Set<String> keys = new HashSet<>();
+            // 使用 try-with-resources 确保 Cursor 无论是否异常都会被关闭
+            try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
+                    .match(pattern)
+                    .count(1000) // 每次扫描的批量大小,可根据实际情况调整
+                    .build())) {
+                while (cursor.hasNext()) {
+                    keys.add(new String(cursor.next(), StandardCharsets.UTF_8)); // 显式指定字符集,避免依赖默认值
+                }
+            } catch (IOException e) {
+                // 在实际项目中,应该使用更合适的日志记录和异常处理方式
+                // 例如抛出自定义异常或记录错误日志
+                throw new RuntimeException("Error during SCAN operation", e);
+            }
+            return keys;
+        });
+    }
+
+}

+ 73 - 0
fs-service/src/main/java/com/fs/course/config/WxConfig.java

@@ -0,0 +1,73 @@
+package com.fs.course.config;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class WxConfig implements Serializable {
+    /**
+     *新账号判断时间
+     */
+    private Integer newAccountTime;
+    /**
+     * 新账号每天添加数量
+     */
+    private Integer newAccountAddNum;
+    /**
+     * 普通账号每天添加数量
+     */
+    private Integer accountAddMin;
+    /**
+     * 普通账号每天添加数量
+     */
+    private Integer accountAddMax;
+    /**
+     * 文字消息(条/分)
+     */
+    private Integer txtMsgMinNum;
+    /**
+     * 文字消息(条/分)
+     */
+    private Integer txtMsgMaxNum;
+    /**
+     * 图片消息(条/分)
+     */
+    private Integer imgMsgMinNum;
+    /**
+     * 图片消息(条/分)
+     */
+    private Integer imgMsgMaxNum;
+    /**
+     * 新号添加好友频率(条/分)
+     */
+    private Integer newAccountAddWxMax;
+    /**
+     * 新号添加好友频率(条/分)
+     */
+    private Integer newAccountAddWxMin;
+    /**
+     * 新号消息发送频率(条/秒)
+     */
+    private Integer newAccountSendMsgMax;
+    /**
+     * 新号消息发送频率(条/秒)
+     */
+    private Integer newAccountSendMsgMin;
+    /**
+     * 添加好友频率(条/分)
+     */
+    private Integer accountAddWxMax;
+    /**
+     * 添加好友频率(条/分)
+     */
+    private Integer accountAddWxMin;
+    /**
+     * 消息发送频率(条/秒)
+     */
+    private Integer accountSendMsgMax;
+    /**
+     * 消息发送频率(条/秒)
+     */
+    private Integer accountSendMsgMin;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/qw/domain/QwUser.java

@@ -113,4 +113,6 @@ public class QwUser extends BaseEntity
 
     @TableField(exist = false)
     private Integer disableCompanyId;
+
+    private String friendUrl;
 }

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

@@ -562,4 +562,15 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
 
 
     int batchUpdateFsUserPhoneById(@Param("list") List<QwExternalContact> list);
+
+    @Select("SELECT " +
+            "id " +
+            "FROM " +
+            "qw_external_contact " +
+            "WHERE " +
+            "qw_user_id = #{qwUserId} " +
+            "and add_way = #{addWay} " +
+            "and remark_mobiles LIKE concat('%',#{phone},'%') " +
+            "limit 1")
+    QwExternalContact queryQwUserIdIsAddContact(@Param("qwUserId") Long qwUserId, @Param("phone") String phone, @Param("addWay") int addWay);
 }

+ 38 - 0
fs-service/src/main/java/com/fs/wxcid/dto/callback/WxCallbackVo.java

@@ -0,0 +1,38 @@
+package com.fs.wxcid.dto.callback;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fs.wxcid.dto.common.IntegerWrapper;
+import com.fs.wxcid.dto.common.StringWrapper;
+import lombok.Data;
+
+@Data
+public class WxCallbackVo {
+    private String key;
+    private Message message;
+    private String type;
+    @Data
+    public static class Message{
+        @JsonProperty("msg_id")
+        private Long msgId;
+        @JsonProperty("from_user_name")
+        private StringWrapper fromUserName;
+        @JsonProperty("to_user_name")
+        private StringWrapper toUserName;
+        @JsonProperty("msg_type")
+        private Integer msgType;
+        private StringWrapper content;
+        private Integer status;
+        @JsonProperty("img_status")
+        private Integer imgStatus;
+        @JsonProperty("img_buf")
+        private IntegerWrapper imgBuf;
+        @JsonProperty("create_time")
+        private Integer createTime;
+        @JsonProperty("msg_source")
+        private String msgSource;
+        @JsonProperty("push_content")
+        private String pushContent;
+        @JsonProperty("new_msg_id")
+        private Long newMsgId;
+    }
+}

+ 16 - 0
fs-service/src/main/java/com/fs/wxcid/dto/friend/AddContactParam.java

@@ -0,0 +1,16 @@
+package com.fs.wxcid.dto.friend;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 添加联系人业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AddContactParam extends BaseAccountRequest {
+    private String mobile;
+    private String txt;
+    private Long clientId;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/wxcid/dto/friend/SearchContactParam.java

@@ -0,0 +1,14 @@
+package com.fs.wxcid.dto.friend;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 搜索联系人业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SearchContactParam extends BaseAccountRequest {
+    private String phone;
+}

+ 86 - 0
fs-service/src/main/java/com/fs/wxcid/dto/friend/SearchContactResponse.java

@@ -0,0 +1,86 @@
+package com.fs.wxcid.dto.friend;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+@Data
+public class SearchContactResponse {
+
+    @JSONField(name = "country")
+    private String country;
+
+    @JSONField(name = "small_head_img_url")
+    private String smallHeadImgUrl;
+
+    @JSONField(name = "big_head_img_url")
+    private String bigHeadImgUrl;
+
+    @JSONField(name = "signature")
+    private String signature;
+
+    @JSONField(name = "sex")
+    private Integer sex;
+
+    @JSONField(name = "verify_flag")
+    private Integer verifyFlag;
+
+    @JSONField(name = "personal_card")
+    private Integer personalCard;
+
+    @JSONField(name = "antispam_ticket") // 👈 新增字段
+    private String antispamTicket;
+
+    @JSONField(name = "nick_name")
+    private NickNameWrapper nickName;
+
+    @JSONField(name = "user_name")
+    private UserNameWrapper userName;
+
+    @JSONField(name = "quan_pin")
+    private QuanPinWrapper quanPin;
+
+    @JSONField(name = "pyinitial")
+    private PyInitialWrapper pyinitial;
+
+    // --- Wrappers ---
+    @Data
+    public static class NickNameWrapper {
+        @JSONField(name = "str")
+        private String str;
+    }
+
+    @Data
+    public static class UserNameWrapper {
+        @JSONField(name = "str")
+        private String str;
+    }
+
+    @Data
+    public static class QuanPinWrapper {
+        @JSONField(name = "str")
+        private String str;
+    }
+
+    @Data
+    public static class PyInitialWrapper {
+        @JSONField(name = "str")
+        private String str;
+    }
+
+    // --- Helper Getters ---
+    public String getNickNameStr() {
+        return nickName != null ? nickName.getStr() : null;
+    }
+
+    public String getUserNameStr() {
+        return userName != null ? userName.getStr() : null;
+    }
+
+    public String getQuanPinStr() {
+        return quanPin != null ? quanPin.getStr() : null;
+    }
+
+    public String getPyInitialStr() {
+        return pyinitial != null ? pyinitial.getStr() : null;
+    }
+}

+ 19 - 0
fs-service/src/main/java/com/fs/wxcid/dto/friend/VerifyUserParam.java

@@ -0,0 +1,19 @@
+package com.fs.wxcid.dto.friend;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 发起好友验证业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class VerifyUserParam extends BaseAccountRequest {
+    private String v3;
+    private String v4;
+    private Integer scene;
+    private Integer opCode;
+    private String chatRoomUserName;
+    private String content;
+}

+ 74 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/CdnUploadVideoResult.java

@@ -0,0 +1,74 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * /message/CdnUploadVideo 接口返回的 Data 结构
+ */
+@Data
+public class CdnUploadVideoResult {
+
+    @JSONField(name = "FileKey")
+    private String fileKey;
+
+    @JSONField(name = "Ver")
+    private Integer ver;
+
+    @JSONField(name = "ThumbDataSize")
+    private Integer thumbDataSize;
+
+    @JSONField(name = "ThumbURL")
+    private String thumbURL;
+
+    @JSONField(name = "FileAesKey")
+    private String fileAesKey;
+
+    @JSONField(name = "Mp4identify")
+    private String mp4identify;
+
+    @JSONField(name = "EnableQuic")
+    private Integer enableQuic;
+
+    @JSONField(name = "IsRetry")
+    private Integer isRetry;
+
+    @JSONField(name = "IsOverLoad")
+    private Integer isOverLoad;
+
+    @JSONField(name = "RecvLen")
+    private Integer recvLen;
+
+    @JSONField(name = "IsGetCDN")
+    private Integer isGetCDN;
+
+    @JSONField(name = "RetrySec")
+    private Integer retrySec;
+
+    @JSONField(name = "XClientIP")
+    private String xClientIP;
+
+    @JSONField(name = "FileURL")
+    private String fileURL;
+
+    @JSONField(name = "VideoDataMD5")
+    private String videoDataMD5;
+
+    @JSONField(name = "RetCode")
+    private Integer retCode;
+
+    @JSONField(name = "FileID")
+    private String fileID;
+
+    @JSONField(name = "ThumbHeight")
+    private Integer thumbHeight;
+
+    @JSONField(name = "ThumbWidth")
+    private Integer thumbWidth;
+
+    @JSONField(name = "Seq")
+    private Integer seq;
+
+    @JSONField(name = "VideoDataSize")
+    private Integer videoDataSize;
+}

+ 59 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/ChatSendRet.java

@@ -0,0 +1,59 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 单条消息的发送结果详情(来自微信服务器)
+ */
+@Data
+public class ChatSendRet {
+
+    /**
+     * 发送结果码:0 表示成功
+     */
+    @JSONField(name = "ret")
+    private long ret;
+
+    /**
+     * 消息创建时间(Unix 时间戳,秒)
+     */
+    @JSONField(name = "createTime")
+    private long createTime;
+
+    /**
+     * 客户端生成的消息 ID(用于去重或追踪)
+     */
+    @JSONField(name = "clientMsgId")
+    private long clientMsgId;
+
+    /**
+     * 微信服务器分配的新消息 ID(64位整数)
+     */
+    @JSONField(name = "newMsgId")
+    private long newMsgId;
+
+    /**
+     * 接收者用户名(包装结构,实际值在 str 字段中)
+     */
+    @JSONField(name = "toUserName")
+    private UserNameWrapper toUserName;
+
+    /**
+     * 消息 ID(旧版字段,通常为 0)
+     */
+    @JSONField(name = "msgId")
+    private long msgId;
+
+    /**
+     * 服务器处理时间(Unix 时间戳,秒)
+     */
+    @JSONField(name = "serverTime")
+    private long serverTime;
+
+    /**
+     * 消息类型:1 表示文本消息
+     */
+    @JSONField(name = "type")
+    private int type;
+}

+ 22 - 15
fs-service/src/main/java/com/fs/wxcid/dto/message/RevokeMsgRequest.java

@@ -1,25 +1,32 @@
 package com.fs.wxcid.dto.message;
 
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fs.wxcid.dto.common.BaseAccountRequest;
 import lombok.Data;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.EqualsAndHashCode;
 
+/**
+ * 撤回消息请求参数(RevokeMsgNew 接口)
+ */
 @Data
-public class RevokeMsgRequest {
-    @JsonProperty("ClientImgIdStr")
-    private String ClientImgIdStr;
+@EqualsAndHashCode(callSuper = true)
+public class RevokeMsgRequest extends BaseAccountRequest {
 
-    @JsonProperty("ClientMsgId")
-    private Long ClientMsgId;
+    @JSONField(name = "ClientImgIdStr")
+    private String clientImgIdStr;
 
-    @JsonProperty("CreateTime")
-    private Long CreateTime;
+    @JSONField(name = "ClientMsgId")
+    private Long clientMsgId;
 
-    @JsonProperty("IsImage")
-    private Boolean IsImage;
+    @JSONField(name = "CreateTime")
+    private Long createTime;
 
-    @JsonProperty("NewMsgId")
-    private String NewMsgId;
+    @JSONField(name = "IsImage")
+    private boolean isImage;
 
-    @JsonProperty("ToUserName")
-    private String ToUserName;
-}
+    @JSONField(name = "NewMsgId")
+    private String newMsgId;
+
+    @JSONField(name = "ToUserName")
+    private String toUserName;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/RevokeMsgResult.java

@@ -0,0 +1,25 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fs.wxcid.dto.common.BaseResponse;
+import lombok.Data;
+
+/**
+ * 撤回消息接口(RevokeMsgNew)返回的 Data 结构
+ */
+@Data
+public class RevokeMsgResult {
+
+    @JSONField(name = "baseResponse")
+    private BaseResponse baseResponse;
+
+    @JSONField(name = "sysWording")
+    private String sysWording;
+
+    /**
+     * 是否撤回成功(根据 baseResponse.ret == 0 判断)
+     */
+    public boolean isSuccess() {
+        return baseResponse != null && baseResponse.getRet() == 0;
+    }
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendImageMessageParam.java

@@ -0,0 +1,15 @@
+package com.fs.wxcid.dto.message;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 发送图片消息业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SendImageMessageParam extends BaseAccountRequest {
+    private String imgUrl;
+    private String toUser;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendImageMessageResult.java

@@ -0,0 +1,40 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 发送图片消息的单条结果(SendImageNewMessage 接口 Data 数组中的每个元素)
+ */
+@Data
+public class SendImageMessageResult {
+
+    /**
+     * 图片 ID
+     */
+    @JSONField(name = "imageId")
+    private String imageId;
+
+    /**
+     * 微信底层返回的详细响应信息
+     */
+    @JSONField(name = "resp")
+    private SendImageResponse resp;
+
+    /**
+     * 接收方的微信 ID(如 wxid_xxx)
+     * 注意:接口返回字段名为 toUSerName(拼写错误)
+     */
+    @JSONField(name = "toUSerName")
+    private String toUserName;
+
+    /**
+     * 是否发送成功(根据 resp.baseResponse.ret == 0 判断)
+     */
+    public boolean isSendSuccess() {
+        if (resp == null || resp.getBaseResponse() == null) {
+            return false;
+        }
+        return resp.getBaseResponse().getRet() == 0;
+    }
+}

+ 46 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendImageResponse.java

@@ -0,0 +1,46 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fs.wxcid.dto.common.BaseResponse;
+import com.fs.wxcid.dto.common.StringWrapper;
+import lombok.Data;
+
+/**
+ * 发送图片消息时 resp 字段的响应结构(SendImageNewMessage 接口返回)
+ */
+@Data
+public class SendImageResponse {
+
+    @JSONField(name = "baseResponse")
+    private BaseResponse baseResponse;
+
+    @JSONField(name = "msgId")
+    private Long msgId;
+
+    @JSONField(name = "clientImgId")
+    private StringWrapper clientImgId;
+
+    @JSONField(name = "fromUserName")
+    private StringWrapper fromUserName;
+
+    @JSONField(name = "toUserName")
+    private StringWrapper toUserName;
+
+    @JSONField(name = "totalLen")
+    private Long totalLen;
+
+    @JSONField(name = "startPos")
+    private Long startPos;
+
+    @JSONField(name = "dataLen")
+    private Long dataLen;
+
+    @JSONField(name = "createTime")
+    private Long createTime;
+
+    @JSONField(name = "newMsgId")
+    private Long newMsgId;
+
+    @JSONField(name = "msgSource")
+    private String msgSource;
+}

+ 35 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendMessageResult.java

@@ -0,0 +1,35 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 发送消息的单条结果(data 数组中的每个元素)
+ */
+@Data
+public class SendMessageResult {
+
+    /**
+     * 接收方的微信 ID(如 wxid_xxx)
+     */
+    @JSONField(name = "toUSerName")
+    private String toUserName;
+
+    /**
+     * 消息是否发送成功
+     */
+    @JSONField(name = "sendSuccess")
+    private boolean sendSuccess;
+
+    /**
+     * 微信底层返回的详细响应信息
+     */
+    @JSONField(name = "resp")
+    private SendResponse resp;
+
+    /**
+     * 实际发送的文本内容
+     */
+    @JSONField(name = "textContent")
+    private String textContent;
+}

+ 32 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendResponse.java

@@ -0,0 +1,32 @@
+package com.fs.wxcid.dto.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fs.wxcid.dto.common.BaseResponse;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 微信服务端返回的原始消息发送响应体
+ */
+@Data
+public class SendResponse {
+
+    /**
+     * 基础响应信息(包含 ret 错误码)
+     */
+    @JSONField(name = "base_response")
+    private BaseResponse baseResponse;
+
+    /**
+     * 成功发送的消息数量
+     */
+    @JSONField(name = "count")
+    private int count;
+
+    /**
+     * 每条消息的具体发送结果列表
+     */
+    @JSONField(name = "chat_send_ret_list")
+    private List<ChatSendRet> chatSendRetList;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendTextMessageParam.java

@@ -0,0 +1,14 @@
+package com.fs.wxcid.dto.message;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 发送文本消息业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SendTextMessageParam extends BaseAccountRequest {
+    private String txt;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageParam.java

@@ -0,0 +1,27 @@
+package com.fs.wxcid.dto.message;
+
+import com.fs.wxcid.dto.common.BaseAccountRequest;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 发送视频消息业务请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SendVideoMessageParam extends BaseAccountRequest {
+    /**
+     * 视频封面文件 URL
+     */
+    private String thumbUrl;
+
+    /**
+     * 视频文件 URL
+     */
+    private String videoUrl;
+
+    /**
+     * 接收方 wxid
+     */
+    private String toUser;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/wxcid/dto/message/SendVideoMessageRequest.java

@@ -0,0 +1,20 @@
+package com.fs.wxcid.dto.message;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * /message/CdnUploadVideo 请求体
+ */
+@Data
+public class SendVideoMessageRequest {
+
+    @JsonProperty("ThumbData")
+    private byte[] thumbData;
+
+    @JsonProperty("ToUserName")
+    private String toUserName;
+
+    @JsonProperty("VideoData")
+    private byte[] videoData;
+}

+ 9 - 6
fs-service/src/main/java/com/fs/wxcid/service/FriendService.java

@@ -3,7 +3,9 @@
 package com.fs.wxcid.service;
 
 import com.fs.wxcid.dto.common.ApiResponse;
+import com.fs.wxcid.dto.common.BaseResponse;
 import com.fs.wxcid.dto.friend.*;
+import com.fs.wxcid.vo.AddContactVo;
 
 /**
  * 好友管理服务接口
@@ -120,11 +122,9 @@ public interface FriendService {
      * 支持按昵称、微信号、手机号等模糊搜索。
      * </p>
      *
-     * @param key    账号唯一标识
-     * @param request 搜索参数(UserName + 场景配置)
      * @return 统一响应结果
      */
-    ApiResponse searchContact(String key, SearchContactRequest request);
+    SearchContactResponse searchContact(SearchContactParam param);
 
     /**
      * 上传手机通讯录用于匹配微信好友
@@ -147,11 +147,14 @@ public interface FriendService {
      * 若无 V3/V4,部分场景可能无法添加。
      * </p>
      *
-     * @param key    账号唯一标识
-     * @param request 添加请求参数(含 V3、V4、Scene、验证语等)
      * @return 统一响应结果
      */
-    ApiResponse verifyUser(String key, VerifyUserRequest request);
+    BaseResponse verifyUser(VerifyUserParam param);
 
     ContactListResponse getContactListNotKey(GetContactListParam param);
+
+    /**
+     * 添加联系人
+     */
+    AddContactVo addContact(AddContactParam param);
 }

+ 66 - 0
fs-service/src/main/java/com/fs/wxcid/service/IWxMsgLogService.java

@@ -0,0 +1,66 @@
+package com.fs.wxcid.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.wxcid.domain.WxMsgLog;
+import com.fs.wxcid.dto.callback.WxCallbackVo;
+
+import java.util.List;
+
+/**
+ * 个微消息记录Service接口
+ * 
+ * @author fs
+ * @date 2025-12-16
+ */
+public interface IWxMsgLogService extends IService<WxMsgLog>{
+    /**
+     * 查询个微消息记录
+     * 
+     * @param id 个微消息记录主键
+     * @return 个微消息记录
+     */
+    WxMsgLog selectWxMsgLogById(Long id);
+
+    /**
+     * 查询个微消息记录列表
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 个微消息记录集合
+     */
+    List<WxMsgLog> selectWxMsgLogList(WxMsgLog wxMsgLog);
+
+    /**
+     * 新增个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    int insertWxMsgLog(WxMsgLog wxMsgLog);
+
+    /**
+     * 修改个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    int updateWxMsgLog(WxMsgLog wxMsgLog);
+
+    /**
+     * 批量删除个微消息记录
+     * 
+     * @param ids 需要删除的个微消息记录主键集合
+     * @return 结果
+     */
+    int deleteWxMsgLogByIds(Long[] ids);
+
+    /**
+     * 删除个微消息记录信息
+     * 
+     * @param id 个微消息记录主键
+     * @return 结果
+     */
+    int deleteWxMsgLogById(Long id);
+
+    void insertLog(WxCallbackVo callbackVo, CompanyWxAccount account, int type);
+}

+ 6 - 24
fs-service/src/main/java/com/fs/wxcid/service/MessageService.java

@@ -1,32 +1,14 @@
 package com.fs.wxcid.service;
 
 
-import com.fs.wxcid.dto.common.ApiResponse;
-import com.fs.wxcid.dto.message.GetMsgBigImgRequest;
 import com.fs.wxcid.dto.message.*;
 
+import java.util.List;
+
 public interface MessageService {
+    List<SendMessageResult> sendTextMessage(SendTextMessageParam param);
+    List<SendImageMessageResult> sendImageMessage(SendImageMessageParam param);
+    CdnUploadVideoResult sendVideoMessage(SendVideoMessageParam param);
+    RevokeMsgResult revokeMessage(RevokeMsgRequest request);
 
-    ApiResponse addMessageMgr(String key, AddMessageMgrRequest request);
-    ApiResponse cdnUploadVideo(String key, CdnUploadVideoRequest request);
-    ApiResponse downloadEmojiGif(String key, DownloadEmojiGifRequest request);
-    ApiResponse forwardEmoji(String key, ForwardEmojiRequest request);
-    ApiResponse forwardImageMessage(String key, ForwardImageMessageRequest request);
-    ApiResponse forwardVideoMessage(String key, ForwardVideoMessageRequest request);
-    ApiResponse getMsgBigImg(String key, GetMsgBigImgRequest request);
-    ApiResponse getMsgVideo(String key, GetMsgVideoRequest request);
-    ApiResponse getMsgVoice(String key, GetMsgVoiceRequest request);
-    ApiResponse groupMassMsgImage(String key, GroupMassMsgImageRequest request);
-    ApiResponse groupMassMsgText(String key, GroupMassMsgTextRequest request);
-    ApiResponse httpSyncMsg(String key, HttpSyncMsgRequest request);
-    ApiResponse newSyncHistoryMessage(String key); // 无 body
-    ApiResponse revokeMsg(String key, RevokeMsgRequest request);
-    ApiResponse revokeMsgNew(String key, RevokeMsgNewRequest request);
-    ApiResponse sendAppMessage(String key, SendAppMessageRequest request);
-    ApiResponse sendEmojiMessage(String key, SendEmojiMessageRequest request);
-    ApiResponse sendImageMessage(String key, SendImageMessageRequest request);
-    ApiResponse sendImageNewMessage(String key, SendImageNewMessageRequest request);
-    ApiResponse sendTextMessage(String key, SendTextMessageRequest request);
-    ApiResponse sendVoice(String key, SendVoiceRequest request);
-    ApiResponse shareCardMessage(String key, ShareCardMessageRequest request);
 }

+ 61 - 4
fs-service/src/main/java/com/fs/wxcid/service/impl/FriendServiceImpl.java

@@ -1,13 +1,17 @@
 package com.fs.wxcid.service.impl;
 
+import com.alibaba.fastjson.JSON;
 import com.fs.wxcid.ServiceUtils;
 import com.fs.wxcid.dto.common.ApiResponse;
 import com.fs.wxcid.dto.common.ApiResponseCommon;
+import com.fs.wxcid.dto.common.BaseResponse;
 import com.fs.wxcid.dto.friend.*;
 import com.fs.wxcid.dto.login.RequestBaseVo;
 import com.fs.wxcid.service.FriendService;
+import com.fs.wxcid.vo.AddContactVo;
 import com.fs.wxwork.utils.WxWorkHttpUtil;
 import com.alibaba.fastjson.TypeReference;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -18,6 +22,7 @@ import java.util.Map;
  * 好友管理服务实现类
  * 调用微信私有 API 的 /friend 模块
  */
+@Slf4j
 @Service
 public class FriendServiceImpl implements FriendService {
 
@@ -72,8 +77,21 @@ public class FriendServiceImpl implements FriendService {
     }
 
     @Override
-    public ApiResponse searchContact(String key, SearchContactRequest request) {
-        return post("/friend/SearchContact", key, request);
+    public SearchContactResponse searchContact(SearchContactParam param) {
+        SearchContactRequest request = new SearchContactRequest();
+        request.setFromScene(0);
+        request.setOpCode(2);
+        request.setSearchScene(1);
+        request.setUserName(param.getPhone());
+        ApiResponseCommon<SearchContactResponse> response = serviceUtils.sendPost(BASE_URL + "SearchContact", RequestBaseVo.builder().accountId(param.getAccountId()).data(request).build(), new TypeReference<ApiResponseCommon<SearchContactResponse>>() {});
+        SearchContactResponse data = response.getData();
+        String userNameStr = data.getUserNameStr();
+        String antispamTicket = data.getAntispamTicket();
+        log.info("搜索成功 - V3: {}, V4: {}",
+                userNameStr != null ? userNameStr : "null",
+                antispamTicket != null ? antispamTicket : "null"
+        );
+        return data;
     }
 
     @Override
@@ -82,8 +100,17 @@ public class FriendServiceImpl implements FriendService {
     }
 
     @Override
-    public ApiResponse verifyUser(String key, VerifyUserRequest request) {
-        return post("/friend/VerifyUser", key, request);
+    public BaseResponse verifyUser(VerifyUserParam param) {
+        AgreeAddRequest request = new AgreeAddRequest();
+        request.setChatRoomUserName(param.getChatRoomUserName());
+        request.setOpCode(param.getOpCode());
+        request.setScene(param.getScene());
+        request.setV3(param.getV3());
+        request.setV4(param.getV4());
+        request.setVerifyContent(param.getContent());
+        ApiResponseCommon<BaseResponse> response = serviceUtils.sendPost(BASE_URL + "VerifyUser", RequestBaseVo.builder().accountId(param.getAccountId()).data(request).build(), new TypeReference<ApiResponseCommon<BaseResponse>>() {});
+        log.info("好友验证请求已成功发送");
+        return response.getData();
     }
 
     @Override
@@ -123,4 +150,34 @@ public class FriendServiceImpl implements FriendService {
         String resp = WxWorkHttpUtil.get(url, params);
         return com.alibaba.fastjson.JSON.parseObject(resp, ApiResponse.class);
     }
+
+    @Override
+    public AddContactVo addContact(AddContactParam param) {
+        AddContactVo resultVo = new AddContactVo();
+        try {
+            SearchContactParam searchParam = new SearchContactParam();
+            searchParam.setAccountId(param.getAccountId());
+            searchParam.setPhone(param.getMobile());
+            SearchContactResponse searchContactResponse = searchContact(searchParam);
+            VerifyUserParam verifyParam = new VerifyUserParam();
+            verifyParam.setAccountId(param.getAccountId());
+            verifyParam.setV3(searchContactResponse.getUserNameStr());
+            verifyParam.setV4(searchContactResponse.getAntispamTicket());
+            resultVo.setV3(verifyParam.getV3());
+            resultVo.setV4(verifyParam.getV4());
+            verifyParam.setScene(15);
+            verifyParam.setOpCode(2);
+            verifyParam.setContent(param.getTxt());
+            BaseResponse response = verifyUser(verifyParam);
+            if(response.getRet() != 0){
+                log.error("添加失败,非系统错误!:{}", JSON.toJSONString(response.getErrMsg()));
+            }
+            resultVo.setSuccess(response.getRet() == 0);
+            resultVo.setSuccess(true);
+            return resultVo;
+        }catch (Exception e){
+            log.error("CLIENT-ID:{}添加好友失败:{}-手机号:{}", param.getClientId(), param.getAccountId(), param.getMobile(), e);
+            return  resultVo;
+        }
+    }
 }

+ 102 - 147
fs-service/src/main/java/com/fs/wxcid/service/impl/MessageServiceImpl.java

@@ -1,158 +1,113 @@
 package com.fs.wxcid.service.impl;
 
-import com.fs.wxcid.dto.common.ApiResponse;
-import com.fs.wxcid.dto.message.GetMsgBigImgRequest;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.common.exception.CustomException;
+import com.fs.wxcid.FileToBase64Util;
+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.*;
 import com.fs.wxcid.service.MessageService;
-import com.fs.wxwork.utils.WxWorkHttpUtil;
-import com.alibaba.fastjson.TypeReference;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.List;
 
+@Slf4j
 @Service
 public class MessageServiceImpl implements MessageService {
-
-    private static final String BASE_URL = "http://114.117.215.244:7006";
-
-
-    @Override
-    public ApiResponse addMessageMgr(String key, AddMessageMgrRequest request) {
-        return post("/message/AddMessageMgr", key, request);
-    }
-
-    @Override
-    public ApiResponse cdnUploadVideo(String key, CdnUploadVideoRequest request) {
-        return post("/message/CdnUploadVideo", key, request);
-    }
-
-    @Override
-    public ApiResponse downloadEmojiGif(String key, DownloadEmojiGifRequest request) {
-        return post("/message/DownloadEmojiGif", key, request);
-    }
-
-    @Override
-    public ApiResponse forwardEmoji(String key, ForwardEmojiRequest request) {
-        return post("/message/ForwardEmoji", key, request);
-    }
-
-    @Override
-    public ApiResponse forwardImageMessage(String key, ForwardImageMessageRequest request) {
-        return post("/message/ForwardImageMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse forwardVideoMessage(String key, ForwardVideoMessageRequest request) {
-        return post("/message/ForwardVideoMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse getMsgBigImg(String key, GetMsgBigImgRequest request) {
-        return post("/message/GetMsgBigImg", key, request);
-    }
-
-    @Override
-    public ApiResponse getMsgVideo(String key, GetMsgVideoRequest request) {
-        return post("/message/GetMsgVideo", key, request);
-    }
-
-    @Override
-    public ApiResponse getMsgVoice(String key, GetMsgVoiceRequest request) {
-        return post("/message/GetMsgVoice", key, request);
-    }
-
-    @Override
-    public ApiResponse groupMassMsgImage(String key, GroupMassMsgImageRequest request) {
-        return post("/message/GroupMassMsgImage", key, request);
-    }
-
-    @Override
-    public ApiResponse groupMassMsgText(String key, GroupMassMsgTextRequest request) {
-        return post("/message/GroupMassMsgText", key, request);
-    }
-
-    @Override
-    public ApiResponse httpSyncMsg(String key, HttpSyncMsgRequest request) {
-        return post("/message/HttpSyncMsg", key, request);
-    }
-
-    @Override
-    public ApiResponse newSyncHistoryMessage(String key) {
-        return post("/message/NewSyncHistoryMessage", key, new Object()); // 无 body,传空对象或自定义
-    }
-
-    @Override
-    public ApiResponse revokeMsg(String key, RevokeMsgRequest request) {
-        return post("/message/RevokeMsg", key, request);
-    }
-
-    @Override
-    public ApiResponse revokeMsgNew(String key, RevokeMsgNewRequest request) {
-        return post("/message/RevokeMsgNew", key, request);
-    }
-
-    @Override
-    public ApiResponse sendAppMessage(String key, SendAppMessageRequest request) {
-        return post("/message/SendAppMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse sendEmojiMessage(String key, SendEmojiMessageRequest request) {
-        return post("/message/SendEmojiMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse sendImageMessage(String key, SendImageMessageRequest request) {
-        return post("/message/SendImageMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse sendImageNewMessage(String key, SendImageNewMessageRequest request) {
-        return post("/message/SendImageNewMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse sendTextMessage(String key, SendTextMessageRequest request) {
-        return post("/message/SendTextMessage", key, request);
-    }
-
-    @Override
-    public ApiResponse sendVoice(String key, SendVoiceRequest request) {
-        return post("/message/SendVoice", key, request);
-    }
-
-    @Override
-    public ApiResponse shareCardMessage(String key, ShareCardMessageRequest request) {
-        return post("/message/ShareCardMessage", key, request);
-    }
-
-    // ------------------ 工具方法 ------------------
-    /**
-     * 通用 POST 请求方法
-     *
-     * @param path   接口路径,如 "/friend/AgreeAdd"
-     * @param key    账号唯一标识(query 参数)
-     * @param request 请求体对象
-     * @return 统一响应结果
-     */
-    private ApiResponse post(String path, String key, Object request) {
-        String url = BASE_URL + path + "?key=" + key;
-        return WxWorkHttpUtil.postWithType(url, request, new TypeReference<ApiResponse>() {});
-    }
-
-    /**
-     * 通用 GET 请求方法(无请求体)
-     *
-     * @param path 接口路径
-     * @param key  账号唯一标识
-     * @return 统一响应结果
-     */
-    private ApiResponse get(String path, String key) {
-        String url = BASE_URL + path;
-        Map<String, Object> params = new HashMap<>();
-        params.put("key", key);
-        String resp = WxWorkHttpUtil.get(url, params);
-        return com.alibaba.fastjson.JSON.parseObject(resp, ApiResponse.class);
+    private final static String BASE_URL = "/message/";
+    @Autowired
+    private ServiceUtils serviceUtils;
+
+    @Override
+    public List<SendMessageResult> sendTextMessage(SendTextMessageParam param) {
+        SendTextMessageRequest request = new SendTextMessageRequest();
+        List<MsgItem> list = new ArrayList<>();
+        MsgItem msgItem = new MsgItem();
+        msgItem.setTextContent(param.getTxt());
+        list.add(msgItem);
+        request.setMsgItem(list);
+        ApiResponseCommon<List<SendMessageResult>> response = serviceUtils.sendPost(BASE_URL + "SendTextMessage", RequestBaseVo.builder().accountId(param.getAccountId()).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.getData();
+    }
+
+    @Override
+    public List<SendImageMessageResult> sendImageMessage(SendImageMessageParam param) {
+        SendTextMessageRequest request = new SendTextMessageRequest();
+        List<MsgItem> list = new ArrayList<>();
+        MsgItem msgItem = new MsgItem();
+        try {
+            msgItem.setMsgType(0);
+            msgItem.setImageContent(FileToBase64Util.convertImageUrlToBase64(param.getImgUrl()));
+            msgItem.setToUserName(param.getToUser());
+        }catch (Exception e){
+            log.error("发送消息时,图片转换base64错误", e);
+            throw new CustomException("图片消息发送失败");
+        }
+        list.add(msgItem);
+        request.setMsgItem(list);
+        ApiResponseCommon<List<SendImageMessageResult>> response = serviceUtils.sendPost(BASE_URL + "SendImageNewMessage", RequestBaseVo.builder().accountId(param.getAccountId()).data(request).build(), new TypeReference<ApiResponseCommon<List<SendImageMessageResult>>>() {});
+        // 第二层:检查每条消息是否真正发送成功(根据 resp.baseResponse.ret == 0 判断)
+        List<SendImageMessageResult> results = response.getData();
+        List<String> failedRecipients = new ArrayList<>();
+        for (SendImageMessageResult result : results) {
+            if (!result.isSendSuccess()) {
+                failedRecipients.add(result.getToUserName());
+            }
+        }
+        if (!failedRecipients.isEmpty()) {
+            String errorMsg = "部分图片消息发送失败,失败接收人: " + String.join(", ", failedRecipients);
+            throw new CustomException("发送图片消息部分失败: " + errorMsg);
+        }
+        return response.getData();
+    }
+
+    @Override
+    public RevokeMsgResult revokeMessage(RevokeMsgRequest request) {
+        ApiResponseCommon<RevokeMsgResult> response = serviceUtils.sendPost(BASE_URL + "RevokeMsgNew", RequestBaseVo.builder().accountId(request.getAccountId()).data(request).build(), new TypeReference<ApiResponseCommon<RevokeMsgResult>>() {});
+        RevokeMsgResult result = response.getData();
+        if (result != null && !result.isSuccess()) {
+            String errorMsg = result.getSysWording() != null ? result.getSysWording() : "消息撤回失败";
+            throw new CustomException(errorMsg);
+        }
+        return response.getData();
+    }
+
+    @Override
+    public CdnUploadVideoResult sendVideoMessage(SendVideoMessageParam param) {
+        SendVideoMessageRequest request = new SendVideoMessageRequest();
+        request.setToUserName(param.getToUser());
+        try {
+            request.setThumbData(FileToBase64Util.downloadToBytes(param.getThumbUrl()));
+            request.setVideoData(FileToBase64Util.downloadToBytes(param.getVideoUrl()));
+        } catch (Exception e) {
+            log.error("发送视频消息时,文件下载失败", e);
+            throw new CustomException("视频消息发送失败");
+        }
+        ApiResponseCommon<CdnUploadVideoResult> response = serviceUtils.sendPost(
+                BASE_URL + "CdnUploadVideo",
+                RequestBaseVo.builder().accountId(param.getAccountId()).data(request).build(),
+                new TypeReference<ApiResponseCommon<CdnUploadVideoResult>>() {}
+        );
+        CdnUploadVideoResult result = response.getData();
+        if (result != null && result.getRetCode() != null && result.getRetCode() != 0) {
+            throw new CustomException("视频上传失败,RetCode=" + result.getRetCode());
+        }
+        return result;
     }
 }

+ 116 - 0
fs-service/src/main/java/com/fs/wxcid/service/impl/WxMsgLogServiceImpl.java

@@ -0,0 +1,116 @@
+package com.fs.wxcid.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.wxcid.domain.WxMsgLog;
+import com.fs.wxcid.dto.callback.WxCallbackVo;
+import com.fs.wxcid.mapper.WxMsgLogMapper;
+import com.fs.wxcid.service.IWxMsgLogService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 个微消息记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-16
+ */
+@Service
+public class WxMsgLogServiceImpl extends ServiceImpl<WxMsgLogMapper, WxMsgLog> implements IWxMsgLogService {
+
+    /**
+     * 查询个微消息记录
+     * 
+     * @param id 个微消息记录主键
+     * @return 个微消息记录
+     */
+    @Override
+    public WxMsgLog selectWxMsgLogById(Long id)
+    {
+        return baseMapper.selectWxMsgLogById(id);
+    }
+
+    /**
+     * 查询个微消息记录列表
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 个微消息记录
+     */
+    @Override
+    public List<WxMsgLog> selectWxMsgLogList(WxMsgLog wxMsgLog)
+    {
+        return baseMapper.selectWxMsgLogList(wxMsgLog);
+    }
+
+    /**
+     * 新增个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    @Override
+    public int insertWxMsgLog(WxMsgLog wxMsgLog)
+    {
+        wxMsgLog.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxMsgLog(wxMsgLog);
+    }
+
+    /**
+     * 修改个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    @Override
+    public int updateWxMsgLog(WxMsgLog wxMsgLog)
+    {
+        wxMsgLog.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxMsgLog(wxMsgLog);
+    }
+
+    /**
+     * 批量删除个微消息记录
+     * 
+     * @param ids 需要删除的个微消息记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteWxMsgLogByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxMsgLogByIds(ids);
+    }
+
+    /**
+     * 删除个微消息记录信息
+     * 
+     * @param id 个微消息记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteWxMsgLogById(Long id)
+    {
+        return baseMapper.deleteWxMsgLogById(id);
+    }
+
+    @Override
+    public void insertLog(WxCallbackVo callbackVo, CompanyWxAccount account, int type) {
+        String formUser = callbackVo.getMessage().getFromUserName().getStr();
+        WxMsgLog logs = new WxMsgLog();
+        logs.setAccountId(account.getId());
+        logs.setAuthKey(account.getAuthKey());
+        logs.setType(callbackVo.getType());
+        logs.setMsgId(callbackVo.getMessage().getMsgId());
+        logs.setFromUserName(formUser);
+        logs.setToUserName(callbackVo.getMessage().getToUserName().getStr());
+        logs.setMsgType(callbackVo.getMessage().getMsgType());
+        logs.setContent(callbackVo.getMessage().getContent().getStr());
+        logs.setImgStatus(callbackVo.getMessage().getImgStatus());
+        logs.setImgBuf(callbackVo.getMessage().getImgBuf().getLen());
+        logs.setPushContent(callbackVo.getMessage().getPushContent());
+        logs.setNewMsgId(callbackVo.getMessage().getNewMsgId());
+        logs.setReceiveType(type);
+        save(logs);
+    }
+}

+ 26 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxAddSearchDTO.java

@@ -0,0 +1,26 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+/**
+ * @Author:peicj
+ * @Description: 搜索添加外部联系人
+ * @Date:2026/2/28 16:47
+ */
+@Data
+public class WxAddSearchDTO {
+    /**
+     * 用户UUID
+     */
+    private String uuid;
+
+    private Long vid;
+
+    private String optionid;
+
+    private String phone;
+
+    private String content;
+
+    private String ticket;
+}

+ 21 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxSearchContactDTO.java

@@ -0,0 +1,21 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+/**
+ * @Author:peicj
+ * @Description: 根据手机号搜索联系人
+ * @Date:2026/2/28 16:46
+ */
+@Data
+public class WxSearchContactDTO {
+    /**
+     * 用户UUID
+     */
+    private String uuid;
+    /**
+     * 手机号
+     */
+    private String phoneNumber;
+
+}

+ 35 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxSearchContactResp.java

@@ -0,0 +1,35 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author:peicj
+ * @Description: 根据手机号搜索联系人
+ * @Date:2026/2/28 16:50
+ */
+@Data
+public class WxSearchContactResp {
+
+    private List<UserList> userList;
+
+    @Data
+    public class UserList {
+        private String headImg;
+
+        private String ticket;
+
+        private Long user_id;
+
+        private Integer sex;
+
+        private String name;
+        //1:企微 2:个微
+        private String state;
+
+        private Long corp_id;
+
+        private String openid;
+    }
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java

@@ -240,4 +240,19 @@ public interface WxWorkService {
     WxWorkResponseDTO<WxCdnUploadImgLinkResp> cdnUploadImgLink(WxCdnUploadImgLinkDTO param, Long serverId);
 
     WxWorkResponseDTO<WxCdnUploadVideoResp> uploadCdnVideoLink(WxCdnUploadVideoLinkDTO param, Long serverId);
+
+    /**
+     * 根据手机号搜索联系人
+     * @param param     搜索参数
+     * @param serverId  服务器ID
+     * @return WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<WxSearchContactResp> searchContact(WxSearchContactDTO param, Long serverId);
+    /**
+     * 搜索添加外部联系人
+     * @param param     搜索参数
+     * @param serverId  服务器ID
+     * @return WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<String> addSearch(WxAddSearchDTO param, Long serverId);
 }

+ 14 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java

@@ -335,4 +335,18 @@ public class WxWorkServiceImpl implements WxWorkService {
         String url = getUrl(serverId) + "/UploadCdnVideoLink";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxCdnUploadVideoResp>>() {});
     }
+
+    @Override
+    public WxWorkResponseDTO<WxSearchContactResp> searchContact(WxSearchContactDTO param, Long serverId) {
+        String url = getUrl(serverId) + "/SearchContact";
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxSearchContactResp>>() {
+        });
+    }
+
+    @Override
+    public WxWorkResponseDTO<String> addSearch(WxAddSearchDTO param, Long serverId) {
+        String url = getUrl(serverId) + "/AddSearch";
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<String>>() {
+        });
+    }
 }

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

@@ -193,7 +193,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where deleted = 0
         and status = 1
         and company_id is null
-        and target_type = #{targetType}
+        <if test="targetType != null and targetType != ''">
+            and target_type = #{targetType}
+        </if>
         <if test="businessType != null and businessType != ''">
             and business_type = #{businessType}
         </if>

+ 98 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogMapper.xml

@@ -0,0 +1,98 @@
+<?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.company.mapper.CompanyVoiceRoboticCallLogMapper">
+    
+    <resultMap type="CompanyVoiceRoboticCallLog" id="CompanyVoiceRoboticCallLogResult">
+        <result property="logId"    column="log_id"    />
+        <result property="roboticId"    column="robotic_id"    />
+        <result property="callerId"    column="caller_id"    />
+        <result property="runFunction"    column="run_function"    />
+        <result property="runTime"    column="run_time"    />
+        <result property="runParam"    column="run_param"    />
+        <result property="result"    column="result"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectCompanyVoiceRoboticCallLogVo">
+        select log_id, robotic_id, caller_id, wx_client_id, run_function, run_time, run_param, result, status,create_time from company_voice_robotic_call_log
+    </sql>
+
+    <select id="selectCompanyVoiceRoboticCallLogList" parameterType="CompanyVoiceRoboticCallLog" resultMap="CompanyVoiceRoboticCallLogResult">
+        <include refid="selectCompanyVoiceRoboticCallLogVo"/>
+        <where>  
+            <if test="roboticId != null "> and robotic_id = #{roboticId}</if>
+            <if test="callerId != null "> and caller_id = #{callerId}</if>
+            <if test="wxClientId != null "> and wx_client_id = #{wxClientId}</if>
+            <if test="runFunction != null  and runFunction != ''"> and run_function = #{runFunction}</if>
+            <if test="runTime != null "> and run_time = #{runTime}</if>
+            <if test="runParam != null  and runParam != ''"> and run_param = #{runParam}</if>
+            <if test="result != null  and result != ''"> and result = #{result}</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="createTime != null "> and create_time = #{createTime}</if>
+        </where>
+    </select>
+    
+    <select id="selectCompanyVoiceRoboticCallLogByLogId" parameterType="Long" resultMap="CompanyVoiceRoboticCallLogResult">
+        <include refid="selectCompanyVoiceRoboticCallLogVo"/>
+        where log_id = #{logId}
+    </select>
+        
+    <insert id="insertCompanyVoiceRoboticCallLog" parameterType="CompanyVoiceRoboticCallLog" useGeneratedKeys="true" keyProperty="logId">
+        insert into company_voice_robotic_call_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="roboticId != null">robotic_id,</if>
+            <if test="callerId != null">caller_id,</if>
+            <if test="wxClientId != null">wx_client_id,</if>
+            <if test="runFunction != null">run_function,</if>
+            <if test="runTime != null">run_time,</if>
+            <if test="runParam != null">run_param,</if>
+            <if test="result != null">result,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="roboticId != null">#{roboticId},</if>
+            <if test="callerId != null">#{callerId},</if>
+            <if test="wxClientId != null">#{wxClientId},</if>
+            <if test="runFunction != null">#{runFunction},</if>
+            <if test="runTime != null">#{runTime},</if>
+            <if test="runParam != null">#{runParam},</if>
+            <if test="result != null">#{result},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanyVoiceRoboticCallLog" parameterType="CompanyVoiceRoboticCallLog">
+        update company_voice_robotic_call_log
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="roboticId != null">robotic_id = #{roboticId},</if>
+            <if test="callerId != null">caller_id = #{callerId},</if>
+            <if test="wxClientId != null">wx_client_id = #{wxClientId},</if>
+            <if test="runFunction != null">run_function = #{runFunction},</if>
+            <if test="runTime != null">run_time = #{runTime},</if>
+            <if test="runParam != null">run_param = #{runParam},</if>
+            <if test="result != null">result = #{result},</if>
+            <if test="status != null">status = #{status},</if>
+        </trim>
+        where log_id = #{logId}
+    </update>
+
+    <delete id="deleteCompanyVoiceRoboticCallLogByLogId" parameterType="Long">
+        delete from company_voice_robotic_call_log where log_id = #{logId}
+    </delete>
+
+    <delete id="deleteCompanyVoiceRoboticCallLogByLogIds" parameterType="String">
+        delete from company_voice_robotic_call_log where log_id in 
+        <foreach item="logId" collection="array" open="(" separator="," close=")">
+            #{logId}
+        </foreach>
+    </delete>
+
+    <select id="selectNoResultLogByCallees" parameterType="com.fs.company.domain.CompanyVoiceRoboticCallees"  resultType="CompanyVoiceRoboticCallLog">
+        select * from company_voice_robotic_call_log where robotic_id = #{callees.roboticId} And caller_id = #{callees.id} And status = 1
+    </select>
+</mapper>

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

@@ -210,5 +210,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </update>
+
+    <update id="finishRobotic" parameterType="java.lang.Long">
+        update company_voice_robotic set task_status = 3 where id = #{id} and task_flow = run_task_flow
+    </update>
+
+    <select id="selectSceneTaskByCompanyIdAndType" resultType="CompanyVoiceRobotic">
+        select * from company_voice_robotic where company_id = #{companyId}
+                                              and scene_type = #{sceneType}
+                                              and task_status = 1
+                                              and del_flag = 0
+        order by create_time desc
+    </select>
     
 </mapper>

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

@@ -197,7 +197,7 @@
     </select>
     <select id="getQwAddWxList4Workflow" resultType="com.fs.company.vo.CompanyWxClient4WorkFlowVO">
 
-        SELECT t1.*,t3.workflow_instance_id,t3.current_node_key,t3.current_node_name,t3.current_node_type,t4.node_config,t2.callee_id FROM company_wx_client t1
+        SELECT t1.*,t3.workflow_instance_id,t3.current_node_key,t3.current_node_name,t3.current_node_type,t4.node_config,t2.callee_id,t4.node_key FROM company_wx_client t1
         inner join company_voice_robotic_business t2 on t1.id = t2.wx_client_id and t1.robotic_id = t2.robotic_id
         inner join company_ai_workflow_exec t3 on t3.business_key = t2.id
         inner join company_ai_workflow_node t4 on t3.current_node_key=t4.node_key

+ 0 - 1
fs-wx-api/src/main/java/com/fs/app/controller/AppBaseController.java

@@ -2,7 +2,6 @@ package com.fs.app.controller;
 
 
 import com.fs.app.utils.JwtUtils;
-import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.ServletUtils;
 import io.jsonwebtoken.Claims;
 import org.springframework.beans.factory.annotation.Autowired;

+ 43 - 0
fs-wx-api/src/main/java/com/fs/app/controller/CallBackController.java

@@ -0,0 +1,43 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.wxcid.dto.callback.WxCallbackVo;
+import com.fs.wxcid.service.IWxMsgLogService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+
+/**
+ * 客户Controller
+ *
+ * @author fs
+ * @date 2022-12-21
+ */
+@Slf4j
+@AllArgsConstructor
+@RestController
+@RequestMapping("/wx/back")
+public class CallBackController {
+    @Autowired
+    private ICompanyWxAccountService companyWxAccountService;
+    @Autowired
+    private IWxMsgLogService wxMsgLogService;
+    @PostMapping
+    public R addWxResult(@RequestBody WxCallbackVo callbackVo) throws IOException {
+//        log.info("===进入回调===");
+//        CompanyWxAccount account = companyWxAccountService.getOne(new QueryWrapper<CompanyWxAccount>().eq("auth_key", callbackVo.getKey()));
+//        String formUser = callbackVo.getMessage().getFromUserName().getStr();
+//        if(formUser.equals(account.getWxNo())){
+//            wxMsgLogService.insertLog(callbackVo, account, 1);
+//        }
+        return R.ok();
+    }
+
+}

+ 3 - 36
fs-wx-api/src/main/java/com/fs/app/controller/CommonController.java

@@ -1,57 +1,24 @@
 package com.fs.app.controller;
 
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.fs.app.annotation.Login;
-import com.fs.app.param.SendSopParam;
 import com.fs.app.utils.JwtUtils;
-import com.fs.app.websocket.bean.MsgBean;
 import com.fs.app.websocket.service.WebSocketServer;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
-import com.fs.company.domain.CompanyConfig;
-import com.fs.company.domain.CompanyMoneyLogs;
-import com.fs.company.service.ICompanyConfigService;
 import com.fs.company.service.ICompanyWxChatService;
-import com.fs.his.service.IFsAppVersionService;
-import com.fs.his.service.IFsCityService;
-import com.fs.his.service.IFsInquiryOrderMsgService;
-import com.fs.his.utils.ConfigUtil;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.mapper.QwSopLogsMapper;
-import com.fs.system.service.ISysConfigService;
-import com.fs.system.service.ISysDictDataService;
-import com.fs.wx.kf.dto.WeixinKfMsgDTO;
-import com.fs.wx.kf.dto.WeixinKfMsgSendDTO;
-import com.fs.wx.kf.dto.WeixinKfTextMsgDTO;
-import com.fs.wx.kf.service.IWeixinKfService;
-import com.fs.wx.kf.vo.WeixinKfMsgItemVO;
-import com.fs.wx.kf.vo.WeixinKfMsgVO;
 import com.fs.wxUser.service.ICompanyWxUserService;
-import com.google.code.kaptcha.Producer;
-import com.qq.weixin.mp.aes.AesException;
-import com.qq.weixin.mp.aes.WXBizMsgCrypt;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.format.annotation.DateTimeFormat;
-import org.springframework.web.bind.annotation.*;
-import org.w3c.dom.Element;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
-import javax.annotation.Resource;
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.util.List;
-import java.util.UUID;
-import java.util.logging.Logger;
-
-import static com.fs.common.constant.FsConstants.REDIS_CHAT_NEXTCURSOR;
 
 
 @Api("公共接口")

+ 147 - 0
fs-wx-api/src/main/java/com/fs/app/controller/WebscoketServer.java

@@ -0,0 +1,147 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.wxcid.domain.CidIpadServer;
+import com.fs.wxcid.dto.callback.WxCallbackVo;
+import com.fs.wxcid.service.ICidIpadServerService;
+import com.fs.wxcid.service.IWxMsgLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.*;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+
+@Slf4j
+@Component
+public class WebscoketServer {
+    @Autowired
+    private ICidIpadServerService cidIpadServerService;
+    @Autowired
+    private ICompanyWxAccountService companyWxAccountService;
+    @Autowired
+    private IWxMsgLogService wxMsgLogService;
+    @Value("${group-no}")
+    private String groupNo;
+
+    // 重连间隔(秒)
+    private static final int RECONNECT_INTERVAL = 5;
+    // 客户端会话
+    private Map<Long, Session> sessionMap = new ConcurrentHashMap<>();
+    // ipad 数据
+    private Map<Long, CidIpadServer> ipadMap = new HashMap<>();
+    // 重连调度器
+    private final ScheduledExecutorService reconnectScheduler = Executors.newSingleThreadScheduledExecutor();
+
+    /**
+     * 启动客户端(连接第三方 + 监听消息)
+     */
+    public void start() {
+        List<CidIpadServer> ipadList = cidIpadServerService.list(new QueryWrapper<CidIpadServer>().eq("group_no", groupNo));
+        if(ipadList.isEmpty()){
+            log.info("没有需要连接的账号");
+            return;
+        }
+        ipadMap = PubFun.listToMapByGroupObject(ipadList, CidIpadServer::getId);
+        List<CompanyWxAccount> list = companyWxAccountService.list(new QueryWrapper<CompanyWxAccount>().eq("server_status", 1).eq("login_status", 1).in("server_id", PubFun.listToNewList(ipadList, CidIpadServer::getId)));
+        list.forEach(this::connect);
+//        connect();
+    }
+
+    /**
+     * 连接第三方 WebSocket 服务
+     */
+    private void connect(CompanyWxAccount account) {
+        try {
+            CidIpadServer ipad = ipadMap.get(account.getServerId());
+            String url = "ws://" + ipad.getIp() + ":" + ipad.getPort() + "/ws/GetSyncMsg?key=" + account.getAuthKey();
+            // 1. 创建 WebSocket 客户端容器(SpringBoot 2 内置)
+            WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+            // 2. 连接第三方服务,绑定消息处理器
+            container.connectToServer(new WebSocketMessageHandler(account, ipad), URI.create(url));
+            log.info("✅ 原生 WebSocket 连接第三方服务成功:{}", url);
+        } catch (Exception e) {
+            log.error("❌ 原生 WebSocket 连接失败:{}", e.getMessage(), e);
+            scheduleReconnect(account);
+        }
+    }
+
+    /**
+     * 消息处理器(核心:接收/处理第三方消息)
+     */
+    @ClientEndpoint
+    public class WebSocketMessageHandler {
+        private CompanyWxAccount account;
+        private CidIpadServer ipad;
+        public WebSocketMessageHandler(CompanyWxAccount account, CidIpadServer ipad) {
+            this.account = account;
+            this.ipad = ipad;
+        }
+
+        // 连接成功回调(保存会话)
+        @OnOpen
+        public void onOpen(Session session) {
+            sessionMap.put(account.getId(), session);
+            log.info("✅ WebSocket 会话建立,SessionID:{}", session.getId());
+            // 取消重连(若有)
+            reconnectScheduler.shutdownNow();
+        }
+
+        // 接收第三方消息(核心业务逻辑)
+        @OnMessage
+        public void onMessage(String message) {
+            WxCallbackVo callbackVo = JSON.parseObject(message, WxCallbackVo.class);
+            log.info("📩 收到第三方 WebSocket 消息:{}", callbackVo);
+            WxCallbackVo.Message msgVo = callbackVo.getMessage();
+            String formUser = msgVo.getFromUserName().getStr();
+            if(msgVo.getMsgType() == 10000 && "以上是打招呼的消息".equals(msgVo.getContent().getStr())){
+                companyWxAccountService.isCheckContact(formUser, account.getId());
+            }
+            wxMsgLogService.insertLog(callbackVo, account, !formUser.equals(account.getWxNo()) ? 0 : 1);
+        }
+
+        // 处理二进制消息(若第三方推送二进制数据,如文件/字节流)
+        @OnMessage
+        public void onMessage(byte[] binaryData) {
+            log.info("📩 收到第三方二进制消息,长度:{} 字节", binaryData.length);
+        }
+
+        // 连接关闭回调(触发重连)
+        @OnClose
+        public void onClose(Session session, CloseReason reason) {
+            log.error("❌ WebSocket 会话关闭:{},{}秒后重连", reason.getReasonPhrase(), RECONNECT_INTERVAL);
+            sessionMap.remove(account.getId());
+            scheduleReconnect(account);
+        }
+
+        // 连接异常回调(触发重连)
+        @OnError
+        public void onError(Session session, Throwable error) {
+            log.error("连接报错!!!", error);
+        }
+    }
+
+    /**
+     * 调度重连
+     */
+    private void scheduleReconnect(CompanyWxAccount account) {
+        if (!reconnectScheduler.isShutdown()) {
+            reconnectScheduler.schedule(() -> connect(account), RECONNECT_INTERVAL, TimeUnit.SECONDS);
+            log.info("⏳ 已调度 {} 秒后重连第三方 WebSocket 服务", RECONNECT_INTERVAL);
+        }
+    }
+}
+

+ 0 - 1
fs-wx-api/src/main/java/com/fs/app/param/CompanyWxListParam.java

@@ -1,6 +1,5 @@
 package com.fs.app.param;
 
-import com.fs.wxUser.domain.CompanyWxUser;
 import lombok.Data;
 
 @Data

+ 0 - 1
fs-wx-api/src/main/java/com/fs/app/websocket/bean/MsgBean.java

@@ -1,6 +1,5 @@
 package com.fs.app.websocket.bean;
 
-import com.fs.chat.domain.ChatMsg;
 import lombok.Data;
 
 import java.io.Serializable;

+ 1 - 32
fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java

@@ -1,54 +1,23 @@
 package com.fs.app.websocket.service;
 
 
-import cn.hutool.core.util.IdUtil;
-import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
-import com.fs.ai.service.IBaiduAIService;
-import com.fs.ai.vo.BaiduAIMsgResultVO;
-import com.fs.app.param.SopLogsEditParam;
-import com.fs.app.websocket.bean.MsgBean;
 import com.fs.app.websocket.bean.SendMsgVo;
-import com.fs.chat.config.WxConfig;
-import com.fs.chat.domain.ChatKeyword;
-import com.fs.chat.domain.ChatMsg;
-import com.fs.chat.domain.ChatRole;
-import com.fs.chat.domain.ChatSession;
-import com.fs.chat.service.IChatMsgService;
-import com.fs.chat.service.IChatRoleService;
-import com.fs.chat.service.IChatSessionService;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.spring.SpringUtils;
-
-import com.fs.company.domain.CompanyUser;
 import com.fs.company.domain.CompanyWxChat;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyWxChatService;
-import com.fs.course.domain.FsCourseSopLogs;
 import com.fs.course.service.IFsCourseSopLogsService;
-import com.fs.his.domain.FsUser;
-import com.fs.his.service.IFsUserService;
-import com.fs.wx.kf.dto.WeixinKfImageMsgDTO;
-import com.fs.wx.kf.dto.WeixinKfMsgSendDTO;
-import com.fs.wx.kf.dto.WeixinKfTextMsgDTO;
 import com.fs.wxUser.domain.CompanyWxUser;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.PathParam;
 import javax.websocket.server.ServerEndpoint;
 import java.io.IOException;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.Date;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import static com.fs.common.constant.FsConstants.REDIS_CHAT_SESSION;
 
 @ServerEndpoint("/app/webSocket/{uid}")
 @Component

+ 0 - 1
fs-wx-api/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -9,7 +9,6 @@ import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.ip.IpUtils;
-
 import com.fs.framework.manager.AsyncManager;
 import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.system.domain.SysOperLog;

+ 1 - 1
fs-wx-api/src/main/java/com/fs/framework/config/DruidConfig.java

@@ -1,4 +1,4 @@
-//package com.fs.framework.config;
+package com.fs.framework.config;//package com.fs.framework.config;
 //
 //import com.alibaba.druid.pool.DruidDataSource;
 //import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;

+ 0 - 2
fs-wx-api/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -1,10 +1,8 @@
 package com.fs.framework.config;
 
 import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
-import com.fs.common.utils.StringUtils;
 import org.apache.ibatis.io.VFS;
 import org.apache.ibatis.session.SqlSessionFactory;
-import org.mybatis.spring.SqlSessionFactoryBean;
 import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;

+ 0 - 1
fs-wx-api/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -2,7 +2,6 @@ package com.fs.framework.config;
 
 import com.fs.common.config.FSConfig;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.models.auth.In;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;

+ 23 - 0
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -1054,6 +1054,13 @@ public class WxTaskService {
             log.info("ROBOTIC-ID:{},企微申请加好友任务申请成功", client.getRoboticId());
 
             asyncSaveCompanyVoiceRoboticCallLog(addLog);
+
+            //更新工作流的执行日志
+            CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+            queryP.setWorkflowInstanceId(client.getWorkflowInstanceId());
+            queryP.setNodeKey(client.getNodeKey());
+            queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
+            companyAiWorkflowExecLogMapper.updateCompanyAiWorkflowExecLog(queryP);
             return addItem;
         } else {
             // 加微失败
@@ -1071,6 +1078,15 @@ public class WxTaskService {
                     client.getRoboticId(), runParam);
 
             asyncSaveCompanyVoiceRoboticCallLog(addLog);
+
+            //更新工作流的执行日志
+            CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+
+            queryP.setWorkflowInstanceId(client.getWorkflowInstanceId());
+            queryP.setNodeKey(client.getNodeKey());
+            queryP.setStatus(ExecutionStatusEnum.FAILURE.getValue());
+            companyAiWorkflowExecLogMapper.updateCompanyAiWorkflowExecLog(queryP);
+
             return addItem;
         }
     }
@@ -1712,6 +1728,13 @@ public class WxTaskService {
                 .set(CompanyVoiceRoboticCallLogAddwx::getStatus, 2)
                 .update();
 
+        //更新工作流的执行日志
+//        CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+//        queryP.setWorkflowInstanceId(client.getWorkflowInstanceId());
+//        queryP.setNodeKey(client.getNodeKey());
+//        queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
+//        companyAiWorkflowExecLogMapper.updateCompanyAiWorkflowExecLog(queryP);
+
         client.setIsAdd(1);
         client.setAddTime(LocalDateTime.now());
         upClientList.add(client);