Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yd-csf
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xingmin
yd-csf
Commits
15a970a0
Commit
15a970a0
authored
Sep 16, 2026
by
jianan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
保费到期提醒22-保单邮件逐条通知
parent
3cab64d8
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
302 additions
and
0 deletions
+302
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalPolicyMailBuilder.java
+302
-0
No files found.
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalPolicyMailBuilder.java
0 → 100644
View file @
15a970a0
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 转义数据值(& < > " ')
*/
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
(
"&"
);
break
;
case
'<'
:
sb
.
append
(
"<"
);
break
;
case
'>'
:
sb
.
append
(
">"
);
break
;
case
'"'
:
sb
.
append
(
"""
);
break
;
case
'\''
:
sb
.
append
(
"'"
);
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
(
"【繁體】"
,
"產品公司"
,
"計劃名稱"
,
"保單號碼"
,
"保單生效日"
,
"保單持有人"
,
"受保人"
,
"保費及征費"
,
"應繳總保費"
,
"備註:此金額未計入保費優惠,保費優惠待確認後通知客戶。"
,
"保費到期日"
,
"寬限期到期日"
,
"以下保單的保費將於 "
,
" 到期,請在此日期前繳付應繳保費。"
,
"以下保單的保費已於 "
,
" 到期,寬限期將於 "
,
" 屆滿,請盡快跟進確認繳費情況。"
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment