Commit 15a970a0 by jianan

保费到期提醒22-保单邮件逐条通知

parent 3cab64d8
package com.yd.csf.api.service.impl;
import com.yd.csf.service.dto.RenewalDuePolicy;
import com.yd.csf.service.enums.RenewalMailTypeEnum;
import com.yd.csf.service.model.PolicyFollow;
import com.yd.csf.service.utils.RenewalPremiumUtils;
import com.yd.csf.service.utils.SimpleDateUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 逐单续期保费通知邮件构建器:简繁双语 HTML(内联 CSS、无图片无外链)。
* 静态标签简体段/繁体段各一份,数据值原样填充不做简繁转换,全部做 HTML 转义。
*/
@Component
public class RenewalPolicyMailBuilder {
private static final String MISSING_SC = "待核对";
private static final String MISSING_TC = "待核對";
private static final String FONT = "'Microsoft YaHei','PingFang SC',Arial,sans-serif";
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
private static final DecimalFormat MONEY_FMT = new DecimalFormat("#,##0.00");
/**
* grace_period 缺失时括注中的默认天数,与筛选侧同源配置
*/
@Value("${renewal.reminder.default-grace-days:31}")
private Integer configuredDefaultGraceDays;
/**
* 构建一封逐单通知邮件。
*
* @return 主题、HTML 正文、缺失字段数(非主键字段「待核对」项数,用于批次数据异常统计)
*/
public BuiltMail build(RenewalDuePolicy item, RenewalMailTypeEnum type) {
PolicyFollow p = item.getPolicyFollow();
String policyNo = StringUtils.trimToNull(p.getPolicyNo());
if (policyNo == null) {
// 无法标识保单的主键级缺失:不发信,由编排层计入跳过
throw new IllegalArgumentException("保单号为空,无法构建逐单通知邮件,policyFollowId=" + p.getId());
}
AtomicInteger missing = new AtomicInteger(0);
String company = escapedOrNull(p.getInsuranceCompany(), missing);
String product = escapedOrNull(p.getProductName(), missing);
String holderRaw = StringUtils.defaultIfBlank(p.getPolicyHolder(), p.getPolicyHolderEn());
String holder = escapedOrNull(holderRaw, missing);
String insured = escapedOrNull(p.getInsured(), missing);
BigDecimal total = RenewalPremiumUtils.totalPremiumAndLevy(p);
String currency = StringUtils.trimToNull(p.getPolicyCurrency());
if (currency == null) {
missing.incrementAndGet();
}
if (total == null) {
missing.incrementAndGet();
}
// 金额 HTML(已转义)或 null(由各语言段替换为本地「待核对」)
String money = total == null ? null : formatMoney(currency, total);
LocalDate effectiveDate = SimpleDateUtils.toLocalDate(p.getEffectiveDate());
String dueDateText = item.getDueDate().format(DATE_FMT);
String graceEndText = item.getGraceEnd().format(DATE_FMT);
String effectiveText = effectiveDate == null ? null : effectiveDate.format(DATE_FMT);
String graceNoteSc = item.isGraceFallback()
? "(宽限天数待核对(按默认 " + resolveDefaultGraceDays() + " 天估算))"
: "(保费到期日起计 " + item.getGraceDays() + " 天)";
String graceNoteTc = item.isGraceFallback()
? "(寬限天數待核對(按預設 " + resolveDefaultGraceDays() + " 天估算))"
: "(保費到期日起計 " + item.getGraceDays() + " 天)";
String subject = (type == RenewalMailTypeEnum.R2 ? "【宽限期跟进】" : "")
+ "续期保费通知-" + policyNo;
String html = "<div style=\"font-family:" + FONT + ";font-size:14px;color:#333333;line-height:1.9;\">"
+ segment(Labels.SC, MISSING_SC, type, company, product, esc(policyNo), holder, insured,
money, effectiveText, dueDateText, graceEndText, graceNoteSc)
+ "<hr style=\"border:none;border-top:1px solid #dddddd;margin:18px 0;\">"
+ segment(Labels.TC, MISSING_TC, type, company, product, esc(policyNo), holder, insured,
money, effectiveText, dueDateText, graceEndText, graceNoteTc)
+ "<p style=\"margin:16px 0 0 0;color:#888888;font-size:12px;\">"
+ "本邮件由系统自动发送,请勿直接回复。<br>"
+ "本郵件由系統自動發送,請勿直接回覆。"
+ "</p>"
+ "</div>";
return new BuiltMail(subject, html, missing.get());
}
/**
* 渲染一个语言段(简体或繁体)。数据值在入参前统一转义,繁体段不做简繁转换;
* 为空的字段以本段语言的「待核对」占位展示。
*/
private String segment(Labels l, String missingMark, RenewalMailTypeEnum type,
String company, String product, String policyNo, String holder, String insured,
String money, String effectiveText, String dueDateText,
String graceEndText, String graceNote) {
String opening;
if (type == RenewalMailTypeEnum.R2) {
opening = l.openingR2Prefix + dueDateText + l.openingR2Middle + graceEndText + l.openingR2Suffix;
} else {
opening = l.openingR1Prefix + dueDateText + l.openingR1Suffix;
}
String companyV = markOrValue(company, missingMark);
String productV = markOrValue(product, missingMark);
String holderV = markOrValue(holder, missingMark);
String insuredV = markOrValue(insured, missingMark);
String moneyV = markOrValue(money, missingMark);
String effectiveV = markOrValue(effectiveText, missingMark);
StringBuilder sb = new StringBuilder();
sb.append("<p style=\"margin:0 0 6px 0;font-weight:bold;\">").append(l.segmentTitle).append("</p>");
sb.append("<p style=\"margin:0 0 12px 0;\">").append(opening).append("</p>");
sb.append("<table cellspacing=\"0\" cellpadding=\"4\" style=\"border-collapse:collapse;font-size:14px;\">");
sb.append(row4(l.company, companyV, l.product, productV));
sb.append(row4(l.policyNo, policyNo, l.effectiveDate, effectiveV));
sb.append(row4(l.holder, holderV, l.insured, insuredV));
sb.append(row1(l.premiumAndLevy, moneyV, false));
sb.append(row1(l.totalPremium, moneyV, true));
sb.append("</table>");
sb.append("<p style=\"margin:10px 0 0 0;\">").append(l.remark).append("</p>");
sb.append("<p style=\"margin:6px 0 0 0;\">")
.append(l.dueDateLabel).append(":<span style=\"font-weight:bold;text-decoration:underline;\">")
.append(dueDateText).append("</span></p>");
sb.append("<p style=\"margin:6px 0 0 0;\">")
.append(l.graceEndLabel).append(":<span style=\"font-weight:bold;text-decoration:underline;\">")
.append(graceEndText).append("</span>").append(graceNote).append("</p>");
return sb.toString();
}
private String row4(String label1, String value1, String label2, String value2) {
return "<tr>"
+ "<td style=\"white-space:nowrap;color:#666666;\">" + label1 + ":</td>"
+ "<td style=\"padding-right:24px;\">" + value1 + "</td>"
+ "<td style=\"white-space:nowrap;color:#666666;\">" + label2 + ":</td>"
+ "<td>" + value2 + "</td>"
+ "</tr>";
}
private String row1(String label, String value, boolean total) {
String valueStyle = total
? "<span style=\"color:#D90000;font-weight:bold;\">" + value + "</span>"
: value;
return "<tr>"
+ "<td style=\"white-space:nowrap;color:#666666;\">" + label + ":</td>"
+ "<td colspan=\"3\">" + valueStyle + "</td>"
+ "</tr>";
}
/**
* 取非空字段值并 HTML 转义;空/空白时返回 null(段落渲染时替换为对应语言的「待核对」),计数 +1。
*/
private String escapedOrNull(String raw, AtomicInteger missing) {
String value = StringUtils.trimToNull(raw);
if (value == null) {
missing.incrementAndGet();
return null;
}
return esc(value);
}
private String markOrValue(String escapedValue, String missingMark) {
return escapedValue == null ? missingMark : escapedValue;
}
private String formatMoney(String currency, BigDecimal total) {
String number = MONEY_FMT.format(total);
return currency == null ? number : esc(currency) + " " + number;
}
private int resolveDefaultGraceDays() {
if (configuredDefaultGraceDays == null || configuredDefaultGraceDays <= 0) {
return 31;
}
return configuredDefaultGraceDays;
}
/**
* HTML 转义数据值(& &lt; &gt; " ')
*/
static String esc(String value) {
if (value == null) {
return null;
}
StringBuilder sb = new StringBuilder(value.length() + 16);
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '&':
sb.append("&amp;");
break;
case '<':
sb.append("&lt;");
break;
case '>':
sb.append("&gt;");
break;
case '"':
sb.append("&quot;");
break;
case '\'':
sb.append("&#39;");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
/**
* 构建结果
*/
@Getter
@AllArgsConstructor
public static class BuiltMail {
/**
* 邮件主题
*/
private final String subject;
/**
* HTML 正文
*/
private final String htmlContent;
/**
* 非主键字段缺失(显示「待核对」)数量
*/
private final int missingFieldCount;
}
/**
* 一套语言下的全部静态文案;数据值不在两套文案间转换。
*/
private static final class Labels {
private final String segmentTitle;
private final String company;
private final String product;
private final String policyNo;
private final String effectiveDate;
private final String holder;
private final String insured;
private final String premiumAndLevy;
private final String totalPremium;
private final String remark;
private final String dueDateLabel;
private final String graceEndLabel;
private final String openingR1Prefix;
private final String openingR1Suffix;
private final String openingR2Prefix;
private final String openingR2Middle;
private final String openingR2Suffix;
private Labels(String segmentTitle, String company, String product, String policyNo,
String effectiveDate, String holder, String insured,
String premiumAndLevy, String totalPremium, String remark,
String dueDateLabel, String graceEndLabel,
String openingR1Prefix, String openingR1Suffix,
String openingR2Prefix, String openingR2Middle, String openingR2Suffix) {
this.segmentTitle = segmentTitle;
this.company = company;
this.product = product;
this.policyNo = policyNo;
this.effectiveDate = effectiveDate;
this.holder = holder;
this.insured = insured;
this.premiumAndLevy = premiumAndLevy;
this.totalPremium = totalPremium;
this.remark = remark;
this.dueDateLabel = dueDateLabel;
this.graceEndLabel = graceEndLabel;
this.openingR1Prefix = openingR1Prefix;
this.openingR1Suffix = openingR1Suffix;
this.openingR2Prefix = openingR2Prefix;
this.openingR2Middle = openingR2Middle;
this.openingR2Suffix = openingR2Suffix;
}
private static final Labels SC = new Labels(
"【简体】", "产品公司", "计划名称", "保单号码", "保单生效日",
"保单持有人", "受保人", "保费及征费", "应缴总保费",
"备注:此金额未计入保费优惠,保费优惠待确认后通知客户。",
"保费到期日", "宽限期到期日",
"以下保单的保费将于 ", " 到期,请在此日期前缴付应缴保费。",
"以下保单的保费已于 ", " 到期,宽限期将于 ", " 届满,请尽快跟进确认缴费情况。");
private static final Labels TC = new Labels(
"【繁體】", "產品公司", "計劃名稱", "保單號碼", "保單生效日",
"保單持有人", "受保人", "保費及征費", "應繳總保費",
"備註:此金額未計入保費優惠,保費優惠待確認後通知客戶。",
"保費到期日", "寬限期到期日",
"以下保單的保費將於 ", " 到期,請在此日期前繳付應繳保費。",
"以下保單的保費已於 ", " 到期,寬限期將於 ", " 屆滿,請盡快跟進確認繳費情況。");
}
}
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