Procházet zdrojové kódy

sop记录记录异常未发送

xw před 1 měsícem
rodič
revize
7e79f7e7a8

+ 222 - 0
fs-company/src/main/java/com/fs/company/controller/qw/SopGenerationFailedLogController.java

@@ -0,0 +1,222 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.sop.domain.SopGenerationFailedLog;
+import com.fs.sop.service.ISopGenerationFailedLogService;
+import com.fs.sop.vo.SopGenerationFailedLogVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SOP生成失败日志Controller
+ * 用于查询被过滤的客户信息,支持补发操作
+ *
+ * @author fs
+ * @date 2026-01-23
+ */
+@RestController
+@RequestMapping("/qwSop/failedLog")
+public class SopGenerationFailedLogController extends BaseController {
+
+    @Autowired
+    private ISopGenerationFailedLogService failedLogService;
+
+    @Autowired
+    private TokenService tokenService;
+
+
+    private Long getCurrentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getUser().getCompanyId();
+    }
+
+    /**
+     * 查询SOP生成失败日志列表
+     *
+     * @param log 查询条件
+     * @return 失败日志列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(SopGenerationFailedLog log) {
+        log.setCompanyId(getCurrentCompanyId().toString());
+
+        startPage();
+        List<SopGenerationFailedLog> list = failedLogService.selectFailedLogList(log);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询失败日志列表(带客户名称等扩展信息)
+     * @param log 查询条件
+     * @return 失败日志VO列表
+     */
+    @GetMapping("/voList")
+    public TableDataInfo voList(SopGenerationFailedLog log) {
+        log.setCompanyId(getCurrentCompanyId().toString());
+
+        startPage();
+        List<SopGenerationFailedLogVO> list = failedLogService.selectFailedLogVOList(log);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据营期ID查询失败日志
+     *
+     * @param userLogsId 营期ID
+     * @return 失败日志列表
+     */
+    @GetMapping("/listByUserLogsId/{userLogsId}")
+    public AjaxResult listByUserLogsId(@PathVariable String userLogsId) {
+        SopGenerationFailedLog query = new SopGenerationFailedLog();
+        query.setUserLogsId(userLogsId);
+        query.setCompanyId(getCurrentCompanyId().toString());
+
+        List<SopGenerationFailedLog> list = failedLogService.selectFailedLogList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 根据SOP ID查询失败日志
+     *
+     * @param sopId SOP ID
+     * @return 失败日志列表
+     */
+    @GetMapping("/listBySopId/{sopId}")
+    public AjaxResult listBySopId(@PathVariable String sopId) {
+        SopGenerationFailedLog query = new SopGenerationFailedLog();
+        query.setSopId(sopId);
+        query.setCompanyId(getCurrentCompanyId().toString());
+
+        List<SopGenerationFailedLog> list = failedLogService.selectFailedLogList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 查询失败统计信息
+     *
+     * @param sopId SOP ID
+     * @param userLogsId 营期ID(可选)
+     * @return 统计信息
+     */
+    @GetMapping("/statistics")
+    public AjaxResult statistics(@RequestParam(required = false) String sopId,
+                                  @RequestParam(required = false) String userLogsId) {
+        SopGenerationFailedLog query = new SopGenerationFailedLog();
+        query.setSopId(sopId);
+        query.setUserLogsId(userLogsId);
+        query.setCompanyId(getCurrentCompanyId().toString());
+
+        List<SopGenerationFailedLog> list = failedLogService.selectFailedLogList(query);
+
+        Map<Integer, Long> typeCountMap = new HashMap<>();
+        Map<Integer, String> typeNameMap = new HashMap<>();
+
+        typeNameMap.put(1, "未注册小程序");
+        typeNameMap.put(2, "E级客户过滤");
+        typeNameMap.put(3, "数据缺失");
+        typeNameMap.put(4, "营期超期");
+        typeNameMap.put(5, "其他原因");
+
+        for (SopGenerationFailedLog log : list) {
+            Integer failType = log.getFailType();
+            typeCountMap.put(failType, typeCountMap.getOrDefault(failType, 0L) + 1);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("total", list.size());
+        result.put("notRetryCount", list.stream().filter(l -> l.getIsRetry() == 0).count());
+        result.put("retriedCount", list.stream().filter(l -> l.getIsRetry() == 1).count());
+        result.put("typeStatistics", typeCountMap);
+        result.put("typeNames", typeNameMap);
+
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取失败日志详情
+     *
+     * @param id 主键ID
+     * @return 失败日志详情
+     */
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(failedLogService.selectFailedLogById(id));
+    }
+
+    /**
+     * 标记为已补发
+     *
+     * @param id 主键ID
+     * @param retryBy 补发人
+     * @return 操作结果
+     */
+    @PutMapping("/markRetried/{id}")
+    public AjaxResult markRetried(@PathVariable Long id,
+                                   @RequestParam(required = false) String retryBy) {
+        if (retryBy == null || retryBy.trim().isEmpty()) {
+            retryBy = getUsername();
+        }
+
+        int rows = failedLogService.updateRetryStatus(id, retryBy);
+        return toAjax(rows);
+    }
+
+    /**
+     * 批量标记为已补发
+     *
+     * @param ids 主键ID数组
+     * @param retryBy 补发人
+     * @return 操作结果
+     */
+    @PutMapping("/batchMarkRetried")
+    public AjaxResult batchMarkRetried(@RequestParam Long[] ids,
+                                        @RequestParam(required = false) String retryBy) {
+        if (retryBy == null || retryBy.trim().isEmpty()) {
+            retryBy = getUsername();
+        }
+
+        int successCount = 0;
+        for (Long id : ids) {
+            try {
+                int rows = failedLogService.updateRetryStatus(id, retryBy);
+                if (rows > 0) {
+                    successCount++;
+                }
+            } catch (Exception e) {
+                logger.error("标记补发状态失败: id={}, error={}", id, e.getMessage(), e);
+            }
+        }
+
+        return AjaxResult.success("成功标记" + successCount + "条记录为已补发");
+    }
+
+    /**
+     *
+     * @param sopId SOP ID
+     * @param userLogsId 营期ID
+     * @return 未补发的失败日志列表
+     */
+    @GetMapping("/listNotRetried")
+    public TableDataInfo listNotRetried(@RequestParam(required = false) String sopId,
+                                        @RequestParam(required = false) String userLogsId) {
+        startPage();
+        SopGenerationFailedLog query = new SopGenerationFailedLog();
+        query.setSopId(sopId);
+        query.setUserLogsId(userLogsId);
+        query.setIsRetry(0); // 未补发
+        query.setCompanyId(getCurrentCompanyId().toString());
+
+        List<SopGenerationFailedLog> list = failedLogService.selectFailedLogList(query);
+        return getDataTable(list);
+    }
+}

+ 213 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -39,6 +39,7 @@ import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempRulesService;
 import com.fs.sop.service.IQwSopTempVoiceService;
+import com.fs.sop.service.ISopGenerationFailedLogService;
 import com.fs.sop.vo.QwCreateLinkByAppVO;
 import com.fs.sop.vo.SopUserLogsVo;
 import com.fs.system.service.ISysConfigService;
@@ -193,6 +194,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     LiveWatchLogMapper liveWatchLogMapper;
 
+    @Autowired
+    private ISopGenerationFailedLogService sopGenerationFailedLogService;
+
     @PostConstruct
     public void init() {
         loadCourseConfig();
@@ -695,6 +699,24 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                             if (!externalContactIdList.isEmpty()) {
                                 List<QwExternalContact> list = qwExternalContactService.list(new QueryWrapper<QwExternalContact>().isNotNull("fs_user_id").in("id", externalContactIdList));
                                 Map<Long, QwExternalContact> map = PubFun.listToMapByGroupObject(list, QwExternalContact::getId);
+//
+//                                // 记录被过滤的客户(未注册小程序)
+//                                List<SopUserLogsInfo> filteredCustomers = sopUserLogsInfos.stream()
+//                                    .filter(e -> !map.containsKey(e.getExternalId()))
+//                                    .collect(Collectors.toList());
+//
+//                                if (!filteredCustomers.isEmpty()) {
+//                                    try {
+//                                        String dayFormattedSendTime = OUTPUT_FORMATTER.format(targetDate);
+//                                        recordFailedCustomers(filteredCustomers, logVo, day, dayFormattedSendTime, content.getTime(),
+//                                            SopGenerationFailedLog.FailType.NOT_REGISTERED, "未注册小程序(isRegister=1过滤)",
+//                                            qwUserId, qwUserByRedis.getQwUserName(), companyUserId, companyId, logVo.getCorpId());
+//                                    } catch (Exception e) {
+//                                        log.error("记录未注册客户失败: sopId={}, userLogsId={}, error={}",
+//                                            logVo.getSopId(), logVo.getId(), e.getMessage(), e);
+//                                    }
+//                                }
+
                                 sopUserLogsInfos = sopUserLogsInfos.stream().filter(e -> map.containsKey(e.getExternalId())).collect(Collectors.toList());
                             }
                         }
@@ -844,6 +866,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 //            }
         } else {
             // 处理每个 externalContactId
+            // 记录所有被跳过的客户
+            List<SopUserLogsInfo> skippedCustomers = new ArrayList<>();
+
             sopUserLogsInfos.forEach(contactId -> {
                 try {
                     // 空值检查
@@ -853,10 +878,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     }
                     if (contactId.getExternalId() == null) {
                         log.error("contactId.getExternalId() 为 null,contactId: {}", contactId);
+                        skippedCustomers.add(contactId);
                         return;
                     }
                     if (contactId.getExternalContactId() == null) {
                         log.error("contactId.getExternalContactId() 为 null,contactId: {}", contactId);
+                        skippedCustomers.add(contactId);
                         return;
                     }
 
@@ -871,8 +898,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 } catch (Exception e) {
                     e.printStackTrace();
                     log.error("处理 externalContactId {} 时发生异常", contactId, e);
+                    if (contactId != null) {
+                        skippedCustomers.add(contactId);
+                    }
                 }
             });
+
+            // 批量记录被跳过的客户
+            if (!skippedCustomers.isEmpty()) {
+                try {
+                    recordFailedCustomersInBatch(skippedCustomers, logVo, formattedSendTime, content.getTime(),
+                        qwUserId, qwUserName, companyUserId, companyId, logVo.getCorpId());
+                } catch (Exception e) {
+                    log.error("批量记录跳过客户失败: sopId={}, userLogsId={}, error={}",
+                        logVo.getSopId(), logVo.getId(), e.getMessage(), e);
+                }
+            }
         }
 //        // 处理每个 externalContactId
 //        sopUserLogsInfos.forEach(contactId -> {
@@ -2738,4 +2779,176 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     private boolean isValidExternalContact(QwExternalContact externalContact) {
         return externalContact.getStatus() == 0 || externalContact.getStatus() == 2 || externalContact.getStatus() == 3;
     }
+
+    /**
+     * 批量记录被过滤/跳过的客户
+     *
+     * @param customers 被过滤的客户列表
+     * @param logVo SOP营期信息
+     * @param sendTime 发送时间
+     * @param elementTime 元素时间
+     * @param qwUserId 企微员工ID
+     * @param qwUserName 企微员工名称
+     * @param companyUserId 公司员工ID
+     * @param companyId 公司ID
+     * @param corpId 企微主体ID
+     */
+    private void recordFailedCustomersInBatch(List<SopUserLogsInfo> customers,
+                                              SopUserLogsVo logVo,
+                                              String sendTime,
+                                              String elementTime,
+                                              String qwUserId,
+                                              String qwUserName,
+                                              String companyUserId,
+                                              String companyId,
+                                              String corpId) {
+        if (customers == null || customers.isEmpty()) {
+            return;
+        }
+
+        List<SopGenerationFailedLog> failedLogs = new ArrayList<>();
+
+        for (SopUserLogsInfo customer : customers) {
+            try {
+                SopGenerationFailedLog failedLog = new SopGenerationFailedLog();
+
+                failedLog.setSopId(logVo.getSopId());
+                failedLog.setUserLogsId(logVo.getId());
+                failedLog.setExternalId(customer.getExternalId());
+                failedLog.setFsUserId(customer.getFsUserId());
+
+                failedLog.setQwUserId(qwUserId);
+                failedLog.setQwUserName(qwUserName);
+                failedLog.setCompanyUserId(companyUserId);
+                failedLog.setCompanyId(companyId);
+                failedLog.setCorpId(corpId);
+
+                SopGenerationFailedLog.FailType failType;
+                String failReason;
+
+                if (customer.getExternalId() == null) {
+                    failType = SopGenerationFailedLog.FailType.DATA_MISSING;
+                    failReason = "客户ID(externalId)为空";
+                } else if (customer.getExternalContactId() == null) {
+                    failType = SopGenerationFailedLog.FailType.DATA_MISSING;
+                    failReason = "企微客户ID(externalContactId)为空";
+                } else if (Integer.valueOf(1).equals(customer.getIsDaysNotStudy())) {
+                    failType = SopGenerationFailedLog.FailType.E_LEVEL_FILTERED;
+                    failReason = "E级客户过滤(非官方群发且isDaysNotStudy=1)";
+                } else {
+                    failType = SopGenerationFailedLog.FailType.OTHER;
+                    failReason = "其他原因跳过处理";
+                }
+
+                failedLog.setFailType(failType.getCode());
+                String fsUserIdStr = customer.getFsUserId() == null ? "NULL" : customer.getFsUserId().toString();
+                failedLog.setFailReason(failReason + " (fs_user_id=" + fsUserIdStr + ")");
+
+                // 补发状态
+                failedLog.setIsRetry(SopGenerationFailedLog.RetryStatus.NOT_RETRY.getCode());
+
+                // 任务信息
+                failedLog.setDayNum(0L); // 这里没有day信息,设为0
+                failedLog.setSendTime(sendTime);
+                failedLog.setElementTime(elementTime);
+
+                failedLogs.add(failedLog);
+            } catch (Exception e) {
+                log.error("构建失败日志对象失败: externalId={}, error={}",
+                        customer.getExternalId(), e.getMessage(), e);
+            }
+        }
+
+        // 批量插入
+        if (!failedLogs.isEmpty()) {
+            try {
+                int count = sopGenerationFailedLogService.batchInsert(failedLogs);
+                log.info("批量记录SOP生成跳过日志成功: sopId={}, userLogsId={}, count={}",
+                        logVo.getSopId(), logVo.getId(), count);
+            } catch (Exception e) {
+                log.error("批量插入跳过日志失败: sopId={}, userLogsId={}, error={}",
+                        logVo.getSopId(), logVo.getId(), e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * 记录被过滤的客户
+     *
+     * @param filteredCustomers 被过滤的客户列表
+     * @param logVo SOP营期信息
+     * @param currentDay 当前天数
+     * @param sendTime 发送时间
+     * @param elementTime 元素时间
+     * @param failType 失败类型
+     * @param failReasonPrefix 失败原因前缀
+     * @param qwUserId 企微员工ID(发送人)
+     * @param qwUserName 企微员工名称(发送人)
+     * @param companyUserId 公司员工ID
+     * @param companyId 公司ID
+     * @param corpId 企微主体ID
+     */
+    private void recordFailedCustomers(List<SopUserLogsInfo> filteredCustomers,
+                                       SopUserLogsVo logVo,
+                                       long currentDay,
+                                       String sendTime,
+                                       String elementTime,
+                                       SopGenerationFailedLog.FailType failType,
+                                       String failReasonPrefix,
+                                       String qwUserId,
+                                       String qwUserName,
+                                       String companyUserId,
+                                       String companyId,
+                                       String corpId) {
+        if (filteredCustomers == null || filteredCustomers.isEmpty()) {
+            return;
+        }
+
+        List<SopGenerationFailedLog> failedLogs = new ArrayList<>();
+
+        for (SopUserLogsInfo customer : filteredCustomers) {
+            try {
+                SopGenerationFailedLog failedLog = new SopGenerationFailedLog();
+
+                failedLog.setSopId(logVo.getSopId());
+                failedLog.setUserLogsId(logVo.getId());
+                failedLog.setExternalId(customer.getExternalId());
+                failedLog.setFsUserId(customer.getFsUserId());
+
+                failedLog.setQwUserId(qwUserId);
+                failedLog.setQwUserName(qwUserName);
+                failedLog.setCompanyUserId(companyUserId);
+                failedLog.setCompanyId(companyId);
+                failedLog.setCorpId(corpId);
+
+                failedLog.setFailType(failType.getCode());
+                String fsUserIdStr = customer.getFsUserId() == null ? "NULL" : customer.getFsUserId().toString();
+                failedLog.setFailReason(failReasonPrefix + " (fs_user_id=" + fsUserIdStr + ")");
+
+
+                failedLog.setIsRetry(SopGenerationFailedLog.RetryStatus.NOT_RETRY.getCode());
+
+
+                failedLog.setDayNum(currentDay);
+                failedLog.setSendTime(sendTime);
+                failedLog.setElementTime(elementTime);
+
+                failedLogs.add(failedLog);
+            } catch (Exception e) {
+                log.error("构建失败日志对象失败: externalId={}, error={}",
+                        customer.getExternalId(), e.getMessage(), e);
+            }
+        }
+
+        if (!failedLogs.isEmpty()) {
+            try {
+                int count = sopGenerationFailedLogService.batchInsert(failedLogs);
+                log.info("记录SOP生成失败日志成功: sopId={}, userLogsId={}, qwUserId={}, qwUserName={}, failType={}, count={}",
+                        logVo.getSopId(), logVo.getId(), qwUserId, qwUserName, failType.getDescription(), count);
+            } catch (Exception e) {
+                log.error("批量插入失败日志失败: sopId={}, userLogsId={}, error={}",
+                        logVo.getSopId(), logVo.getId(), e.getMessage(), e);
+            }
+        }
+    }
 }

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

@@ -129,4 +129,6 @@ public class FsStorePaymentScrm extends BaseEntity
     /** 退款审核备注 */
     @Excel(name = "退款审核备注")
     private String refundAuditRemark;
+
+    private Long  merConfigId;
 }

+ 176 - 0
fs-service/src/main/java/com/fs/sop/domain/SopGenerationFailedLog.java

@@ -0,0 +1,176 @@
+package com.fs.sop.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * SOP生成失败日志实体类
+ * 记录在SOP任务生成过程中被过滤掉的客户信息
+ */
+@Data
+@TableName("sop_generation_failed_log")
+public class SopGenerationFailedLog implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * SOP ID
+     */
+    private String sopId;
+
+    /**
+     * 营期ID
+     */
+    private String userLogsId;
+
+    /**
+     * 客户ID
+     */
+    private Long externalId;
+
+    /**
+     * 小程序用户ID
+     */
+    private Long fsUserId;
+
+    /**
+     * 企微员工ID(发送人)
+     */
+    private String qwUserId;
+
+    /**
+     * 企微员工名称(发送人)
+     */
+    private String qwUserName;
+
+    /**
+     * 公司员工ID
+     */
+    private String companyUserId;
+
+    /**
+     * 公司ID
+     */
+    private String companyId;
+
+    /**
+     * 企微主体ID(corpid)
+     */
+    private String corpId;
+
+    /**
+     * 失败类型: 1-未注册 2-E级客户 3-数据缺失 4-营期超期 5-其他
+     */
+    private Integer failType;
+
+    /**
+     * 失败原因
+     */
+    private String failReason;
+
+    /**
+     * 是否已补发: 0-未补发 1-已补发
+     */
+    private Integer isRetry;
+
+    /**
+     * 补发时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date retryTime;
+
+    /**
+     * 补发人
+     */
+    private String retryBy;
+
+    /**
+     * 第几天
+     */
+    private Long dayNum;
+
+    /**
+     * 原计划发送时间
+     */
+    private String sendTime;
+
+    /**
+     * 模板时间点
+     */
+    private String elementTime;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 失败类型枚举
+     */
+    public enum FailType {
+        NOT_REGISTERED(1, "未注册小程序"),
+        E_LEVEL_FILTERED(2, "E级客户过滤"),
+        DATA_MISSING(3, "数据缺失"),
+        PERIOD_EXPIRED(4, "营期超期"),
+        OTHER(5, "其他原因");
+
+        private final Integer code;
+        private final String description;
+
+        FailType(Integer code, String description) {
+            this.code = code;
+            this.description = description;
+        }
+
+        public Integer getCode() {
+            return code;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+    }
+
+    /**
+     * 补发状态枚举
+     */
+    public enum RetryStatus {
+        NOT_RETRY(0, "未补发"),
+        RETRIED(1, "已补发");
+
+        private final Integer code;
+        private final String description;
+
+        RetryStatus(Integer code, String description) {
+            this.code = code;
+            this.description = description;
+        }
+
+        public Integer getCode() {
+            return code;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/sop/mapper/SopGenerationFailedLogMapper.java

@@ -0,0 +1,57 @@
+package com.fs.sop.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.sop.domain.SopGenerationFailedLog;
+import com.fs.sop.vo.SopGenerationFailedLogVO;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * SOP生成失败日志Mapper接口
+ */
+@Repository
+public interface SopGenerationFailedLogMapper extends BaseMapper<SopGenerationFailedLog> {
+
+    /**
+     * 批量插入失败日志
+     *
+     * @param logs 失败日志列表
+     * @return 插入数量
+     */
+    int batchInsert(@Param("logs") List<SopGenerationFailedLog> logs);
+
+    /**
+     * 查询失败日志列表
+     *
+     * @param log 查询条件
+     * @return 失败日志列表
+     */
+    List<SopGenerationFailedLog> selectFailedLogList(SopGenerationFailedLog log);
+
+    /**
+     * 查询失败日志列表(带客户名称等扩展信息)
+     *
+     * @param log 查询条件
+     * @return 失败日志VO列表
+     */
+    List<SopGenerationFailedLogVO> selectFailedLogVOList(SopGenerationFailedLog log);
+
+    /**
+     * 根据ID查询失败日志
+     *
+     * @param id 主键ID
+     * @return 失败日志
+     */
+    SopGenerationFailedLog selectFailedLogById(@Param("id") Long id);
+
+    /**
+     * 更新补发状态
+     *
+     * @param id 主键ID
+     * @param retryBy 补发人
+     * @return 更新数量
+     */
+    int updateRetryStatus(@Param("id") Long id, @Param("retryBy") String retryBy);
+}

+ 53 - 0
fs-service/src/main/java/com/fs/sop/service/ISopGenerationFailedLogService.java

@@ -0,0 +1,53 @@
+package com.fs.sop.service;
+
+import com.fs.sop.domain.SopGenerationFailedLog;
+import com.fs.sop.vo.SopGenerationFailedLogVO;
+
+import java.util.List;
+
+/**
+ * SOP生成失败日志Service接口
+ */
+public interface ISopGenerationFailedLogService {
+
+    /**
+     * 批量插入失败日志
+     *
+     * @param logs 失败日志列表
+     * @return 插入数量
+     */
+    int batchInsert(List<SopGenerationFailedLog> logs);
+
+    /**
+     * 查询失败日志列表
+     *
+     * @param log 查询条件
+     * @return 失败日志列表
+     */
+    List<SopGenerationFailedLog> selectFailedLogList(SopGenerationFailedLog log);
+
+    /**
+     * 查询失败日志列表(带客户名称等扩展信息)
+     *
+     * @param log 查询条件
+     * @return 失败日志VO列表
+     */
+    List<SopGenerationFailedLogVO> selectFailedLogVOList(SopGenerationFailedLog log);
+
+    /**
+     * 根据ID查询失败日志
+     *
+     * @param id 主键ID
+     * @return 失败日志
+     */
+    SopGenerationFailedLog selectFailedLogById(Long id);
+
+    /**
+     * 更新补发状态
+     *
+     * @param id 主键ID
+     * @param retryBy 补发人
+     * @return 更新数量
+     */
+    int updateRetryStatus(Long id, String retryBy);
+}

+ 48 - 0
fs-service/src/main/java/com/fs/sop/service/impl/SopGenerationFailedLogServiceImpl.java

@@ -0,0 +1,48 @@
+package com.fs.sop.service.impl;
+
+import com.fs.sop.domain.SopGenerationFailedLog;
+import com.fs.sop.mapper.SopGenerationFailedLogMapper;
+import com.fs.sop.service.ISopGenerationFailedLogService;
+import com.fs.sop.vo.SopGenerationFailedLogVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * SOP生成失败日志Service实现类
+ */
+@Service
+public class SopGenerationFailedLogServiceImpl implements ISopGenerationFailedLogService {
+
+    @Autowired
+    private SopGenerationFailedLogMapper failedLogMapper;
+
+    @Override
+    public int batchInsert(List<SopGenerationFailedLog> logs) {
+        if (logs == null || logs.isEmpty()) {
+            return 0;
+        }
+        return failedLogMapper.batchInsert(logs);
+    }
+
+    @Override
+    public List<SopGenerationFailedLog> selectFailedLogList(SopGenerationFailedLog log) {
+        return failedLogMapper.selectFailedLogList(log);
+    }
+
+    @Override
+    public List<SopGenerationFailedLogVO> selectFailedLogVOList(SopGenerationFailedLog log) {
+        return failedLogMapper.selectFailedLogVOList(log);
+    }
+
+    @Override
+    public SopGenerationFailedLog selectFailedLogById(Long id) {
+        return failedLogMapper.selectFailedLogById(id);
+    }
+
+    @Override
+    public int updateRetryStatus(Long id, String retryBy) {
+        return failedLogMapper.updateRetryStatus(id, retryBy);
+    }
+}

+ 177 - 0
fs-service/src/main/java/com/fs/sop/vo/SopGenerationFailedLogVO.java

@@ -0,0 +1,177 @@
+package com.fs.sop.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * SOP生成失败日志VO
+ * 用于前端展示,包含客户名称等扩展信息
+ */
+@Data
+public class SopGenerationFailedLogVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * SOP ID
+     */
+    private String sopId;
+
+    /**
+     * SOP名称
+     */
+    private String sopName;
+
+    /**
+     * 营期ID
+     */
+    private String userLogsId;
+
+    /**
+     * 营期开始时间
+     */
+    private String startTime;
+
+    /**
+     * 客户ID
+     */
+    private Long externalId;
+
+    /**
+     * 客户名称
+     */
+    private String externalUserName;
+
+    /**
+     * 企微用户ID
+     */
+    private String externalUserId;
+
+    /**
+     * 小程序用户ID
+     */
+    private Long fsUserId;
+
+    /**
+     * 企微员工ID(发送人)
+     */
+    private String qwUserId;
+
+    /**
+     * 企微员工名称(发送人)
+     */
+    private String qwUserName;
+
+    /**
+     * 公司员工ID
+     */
+    private String companyUserId;
+
+    /**
+     * 公司ID
+     */
+    private String companyId;
+
+    /**
+     * 企微主体ID
+     */
+    private String corpId;
+
+    /**
+     * 失败类型: 1-未注册 2-E级客户 3-数据缺失 4-营期超期 5-其他
+     */
+    private Integer failType;
+
+    /**
+     * 失败类型名称
+     */
+    private String failTypeName;
+
+    /**
+     * 失败原因
+     */
+    private String failReason;
+
+    /**
+     * 是否已补发: 0-未补发 1-已补发
+     */
+    private Integer isRetry;
+
+    /**
+     * 补发状态名称
+     */
+    private String retryStatusName;
+
+    /**
+     * 补发时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date retryTime;
+
+    /**
+     * 补发人
+     */
+    private String retryBy;
+
+    /**
+     * 第几天
+     */
+    private Long dayNum;
+
+    /**
+     * 原计划发送时间
+     */
+    private String sendTime;
+
+    /**
+     * 模板时间点
+     */
+    private String elementTime;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 获取失败类型名称
+     */
+    public String getFailTypeName() {
+        if (failType == null) {
+            return "未知";
+        }
+        switch (failType) {
+            case 1: return "未注册小程序";
+            case 2: return "E级客户过滤";
+            case 3: return "数据缺失";
+            case 4: return "营期超期";
+            case 5: return "其他原因";
+            default: return "未知";
+        }
+    }
+
+    /**
+     * 获取补发状态名称
+     */
+    public String getRetryStatusName() {
+        if (isRetry == null) {
+            return "未补发";
+        }
+        return isRetry == 1 ? "已补发" : "未补发";
+    }
+}

+ 159 - 0
fs-service/src/main/resources/mapper/sop/SopGenerationFailedLogMapper.xml

@@ -0,0 +1,159 @@
+<?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.sop.mapper.SopGenerationFailedLogMapper">
+
+    <resultMap type="com.fs.sop.domain.SopGenerationFailedLog" id="SopGenerationFailedLogResult">
+        <id property="id" column="id"/>
+        <result property="sopId" column="sop_id"/>
+        <result property="userLogsId" column="user_logs_id"/>
+        <result property="externalId" column="external_id"/>
+        <result property="fsUserId" column="fs_user_id"/>
+        <result property="qwUserId" column="qw_user_id"/>
+        <result property="qwUserName" column="qw_user_name"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="corpId" column="corp_id"/>
+        <result property="failType" column="fail_type"/>
+        <result property="failReason" column="fail_reason"/>
+        <result property="isRetry" column="is_retry"/>
+        <result property="retryTime" column="retry_time"/>
+        <result property="retryBy" column="retry_by"/>
+        <result property="dayNum" column="day_num"/>
+        <result property="sendTime" column="send_time"/>
+        <result property="elementTime" column="element_time"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectSopGenerationFailedLogVo">
+        select id, sop_id, user_logs_id, external_id, fs_user_id, qw_user_id, qw_user_name, 
+               company_user_id, company_id, corp_id, fail_type, fail_reason, 
+               is_retry, retry_time, retry_by, day_num, send_time, element_time, create_time, update_time
+        from fs_his_sop.sop_generation_failed_log
+    </sql>
+
+    <!-- 批量插入 -->
+    <insert id="batchInsert" parameterType="java.util.List">
+        insert into fs_his_sop.sop_generation_failed_log
+        (sop_id, user_logs_id, external_id, fs_user_id, qw_user_id, qw_user_name, 
+         company_user_id, company_id, corp_id, fail_type, fail_reason, 
+         is_retry, day_num, send_time, element_time, create_time, update_time)
+        values
+        <foreach collection="logs" item="log" separator=",">
+            (#{log.sopId}, #{log.userLogsId}, #{log.externalId}, #{log.fsUserId}, 
+             #{log.qwUserId}, #{log.qwUserName}, #{log.companyUserId}, #{log.companyId}, #{log.corpId},
+             #{log.failType}, #{log.failReason}, #{log.isRetry}, #{log.dayNum}, 
+             #{log.sendTime}, #{log.elementTime}, now(), now())
+        </foreach>
+    </insert>
+
+    <!-- 查询失败日志列表 -->
+    <select id="selectFailedLogList" parameterType="com.fs.sop.domain.SopGenerationFailedLog" 
+            resultMap="SopGenerationFailedLogResult">
+        <include refid="selectSopGenerationFailedLogVo"/>
+        <where>
+            <if test="sopId != null and sopId != ''">
+                and sop_id = #{sopId}
+            </if>
+            <if test="userLogsId != null and userLogsId != ''">
+                and user_logs_id = #{userLogsId}
+            </if>
+            <if test="externalId != null">
+                and external_id = #{externalId}
+            </if>
+            <if test="failType != null">
+                and fail_type = #{failType}
+            </if>
+            <if test="isRetry != null">
+                and is_retry = #{isRetry}
+            </if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <!-- 根据ID查询 -->
+    <select id="selectFailedLogById" parameterType="Long" resultMap="SopGenerationFailedLogResult">
+        <include refid="selectSopGenerationFailedLogVo"/>
+        where id = #{id}
+    </select>
+
+    <!-- 更新补发状态 -->
+    <update id="updateRetryStatus">
+        update fs_his_sop.sop_generation_failed_log
+        set is_retry = 1,
+            retry_time = now(),
+            retry_by = #{retryBy},
+            update_time = now()
+        where id = #{id}
+    </update>
+
+    <!-- VO结果集映射 -->
+    <resultMap type="com.fs.sop.vo.SopGenerationFailedLogVO" id="SopGenerationFailedLogVOResult">
+        <id property="id" column="id"/>
+        <result property="sopId" column="sop_id"/>
+        <result property="sopName" column="sop_name"/>
+        <result property="userLogsId" column="user_logs_id"/>
+        <result property="startTime" column="start_time"/>
+        <result property="externalId" column="external_id"/>
+        <result property="externalUserName" column="external_user_name"/>
+        <result property="externalUserId" column="external_user_id"/>
+        <result property="fsUserId" column="fs_user_id"/>
+        <result property="qwUserId" column="qw_user_id"/>
+        <result property="qwUserName" column="qw_user_name"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="corpId" column="corp_id"/>
+        <result property="failType" column="fail_type"/>
+        <result property="failReason" column="fail_reason"/>
+        <result property="isRetry" column="is_retry"/>
+        <result property="retryTime" column="retry_time"/>
+        <result property="retryBy" column="retry_by"/>
+        <result property="dayNum" column="day_num"/>
+        <result property="sendTime" column="send_time"/>
+        <result property="elementTime" column="element_time"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <!-- 查询失败日志列表(带客户名称等扩展信息) -->
+    <select id="selectFailedLogVOList" parameterType="com.fs.sop.domain.SopGenerationFailedLog" 
+            resultMap="SopGenerationFailedLogVOResult">
+        select 
+            l.id, l.sop_id, l.user_logs_id, l.external_id, l.fs_user_id, 
+            l.qw_user_id, l.qw_user_name, l.company_user_id, l.company_id, l.corp_id,
+            l.fail_type, l.fail_reason, l.is_retry, l.retry_time, l.retry_by, 
+            l.day_num, l.send_time, l.element_time, l.create_time, l.update_time,
+            s.sop_name as sop_name,
+            ul.start_time as start_time,
+            ec.name as external_user_name,
+            ec.external_userid as external_user_id
+        from fs_his_sop.sop_generation_failed_log l
+        left join fs_his_sop.qw_sop s on l.sop_id = s.id
+        left join fs_his_sop.sop_user_logs ul on l.user_logs_id = ul.id
+        left join fs_his.qw_external_contact ec on l.external_id = ec.id
+        <where>
+            <if test="sopId != null and sopId != ''">
+                and l.sop_id = #{sopId}
+            </if>
+            <if test="userLogsId != null and userLogsId != ''">
+                and l.user_logs_id = #{userLogsId}
+            </if>
+            <if test="externalId != null">
+                and l.external_id = #{externalId}
+            </if>
+            <if test="qwUserId != null and qwUserId != ''">
+                and l.qw_user_id = #{qwUserId}
+            </if>
+            <if test="failType != null">
+                and l.fail_type = #{failType}
+            </if>
+            <if test="isRetry != null">
+                and l.is_retry = #{isRetry}
+            </if>
+        </where>
+        order by l.create_time desc
+    </select>
+
+</mapper>