Commit 68cda857 by jianan

保费到期提醒1

parent dd86ab31
......@@ -80,6 +80,12 @@
<artifactId>hutool-all</artifactId>
</dependency>
<!-- 邮件发送:续期提醒等内部通知,直连 SMTP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.yd</groupId>
<artifactId>yd-question-feign</artifactId>
......@@ -103,6 +109,16 @@
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
......
package com.yd.csf.api.handler;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.yd.csf.api.service.RenewalDispatchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 保单续期提醒月度任务处理器。
* 默认每月 1 号由 XXL-Job Admin 触发(cron 在 Admin 维护);
* 支持手动传参 bizMonth=yyyy-MM 补发指定月份。
*/
@Component
@Slf4j
public class RenewalRemindJobHandler {
@Resource
private RenewalDispatchService renewalDispatchService;
@XxlJob("renewalRemindJobHandler")
public void execute() {
String param = com.xxl.job.core.context.XxlJobHelper.getJobParam();
log.info("开始保单续期提醒任务,参数: {}", param);
renewalDispatchService.dispatch(param);
}
}
package com.yd.csf.api.service;
/**
* 续期提醒月度跑批编排:筛选 → 生成清单 → 发邮件 → 批次落库
*/
public interface RenewalDispatchService {
/**
* 执行一次续期提醒跑批
*
* @param bizMonth 执行月参数(yyyy-MM 或 bizMonth=yyyy-MM),为空时取当前月
*/
void dispatch(String bizMonth);
}
package com.yd.csf.api.service;
import java.util.List;
/**
* 续期提醒邮件发送。
* 一期为占位实现,后续替换为公司统一邮件服务(yd-email-api)。
*/
public interface RenewalMailService {
/**
* 发送带 Excel 附件的邮件
*
* @param recipients 收件人邮箱列表
* @param subject 邮件主题
* @param content 邮件正文
* @param fileName 附件文件名
* @param attachment 附件字节内容
*/
void sendWithAttachment(List<String> recipients, String subject, String content,
String fileName, byte[] attachment);
}
package com.yd.csf.api.service.impl;
import com.yd.common.exception.BusinessException;
import com.yd.common.utils.RandomStringGenerator;
import com.yd.csf.api.service.RenewalDispatchService;
import com.yd.csf.api.service.RenewalMailService;
import com.yd.csf.service.component.RenewalPolicyReportService;
import com.yd.csf.service.dto.RenewalReminderData;
import com.yd.csf.service.enums.RenewalRemindStatusEnum;
import com.yd.csf.service.model.RenewalRemindBatch;
import com.yd.csf.service.service.IRenewalRemindBatchService;
import com.yd.csf.service.service.RenewalReminderQueryService;
import com.yd.framework.config.LockExecutor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 续期提醒月度跑批编排实现
*/
@Service
@Slf4j
public class RenewalDispatchServiceImpl implements RenewalDispatchService {
private static final String LOCK_KEY_PREFIX = "renewal:remind:";
private static final String BATCH_BIZ_ID_PREFIX = "RR";
private static final int ERROR_MSG_MAX_LEN = 1900;
@Value("${renewal.reminder.recipients:}")
private String recipientsConfig;
@Resource
private LockExecutor lockExecutor;
@Resource
private RenewalReminderQueryService renewalReminderQueryService;
@Resource
private RenewalPolicyReportService renewalPolicyReportService;
@Resource
private RenewalMailService renewalMailService;
@Resource
private IRenewalRemindBatchService renewalRemindBatchService;
@Override
public void dispatch(String bizMonthParam) {
YearMonth execMonth = parseExecMonth(bizMonthParam);
String bizMonth = execMonth.toString();
lockExecutor.executeWithLock(LOCK_KEY_PREFIX + bizMonth, () -> doDispatch(execMonth));
}
private void doDispatch(YearMonth execMonth) {
String bizMonth = execMonth.toString();
RenewalRemindBatch existBatch = renewalRemindBatchService.queryByBizMonth(bizMonth);
if (existBatch != null
&& RenewalRemindStatusEnum.SUCCESS.getItemValue().equals(existBatch.getStatus())) {
log.info("续期提醒当月已成功发送,跳过。bizMonth={}", bizMonth);
return;
}
RenewalReminderData data = null;
String fileName = null;
String recipientsSnapshot = null;
try {
LocalDate today = LocalDate.now();
data = renewalReminderQueryService.build(execMonth, today);
// 窗口内无到期保单:不发邮件,记 EMPTY
if (CollectionUtils.isEmpty(data.getDuePolicies())) {
saveBatch(existBatch, execMonth, data, null,
RenewalRemindStatusEnum.EMPTY, null, null);
log.info("续期提醒窗口内无到期保单,已记 EMPTY。bizMonth={}", bizMonth);
return;
}
List<String> recipients = parseRecipients();
recipientsSnapshot = String.join(",", recipients);
fileName = renewalPolicyReportService.buildFileName(data, today);
byte[] attachment = renewalPolicyReportService.generate(data);
String subject = buildSubject(data);
String content = buildContent(data);
renewalMailService.sendWithAttachment(recipients, subject, content, fileName, attachment);
saveBatch(existBatch, execMonth, data, fileName,
RenewalRemindStatusEnum.SUCCESS, null, recipientsSnapshot);
log.info("续期提醒发送完成。bizMonth={}, 保单数={}",
bizMonth, data.getDuePolicies().size());
} catch (Exception e) {
String errorMsg = StringUtils.substring(e.getMessage(), 0, ERROR_MSG_MAX_LEN);
saveBatch(existBatch, execMonth, data, fileName,
RenewalRemindStatusEnum.FAIL, errorMsg, recipientsSnapshot);
log.error("续期提醒跑批失败。bizMonth={}", bizMonth, e);
throw new BusinessException("续期提醒跑批失败: " + e.getMessage());
}
}
private void saveBatch(RenewalRemindBatch exist, YearMonth execMonth, RenewalReminderData data,
String fileName, RenewalRemindStatusEnum status,
String errorMsg, String recipients) {
RenewalRemindBatch batch = exist == null ? new RenewalRemindBatch() : exist;
if (exist == null) {
batch.setBatchBizId(RandomStringGenerator.generateBizId16(BATCH_BIZ_ID_PREFIX));
batch.setBizMonth(execMonth.toString());
batch.setCreateTime(LocalDateTime.now());
}
// 查询阶段就失败时 data 可能为空,窗口由执行月推导
batch.setWindowStart(data != null ? data.getWindowStart() : execMonth.plusMonths(1).atDay(1));
batch.setWindowEnd(data != null ? data.getWindowEnd() : execMonth.plusMonths(2).atEndOfMonth());
batch.setPolicyCount(data != null ? data.getDuePolicies().size() : 0);
batch.setSkippedCount(data != null && data.getSkipped() != null ? data.getSkipped().size() : 0);
batch.setRecipients(recipients);
batch.setFileName(fileName);
batch.setStatus(status.getItemValue());
batch.setErrorMsg(errorMsg);
if (status != RenewalRemindStatusEnum.FAIL) {
batch.setSendTime(LocalDateTime.now());
}
batch.setUpdateTime(LocalDateTime.now());
renewalRemindBatchService.saveOrUpdate(batch);
}
private List<String> parseRecipients() {
List<String> recipients = Arrays.stream(StringUtils.defaultString(recipientsConfig).split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(recipients)) {
throw new BusinessException("续期提醒收件人未配置,请在配置中设置 renewal.reminder.recipients");
}
return recipients;
}
private String buildSubject(RenewalReminderData data) {
return String.format("【续期提醒】%d年%d-%d月到期保单清单(共%d张)",
data.getWindowStart().getYear(),
data.getWindowStart().getMonthValue(),
data.getWindowEnd().getMonthValue(),
data.getDuePolicies().size());
}
private String buildContent(RenewalReminderData data) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("以下保单将于 %s 至 %s 到期应缴续期保费,共 %d 张,详见附件。",
data.getWindowStart(), data.getWindowEnd(), data.getDuePolicies().size()));
if (data.getSkipped() != null && !data.getSkipped().isEmpty()) {
sb.append(String.format(" 另有 %d 张保单因数据异常未纳入,请核对源数据(详见跑批日志)。",
data.getSkipped().size()));
}
return sb.toString();
}
/**
* 支持入参为空(当前月)、"yyyy-MM"、"bizMonth=yyyy-MM"
*/
private YearMonth parseExecMonth(String param) {
String value = StringUtils.trimToNull(param);
if (value == null) {
return YearMonth.now();
}
if (value.startsWith("bizMonth=")) {
value = StringUtils.trimToNull(value.substring("bizMonth=".length()));
}
if (value == null) {
return YearMonth.now();
}
try {
return YearMonth.parse(value);
} catch (DateTimeParseException e) {
throw new BusinessException("续期提醒任务参数格式错误,应为 yyyy-MM,实际: " + param);
}
}
}
package com.yd.csf.api.service.impl;
import com.yd.csf.api.service.RenewalMailService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 续期提醒邮件发送:直连 SMTP。
* 配置见 Nacos:spring.mail.* 与 renewal.reminder.mail-from。
*/
@Service
@Slf4j
public class RenewalMailServiceImpl implements RenewalMailService {
@Resource
private JavaMailSender javaMailSender;
/**
* 发件人地址;不配置时取 spring.mail.username
*/
@Value("${renewal.reminder.mail-from:}")
private String mailFrom;
@Value("${spring.mail.username:}")
private String mailUsername;
@Override
public void sendWithAttachment(List<String> recipients, String subject, String content,
String fileName, byte[] attachment) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(
message, true, StandardCharsets.UTF_8.name());
helper.setFrom(resolveFrom());
helper.setTo(recipients.toArray(new String[0]));
helper.setSubject(subject);
helper.setText(content, false);
helper.addAttachment(fileName, new ByteArrayResource(attachment));
javaMailSender.send(message);
log.info("[续期提醒-邮件] 发送成功,收件人={}, 主题={}, 附件={}", recipients, subject, fileName);
} catch (Exception e) {
log.error("[续期提醒-邮件] 发送失败,收件人={}, 主题={}", recipients, subject, e);
throw new RuntimeException("续期提醒邮件发送失败: " + e.getMessage(), e);
}
}
private String resolveFrom() throws UnsupportedEncodingException {
String from = StringUtils.defaultIfBlank(mailFrom, mailUsername);
if (StringUtils.isBlank(from)) {
throw new IllegalStateException("未配置发件人:renewal.reminder.mail-from 或 spring.mail.username");
}
return from;
}
}
package com.yd.csf.service.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yd.csf.service.model.RenewalRemindBatch;
/**
* 针对表【renewal_remind_batch(保单续期提醒发送批次表)】的数据库操作 Mapper
*/
public interface RenewalRemindBatchMapper extends BaseMapper<RenewalRemindBatch> {
}
package com.yd.csf.service.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yd.csf.service.model.RenewalRemindBatch;
/**
* 保单续期提醒发送批次 服务类
*/
public interface IRenewalRemindBatchService extends IService<RenewalRemindBatch> {
/**
* 按执行月查询批次
*
* @param bizMonth 执行月 yyyy-MM
* @return 批次记录,不存在返回 null
*/
RenewalRemindBatch queryByBizMonth(String bizMonth);
}
package com.yd.csf.service.utils;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class SimpleDateUtils {
private static final ZoneId ZONE = ZoneId.systemDefault();
public static LocalDate toLocalDate(Date date) {
return date != null ? date.toInstant().atZone(ZONE).toLocalDate() : null;
}
}
\ No newline at end of file
<?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.yd.csf.service.dao.RenewalRemindBatchMapper">
<resultMap id="BaseResultMap" type="com.yd.csf.service.model.RenewalRemindBatch">
<id property="id" column="id"/>
<result property="batchBizId" column="batch_biz_id"/>
<result property="bizMonth" column="biz_month"/>
<result property="windowStart" column="window_start"/>
<result property="windowEnd" column="window_end"/>
<result property="policyCount" column="policy_count"/>
<result property="skippedCount" column="skipped_count"/>
<result property="recipients" column="recipients"/>
<result property="fileName" column="file_name"/>
<result property="status" column="status"/>
<result property="errorMsg" column="error_msg"/>
<result property="sendTime" column="send_time"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="Base_Column_List">
id
,batch_biz_id,biz_month,window_start,window_end,policy_count,
skipped_count,recipients,file_name,status,error_msg,
send_time,create_time,update_time
</sql>
</mapper>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment