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
b1cd81e8
Commit
b1cd81e8
authored
Sep 20, 2026
by
zhangxingmin
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/dev' into prod
# Conflicts: # yd-csf-api/Dockerfile
parents
68111819
bcd2540e
Show whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
2536 additions
and
56 deletions
+2536
-56
yd-csf-api/Dockerfile
+20
-12
yd-csf-api/pom.xml
+16
-0
yd-csf-api/src/main/java/com/yd/csf/api/controller/ApiPolicyFollowController.java
+14
-0
yd-csf-api/src/main/java/com/yd/csf/api/handler/RenewalRemindJobHandler.java
+28
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/ApiPolicyFollowExportService.java
+119
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/RenewalDispatchService.java
+14
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/RenewalMailService.java
+31
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalDispatchServiceImpl.java
+391
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalMailServiceImpl.java
+91
-0
yd-csf-api/src/main/resources/logback-spring.xml.bak
+2
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/TemplateStyleStrategy.java
+143
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalDueDateCalculator.java
+211
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalDuePolicy.java
+38
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalPolicyMailBuilder.java
+313
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalPolicyReportService.java
+65
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalPremiumUtils.java
+39
-0
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalReminderData.java
+51
-0
yd-csf-service/src/main/java/com/yd/csf/service/config/XxlJobConfig.java
+62
-44
yd-csf-service/src/main/java/com/yd/csf/service/dao/RenewalRemindBatchMapper.java
+10
-0
yd-csf-service/src/main/java/com/yd/csf/service/dao/RenewalRemindPolicyMailMapper.java
+10
-0
yd-csf-service/src/main/java/com/yd/csf/service/enums/RenewalMailTypeEnum.java
+29
-0
yd-csf-service/src/main/java/com/yd/csf/service/enums/RenewalRemindStatusEnum.java
+30
-0
yd-csf-service/src/main/java/com/yd/csf/service/model/RenewalRemindBatch.java
+110
-0
yd-csf-service/src/main/java/com/yd/csf/service/model/RenewalRemindPolicyMail.java
+103
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/IRenewalRemindBatchService.java
+18
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/IRenewalRemindPolicyMailService.java
+25
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/RenewalReminderQueryService.java
+22
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalRemindBatchServiceImpl.java
+24
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalRemindPolicyMailServiceImpl.java
+52
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalReminderQueryServiceImpl.java
+216
-0
yd-csf-service/src/main/java/com/yd/csf/service/utils/SimpleDateUtils.java
+14
-0
yd-csf-service/src/main/java/com/yd/csf/service/vo/RenewalPolicyExportDTO.java
+162
-0
yd-csf-service/src/main/resources/mappers/RenewalRemindBatchMapper.xml
+32
-0
yd-csf-service/src/main/resources/mappers/RenewalRemindPolicyMailMapper.xml
+31
-0
No files found.
yd-csf-api/Dockerfile
View file @
b1cd81e8
# 基础镜像
FROM
openjdk:8
# 维护人
# JRE 运行时(原 openjdk:8 官方镜像已停更无补丁);tag 固定到具体小版本保证构建可重现
# 升级时替换为服务器可拉取的最新 8uXXX-bXX-jre-jammy
FROM
eclipse-temurin:8u422-b05-jre-jammy
LABEL
maintainer="zxm<2060197959@qq.com>"
# 创建应用目录
RUN
mkdir
-p
/home/app
# 创建日志目录并授权(默认 root 可写)
#RUN mkdir -p /var/log/yd-csf-api && chmod 755 /var/log/yd-csf-api
# 拷贝项目jar
COPY
target/yd-csf-api-1.0-SNAPSHOT-exec.jar /home/app/yd-csf-api.jar
# 启动命令
ENTRYPOINT
["java", "-Duser.timezone=Asia/Shanghai", "-Xmx256m", "-Xms128m", "-jar", "/home/app/yd-csf-api.jar"]
ENV
TZ=Asia/Shanghai \
JAVA_OPTS="-Xmx256m -Xms128m"
# 固定 UID 1001,与宿主机日志目录属主对齐;提前创建日志目录并授权给非 root 用户
RUN
groupadd
-r
app
--gid
=
1001
\
&&
useradd
-r
-g
app
--uid
=
1001
-d
/home/app app
\
&&
mkdir
-p
/var/log/yd-csf-api
\
&&
chown app:app /var/log/yd-csf-api
# COPY 会自动创建父目录 /home/app,无需单独 mkdir
COPY
--chown=app:app target/yd-csf-api-1.0-SNAPSHOT-exec.jar /home/app/yd-csf-api.jar
USER
app
EXPOSE
9202
# sh -c + exec:保证 JVM 成为 1 号进程,docker stop 的 SIGTERM 可直达,支持优雅停机
ENTRYPOINT
["sh", "-c", "exec java $JAVA_OPTS -Duser.timezone=Asia/Shanghai -jar /home/app/yd-csf-api.jar"]
yd-csf-api/pom.xml
View file @
b1cd81e8
...
...
@@ -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>
...
...
yd-csf-api/src/main/java/com/yd/csf/api/controller/ApiPolicyFollowController.java
View file @
b1cd81e8
...
...
@@ -26,6 +26,7 @@ import com.yd.csf.api.listener.PolicyDataListener;
import
com.yd.csf.api.service.ApiCommissionConditionService
;
import
com.yd.csf.api.service.ApiExpectedFortuneService
;
import
com.yd.csf.api.service.ApiPolicyFollowService
;
import
com.yd.csf.api.service.ApiPolicyFollowExportService
;
import
com.yd.csf.feign.request.expectedfortune.ApiGenerateExpectedFortuneRequest
;
import
com.yd.csf.feign.response.appointment.ApiAppointmentDetailResponse
;
import
com.yd.csf.feign.response.expectedfortune.ApiGenerateExpectedFortuneResponse
;
...
...
@@ -67,6 +68,7 @@ import javax.servlet.ServletOutputStream;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletResponse
;
import
javax.validation.constraints.NotBlank
;
import
java.io.UnsupportedEncodingException
;
import
java.math.BigDecimal
;
import
java.sql.SQLIntegrityConstraintViolationException
;
import
java.text.ParseException
;
...
...
@@ -112,6 +114,8 @@ public class ApiPolicyFollowController {
private
FeignResultHelper
feignResultHelper
;
@Resource
private
ApiExpectedFortuneAsyncService
apiExpectedFortuneAsyncService
;
@Resource
private
ApiPolicyFollowExportService
apiPolicyFollowExportService
;
@Autowired
private
ApiCommissionConditionService
apiCommissionConditionService
;
...
...
@@ -510,6 +514,16 @@ public class ApiPolicyFollowController {
}
/**
* 按页面查询条件导出新单跟进清单(与列表查询同一筛选口径,30 列模板,R 列为实时计算的保费到期日)
*/
@PostMapping
(
"/export"
)
@Operation
(
summary
=
"导出新单跟进列表"
)
public
void
exportPolicyFollow
(
@RequestBody
PolicyFollowQueryRequest
policyFollowQueryRequest
,
HttpServletResponse
response
)
{
apiPolicyFollowExportService
.
export
(
policyFollowQueryRequest
,
response
);
}
/**
* 修改跟进状态
*
* @param changePolicyFollowStatusRequest
...
...
yd-csf-api/src/main/java/com/yd/csf/api/handler/RenewalRemindJobHandler.java
0 → 100644
View file @
b1cd81e8
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
);
}
}
yd-csf-api/src/main/java/com/yd/csf/api/service/ApiPolicyFollowExportService.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
api
.
service
;
import
com.alibaba.excel.EasyExcel
;
import
com.alibaba.excel.ExcelWriter
;
import
com.alibaba.excel.write.metadata.WriteSheet
;
import
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.csf.service.component.TemplateStyleStrategy
;
import
com.yd.csf.service.dto.PolicyFollowQueryRequest
;
import
com.yd.csf.service.model.PolicyFollow
;
import
com.yd.csf.service.service.PolicyFollowService
;
import
com.yd.csf.service.component.renewal.RenewalDueDateCalculator
;
import
com.yd.csf.service.utils.SimpleDateUtils
;
import
com.yd.csf.service.vo.RenewalPolicyExportDTO
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
javax.servlet.http.HttpServletResponse
;
import
java.io.IOException
;
import
java.io.UnsupportedEncodingException
;
import
java.net.URLEncoder
;
import
java.nio.charset.StandardCharsets
;
import
java.time.LocalDate
;
import
java.time.format.DateTimeFormatter
;
import
java.util.ArrayList
;
import
java.util.Date
;
import
java.util.List
;
/**
* 新单跟进列表导出:行集与分页页面查询同一筛选口径,列沿用保单清单 30 列模板,
* R 列「保费到期日」按当天逐行实时计算。
*/
@Service
@Slf4j
public
class
ApiPolicyFollowExportService
{
/** 单次导出行数硬上限,超过需缩小筛选条件 */
private
static
final
long
MAX_EXPORT_ROWS
=
50_000L
;
private
static
final
long
PAGE_SIZE
=
2_000L
;
private
static
final
String
XLSX_CONTENT_TYPE
=
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
;
private
static
final
DateTimeFormatter
DAY_FMT
=
DateTimeFormatter
.
ofPattern
(
"yyyyMMdd"
);
@Resource
private
PolicyFollowService
policyFollowService
;
public
void
export
(
PolicyFollowQueryRequest
request
,
HttpServletResponse
response
)
{
QueryWrapper
<
PolicyFollow
>
wrapper
=
policyFollowService
.
getQueryWrapper
(
request
);
long
total
=
policyFollowService
.
count
(
wrapper
);
if
(
total
==
0
)
{
throw
new
BusinessException
(
"无导出数据"
);
}
if
(
total
>
MAX_EXPORT_ROWS
)
{
throw
new
BusinessException
(
String
.
format
(
"导出数据量 %d 条超过上限 %d 条,请缩小筛选条件后再导出"
,
total
,
MAX_EXPORT_ROWS
));
}
long
pages
=
(
total
+
PAGE_SIZE
-
1
)
/
PAGE_SIZE
;
LocalDate
today
=
LocalDate
.
now
();
Page
<
PolicyFollow
>
firstPage
=
policyFollowService
.
page
(
new
Page
<>(
1
,
PAGE_SIZE
,
false
),
wrapper
);
setDownloadHeaders
(
response
,
buildFileName
(
today
));
try
(
ExcelWriter
writer
=
EasyExcel
.
write
(
response
.
getOutputStream
(),
RenewalPolicyExportDTO
.
class
)
.
useDefaultStyle
(
false
)
.
registerWriteHandler
(
new
TemplateStyleStrategy
())
.
build
())
{
WriteSheet
sheet
=
EasyExcel
.
writerSheet
(
"新单跟进"
).
build
();
writer
.
write
(
toRows
(
firstPage
.
getRecords
(),
today
),
sheet
);
for
(
long
pageNo
=
2
;
pageNo
<=
pages
;
pageNo
++)
{
Page
<
PolicyFollow
>
pageData
=
policyFollowService
.
page
(
new
Page
<>(
pageNo
,
PAGE_SIZE
,
false
),
wrapper
);
if
(
CollectionUtils
.
isEmpty
(
pageData
.
getRecords
()))
{
break
;
}
writer
.
write
(
toRows
(
pageData
.
getRecords
(),
today
),
sheet
);
}
}
catch
(
IOException
e
)
{
log
.
error
(
"新单跟进导出写入失败,total={}"
,
total
,
e
);
throw
new
BusinessException
(
"导出新单跟进清单失败"
);
}
}
private
List
<
RenewalPolicyExportDTO
>
toRows
(
List
<
PolicyFollow
>
policies
,
LocalDate
today
)
{
List
<
RenewalPolicyExportDTO
>
rows
=
new
ArrayList
<>(
policies
.
size
());
for
(
PolicyFollow
p
:
policies
)
{
RenewalDueDateCalculator
.
Result
result
=
RenewalDueDateCalculator
.
calculate
(
SimpleDateUtils
.
toLocalDate
(
p
.
getEffectiveDate
()),
p
.
getPaymentFrequency
(),
p
.
getIssueNumber
(),
today
);
Date
dueDate
=
result
.
isIncluded
()
?
Date
.
from
(
result
.
getDueDate
().
atStartOfDay
(
java
.
time
.
ZoneId
.
systemDefault
()).
toInstant
())
:
null
;
rows
.
add
(
RenewalPolicyExportDTO
.
toRow
(
p
,
dueDate
));
}
return
rows
;
}
private
String
buildFileName
(
LocalDate
today
)
{
String
raw
=
"新单跟进清单_"
+
today
.
format
(
DAY_FMT
)
+
".xlsx"
;
try
{
return
URLEncoder
.
encode
(
raw
,
StandardCharsets
.
UTF_8
.
name
()).
replaceAll
(
"\\+"
,
"%20"
);
}
catch
(
UnsupportedEncodingException
e
)
{
// UTF-8 为 JVM 规范保证支持,理论不可达
throw
new
IllegalStateException
(
e
);
}
}
private
void
setDownloadHeaders
(
HttpServletResponse
response
,
String
encodedFileName
)
{
response
.
setContentType
(
XLSX_CONTENT_TYPE
);
response
.
setCharacterEncoding
(
StandardCharsets
.
UTF_8
.
name
());
response
.
setHeader
(
"Content-disposition"
,
"attachment;filename*=utf-8''"
+
encodedFileName
);
}
}
yd-csf-api/src/main/java/com/yd/csf/api/service/RenewalDispatchService.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
api
.
service
;
/**
* 续期提醒月度跑批编排:筛选 → 生成清单 → 发邮件 → 批次落库
*/
public
interface
RenewalDispatchService
{
/**
* 执行一次续期提醒跑批
*
* @param bizMonth 执行月参数(yyyy-MM 或 bizMonth=yyyy-MM),为空时取当前月
*/
void
dispatch
(
String
bizMonth
);
}
yd-csf-api/src/main/java/com/yd/csf/api/service/RenewalMailService.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
api
.
service
;
import
java.util.List
;
/**
* 续期提醒邮件发送(JavaMail 直连 SMTP,见 RenewalMailServiceImpl)。
*/
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
);
/**
* 发送无附件 HTML 邮件(逐单文字通知),To 单地址 + CC 列表,发件人带显示名。
*
* @param to 唯一主送人
* @param ccList 抄送列表(可为空)
* @param subject 邮件主题
* @param htmlContent HTML 正文
*/
void
sendHtml
(
String
to
,
List
<
String
>
ccList
,
String
subject
,
String
htmlContent
);
}
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalDispatchServiceImpl.java
0 → 100644
View file @
b1cd81e8
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.renewal.RenewalPolicyMailBuilder
;
import
com.yd.csf.service.component.renewal.RenewalPolicyReportService
;
import
com.yd.csf.service.component.renewal.RenewalDuePolicy
;
import
com.yd.csf.service.component.renewal.RenewalReminderData
;
import
com.yd.csf.service.enums.RenewalMailTypeEnum
;
import
com.yd.csf.service.enums.RenewalRemindStatusEnum
;
import
com.yd.csf.service.model.RenewalRemindBatch
;
import
com.yd.csf.service.model.RenewalRemindPolicyMail
;
import
com.yd.csf.service.service.IRenewalRemindBatchService
;
import
com.yd.csf.service.service.IRenewalRemindPolicyMailService
;
import
com.yd.csf.service.service.RenewalReminderQueryService
;
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.ArrayList
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.HashSet
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Set
;
import
java.util.regex.Pattern
;
import
java.util.stream.Collectors
;
/**
* 续期提醒月度跑批编排实现:R2 宽限期跟进 + R1 到期通知逐单发送,全部成功后发送 Excel 汇总邮件。
*/
@Service
@Slf4j
public
class
RenewalDispatchServiceImpl
implements
RenewalDispatchService
{
private
static
final
String
BATCH_BIZ_ID_PREFIX
=
"RR"
;
private
static
final
int
ERROR_MSG_MAX_LEN
=
1900
;
private
static
final
Pattern
EMAIL_PATTERN
=
Pattern
.
compile
(
"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
);
@Value
(
"${renewal.reminder.recipients:}"
)
private
String
recipientsConfig
;
/**
* 逐单邮件唯一主送人
*/
@Value
(
"${renewal.reminder.policy.to:}"
)
private
String
policyToConfig
;
/**
* 逐单邮件抄送名单(逗号分隔,可空)
*/
@Value
(
"${renewal.reminder.policy.cc:}"
)
private
String
policyCcConfig
;
@Value
(
"${renewal.reminder.default-grace-days:31}"
)
private
int
defaultGraceDays
;
@Value
(
"${renewal.reminder.send-interval-ms:2000}"
)
private
long
sendIntervalMs
;
@Resource
private
RenewalReminderQueryService
renewalReminderQueryService
;
@Resource
private
RenewalPolicyReportService
renewalPolicyReportService
;
@Resource
private
RenewalMailService
renewalMailService
;
@Resource
private
IRenewalRemindBatchService
renewalRemindBatchService
;
@Resource
private
IRenewalRemindPolicyMailService
renewalRemindPolicyMailService
;
@Resource
private
RenewalPolicyMailBuilder
policyMailBuilder
;
@Override
public
void
dispatch
(
String
bizMonthParam
)
{
YearMonth
execMonth
=
parseExecMonth
(
bizMonthParam
);
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
;
List
<
String
>
failures
=
new
ArrayList
<>();
try
{
// 1. 配置 fail-fast:任何收件配置错误都在发信前暴露
List
<
String
>
summaryRecipients
=
parseAddressList
(
recipientsConfig
);
List
<
String
>
policyToList
=
parseAddressList
(
policyToConfig
);
List
<
String
>
policyCcList
=
parseAddressList
(
policyCcConfig
);
validateConfig
(
summaryRecipients
,
policyToList
,
policyCcList
);
String
policyTo
=
policyToList
.
get
(
0
);
String
ccSnapshot
=
CollectionUtils
.
isEmpty
(
policyCcList
)
?
null
:
String
.
join
(
","
,
policyCcList
);
recipientsSnapshot
=
String
.
join
(
","
,
summaryRecipients
);
// 2. 计算基准日固定为执行月 1 号:定时跑与手动补发同一 bizMonth 结果可复现
LocalDate
baseDate
=
execMonth
.
atDay
(
1
);
data
=
renewalReminderQueryService
.
build
(
execMonth
,
baseDate
);
List
<
RenewalDuePolicy
>
r1List
=
data
.
getDuePolicies
();
List
<
RenewalDuePolicy
>
r2List
=
data
.
getDunPolicies
();
// 3. 两轮均空:不发任何邮件,记 EMPTY
if
(
CollectionUtils
.
isEmpty
(
r1List
)
&&
CollectionUtils
.
isEmpty
(
r2List
))
{
saveBatch
(
existBatch
,
execMonth
,
data
,
null
,
RenewalRemindStatusEnum
.
EMPTY
,
null
,
null
,
0
,
0
);
log
.
info
(
"续期提醒两轮均无命中,已记 EMPTY。bizMonth={}"
,
bizMonth
);
return
;
}
// 4. 当月逐单留痕:构建已 SUCCESS 集合,重跑绝不重发
Set
<
String
>
succeededKeys
=
buildSucceededKeys
(
renewalRemindPolicyMailService
.
listByBizMonth
(
bizMonth
));
// 5. 逐封发送:R2 先、R1 后;单封失败仅记该封 FAIL 并继续
int
[]
missingTotal
=
{
0
};
int
[]
attempted
=
{
0
};
// R2 先发送(宽限期紧迫),R1 后发送
sendRound
(
r2List
,
RenewalMailTypeEnum
.
R2
,
bizMonth
,
policyTo
,
ccSnapshot
,
succeededKeys
,
failures
,
missingTotal
,
attempted
,
data
.
getSkipped
());
sendRound
(
r1List
,
RenewalMailTypeEnum
.
R1
,
bizMonth
,
policyTo
,
ccSnapshot
,
succeededKeys
,
failures
,
missingTotal
,
attempted
,
data
.
getSkipped
());
// 6. R1 非空即发送 Excel 汇总:汇总收件人与逐单收件人不同,清单送达不与逐单通道成败挂钩
if
(
CollectionUtils
.
isNotEmpty
(
r1List
))
{
fileName
=
renewalPolicyReportService
.
buildFileName
(
data
,
LocalDate
.
now
());
byte
[]
attachment
=
renewalPolicyReportService
.
generate
(
data
);
String
subject
=
buildSubject
(
data
);
String
content
=
buildContent
(
data
,
missingTotal
[
0
]);
renewalMailService
.
sendWithAttachment
(
summaryRecipients
,
subject
,
content
,
fileName
,
attachment
);
}
// 7. 逐单存在失败:汇总已送达,批次仍记 FAIL 并抛出,使调度标红、重跑只补失败封
if
(!
failures
.
isEmpty
())
{
String
errorMsg
=
StringUtils
.
substring
(
String
.
join
(
"; "
,
failures
),
0
,
ERROR_MSG_MAX_LEN
);
log
.
error
(
"续期提醒 {} 封逐单邮件失败(汇总邮件已发送)。bizMonth={}, 明细={}"
,
failures
.
size
(),
bizMonth
,
errorMsg
);
throw
new
BusinessException
(
"续期提醒逐单邮件失败 "
+
failures
.
size
()
+
" 封: "
+
errorMsg
);
}
saveBatch
(
existBatch
,
execMonth
,
data
,
fileName
,
RenewalRemindStatusEnum
.
SUCCESS
,
null
,
recipientsSnapshot
,
r2List
.
size
(),
0
);
log
.
info
(
"续期提醒发送完成。bizMonth={}, R1={}, R2={}, 待核对项={}"
,
bizMonth
,
r1List
.
size
(),
r2List
.
size
(),
missingTotal
[
0
]);
}
catch
(
BusinessException
e
)
{
// 配置 fail-fast、逐单失败等业务异常:批次落 FAIL(汇总可能已发出,fileName 保留)
String
errorMsg
=
StringUtils
.
substring
(
e
.
getMessage
(),
0
,
ERROR_MSG_MAX_LEN
);
saveBatch
(
existBatch
,
execMonth
,
data
,
fileName
,
RenewalRemindStatusEnum
.
FAIL
,
errorMsg
,
recipientsSnapshot
,
data
==
null
?
0
:
data
.
getDunPolicies
().
size
(),
failures
.
size
());
throw
e
;
}
catch
(
Exception
e
)
{
// 汇总邮件等非逐单环节异常:批次 FAIL(逐单成功封已留痕,重跑只补未竟部分)
String
errorMsg
=
StringUtils
.
substring
(
e
.
getMessage
(),
0
,
ERROR_MSG_MAX_LEN
);
saveBatch
(
existBatch
,
execMonth
,
data
,
fileName
,
RenewalRemindStatusEnum
.
FAIL
,
errorMsg
,
recipientsSnapshot
,
data
==
null
?
0
:
data
.
getDunPolicies
().
size
(),
failures
.
size
());
log
.
error
(
"续期提醒跑批失败。bizMonth={}"
,
bizMonth
,
e
);
throw
new
BusinessException
(
"续期提醒跑批失败: "
+
e
.
getMessage
());
}
}
/**
* 发送一轮逐单邮件。已 SUCCESS 的保单邮件跳过;发送失败仅记该封 FAIL 不中断整轮;
* 模板构建失败(正常已在筛选层拦截,如空保单号)跳过该单计入异常明细,不拖垮整批。
*/
private
void
sendRound
(
List
<
RenewalDuePolicy
>
items
,
RenewalMailTypeEnum
type
,
String
bizMonth
,
String
to
,
String
ccSnapshot
,
Set
<
String
>
succeededKeys
,
List
<
String
>
failures
,
int
[]
missingTotal
,
int
[]
attempted
,
Map
<
String
,
String
>
skipped
)
{
if
(
CollectionUtils
.
isEmpty
(
items
))
{
return
;
}
List
<
String
>
ccList
=
ccSnapshot
==
null
?
Collections
.
emptyList
()
:
Arrays
.
asList
(
ccSnapshot
.
split
(
","
));
for
(
RenewalDuePolicy
item
:
items
)
{
Long
policyFollowId
=
item
.
getPolicyFollow
().
getId
();
String
key
=
mailKey
(
policyFollowId
,
type
.
getItemValue
());
// 模板构建(不依赖网络):缺失字段统计对全部保单进行,保证汇总计数稳定
RenewalPolicyMailBuilder
.
BuiltMail
built
;
try
{
built
=
policyMailBuilder
.
build
(
item
,
type
);
missingTotal
[
0
]
+=
built
.
getMissingFieldCount
();
}
catch
(
Exception
e
)
{
// 无法构建(如保单号为空,筛选层本应已拦):跳过计异常,批次可 SUCCESS,数据修复后重跑补发
log
.
error
(
"续期提醒邮件构建失败,跳过该单,{},policyFollowId={}"
,
type
,
policyFollowId
,
e
);
if
(
skipped
!=
null
)
{
skipped
.
putIfAbsent
(
String
.
valueOf
(
policyFollowId
),
"邮件构建失败("
+
type
.
getItemValue
()
+
"): "
+
StringUtils
.
substring
(
StringUtils
.
defaultString
(
e
.
getMessage
()),
0
,
200
));
}
continue
;
}
// 已 SUCCESS 的保单邮件任何情况下不重发
if
(
succeededKeys
.
contains
(
key
))
{
log
.
info
(
"逐单邮件已成功留痕,跳过。bizMonth={}, key={}"
,
bizMonth
,
key
);
continue
;
}
if
(
attempted
[
0
]
>
0
&&
sendIntervalMs
>
0
)
{
sleepBetweenMails
();
}
attempted
[
0
]++;
try
{
renewalMailService
.
sendHtml
(
to
,
ccList
,
built
.
getSubject
(),
built
.
getHtmlContent
());
upsertMailRecord
(
item
,
type
,
bizMonth
,
to
,
ccSnapshot
,
true
,
null
);
}
catch
(
Exception
e
)
{
log
.
error
(
"续期提醒邮件发送失败,{},policyNo={}"
,
type
,
item
.
getPolicyFollow
().
getPolicyNo
(),
e
);
upsertMailRecord
(
item
,
type
,
bizMonth
,
to
,
ccSnapshot
,
false
,
e
.
getMessage
());
failures
.
add
(
failureDesc
(
item
,
type
,
e
.
getMessage
()));
}
}
}
private
void
sleepBetweenMails
()
{
try
{
Thread
.
sleep
(
sendIntervalMs
);
}
catch
(
InterruptedException
ie
)
{
Thread
.
currentThread
().
interrupt
();
throw
new
BusinessException
(
"续期提醒跑批被中断"
);
}
}
/**
* 逐封发送结果落留痕表(按唯一键 upsert,FAIL→SUCCESS 更新同一行)
*/
private
void
upsertMailRecord
(
RenewalDuePolicy
item
,
RenewalMailTypeEnum
type
,
String
bizMonth
,
String
to
,
String
ccSnapshot
,
boolean
success
,
String
errorMsg
)
{
RenewalRemindPolicyMail
record
=
new
RenewalRemindPolicyMail
();
record
.
setBizMonth
(
bizMonth
);
record
.
setPolicyFollowId
(
item
.
getPolicyFollow
().
getId
());
record
.
setPolicyNo
(
item
.
getPolicyFollow
().
getPolicyNo
());
record
.
setMailType
(
type
.
getItemValue
());
record
.
setDueDate
(
item
.
getDueDate
());
record
.
setGraceEnd
(
item
.
getGraceEnd
());
record
.
setToRecipients
(
to
);
record
.
setCcRecipients
(
ccSnapshot
);
record
.
setStatus
(
success
?
RenewalRemindStatusEnum
.
SUCCESS
.
getItemValue
()
:
RenewalRemindStatusEnum
.
FAIL
.
getItemValue
());
record
.
setErrorMsg
(
success
?
null
:
StringUtils
.
substring
(
errorMsg
,
0
,
ERROR_MSG_MAX_LEN
));
record
.
setSendTime
(
success
?
LocalDateTime
.
now
()
:
null
);
renewalRemindPolicyMailService
.
saveOrUpdateByUk
(
record
);
}
private
Set
<
String
>
buildSucceededKeys
(
List
<
RenewalRemindPolicyMail
>
records
)
{
Set
<
String
>
keys
=
new
HashSet
<>();
if
(
CollectionUtils
.
isEmpty
(
records
))
{
return
keys
;
}
for
(
RenewalRemindPolicyMail
record
:
records
)
{
if
(
RenewalRemindStatusEnum
.
SUCCESS
.
getItemValue
().
equals
(
record
.
getStatus
()))
{
keys
.
add
(
mailKey
(
record
.
getPolicyFollowId
(),
record
.
getMailType
()));
}
}
return
keys
;
}
private
String
mailKey
(
Long
policyFollowId
,
String
mailType
)
{
return
policyFollowId
+
"|"
+
mailType
;
}
private
String
failureDesc
(
RenewalDuePolicy
item
,
RenewalMailTypeEnum
type
,
String
message
)
{
return
StringUtils
.
defaultIfBlank
(
item
.
getPolicyFollow
().
getPolicyNo
(),
"id="
+
item
.
getPolicyFollow
().
getId
())
+
"("
+
type
.
getItemValue
()
+
"): "
+
StringUtils
.
substring
(
StringUtils
.
defaultString
(
message
),
0
,
200
);
}
private
void
saveBatch
(
RenewalRemindBatch
exist
,
YearMonth
execMonth
,
RenewalReminderData
data
,
String
fileName
,
RenewalRemindStatusEnum
status
,
String
errorMsg
,
String
recipients
,
int
dunCount
,
int
mailFailCount
)
{
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
.
setDunCount
(
dunCount
);
batch
.
setMailFailCount
(
mailFailCount
);
batch
.
setSkippedCount
(
data
!=
null
&&
data
.
getSkipped
()
!=
null
?
data
.
getSkipped
().
size
()
:
0
);
batch
.
setRecipients
(
recipients
);
batch
.
setFileName
(
fileName
);
batch
.
setStatus
(
status
.
getItemValue
());
batch
.
setErrorMsg
(
errorMsg
);
// 仅实际发信成功才记录发送时间;EMPTY 未发信、FAIL 发送失败,均置空
batch
.
setSendTime
(
status
==
RenewalRemindStatusEnum
.
SUCCESS
?
LocalDateTime
.
now
()
:
null
);
batch
.
setUpdateTime
(
LocalDateTime
.
now
());
renewalRemindBatchService
.
saveOrUpdate
(
batch
);
}
private
void
validateConfig
(
List
<
String
>
summaryRecipients
,
List
<
String
>
policyToList
,
List
<
String
>
policyCcList
)
{
if
(
CollectionUtils
.
isEmpty
(
summaryRecipients
))
{
throw
new
BusinessException
(
"续期提醒汇总收件人未配置,请设置 renewal.reminder.recipients"
);
}
summaryRecipients
.
forEach
(
a
->
assertValidEmail
(
a
,
"renewal.reminder.recipients"
));
if
(
policyToList
.
size
()
!=
1
)
{
throw
new
BusinessException
(
"renewal.reminder.policy.to 必须且只能配置一个邮箱地址,实际: "
+
policyToList
.
size
()
+
" 个"
);
}
assertValidEmail
(
policyToList
.
get
(
0
),
"renewal.reminder.policy.to"
);
policyCcList
.
forEach
(
a
->
assertValidEmail
(
a
,
"renewal.reminder.policy.cc"
));
if
(
defaultGraceDays
<=
0
)
{
throw
new
BusinessException
(
"renewal.reminder.default-grace-days 必须为正整数,实际: "
+
defaultGraceDays
);
}
if
(
sendIntervalMs
<
0
)
{
throw
new
BusinessException
(
"renewal.reminder.send-interval-ms 不能为负数,实际: "
+
sendIntervalMs
);
}
}
private
void
assertValidEmail
(
String
address
,
String
configKey
)
{
if
(!
EMAIL_PATTERN
.
matcher
(
address
).
matches
())
{
throw
new
BusinessException
(
"配置 "
+
configKey
+
" 存在非法邮箱地址: "
+
address
);
}
}
private
List
<
String
>
parseAddressList
(
String
config
)
{
return
Arrays
.
stream
(
StringUtils
.
defaultString
(
config
).
split
(
","
))
.
map
(
String:
:
trim
)
.
filter
(
StringUtils:
:
isNotBlank
)
.
collect
(
Collectors
.
toList
());
}
private
String
buildSubject
(
RenewalReminderData
data
)
{
LocalDate
start
=
data
.
getWindowStart
();
LocalDate
end
=
data
.
getWindowEnd
();
// 跨年发生在执行月 11 月(窗口为 12/1~次年 1/31):同月省年、跨年带两年
String
period
=
start
.
getYear
()
==
end
.
getYear
()
?
String
.
format
(
"%d年%d-%d月"
,
start
.
getYear
(),
start
.
getMonthValue
(),
end
.
getMonthValue
())
:
String
.
format
(
"%d年%d月-%d年%d月"
,
start
.
getYear
(),
start
.
getMonthValue
(),
end
.
getYear
(),
end
.
getMonthValue
());
return
String
.
format
(
"【续期提醒】%s到期保单清单(共%d张)"
,
period
,
data
.
getDuePolicies
().
size
());
}
private
String
buildContent
(
RenewalReminderData
data
,
int
missingTotal
)
{
StringBuilder
sb
=
new
StringBuilder
();
sb
.
append
(
String
.
format
(
"以下保单将于 %s 至 %s 到期应缴续期保费,共 %d 张,详见附件。"
,
data
.
getWindowStart
(),
data
.
getWindowEnd
(),
data
.
getDuePolicies
().
size
()));
if
(
CollectionUtils
.
isNotEmpty
(
data
.
getDunPolicies
()))
{
sb
.
append
(
String
.
format
(
" 本期宽限期跟进 %d 封。"
,
data
.
getDunPolicies
().
size
()));
}
// 两个口径分开:跳过是整张保单(张),待核对是字段项(项),不可合并计数
int
skippedCount
=
data
.
getSkipped
()
==
null
?
0
:
data
.
getSkipped
().
size
();
if
(
skippedCount
>
0
)
{
sb
.
append
(
String
.
format
(
" 数据异常跳过 %d 张,请核对源数据(详见跑批日志)。"
,
skippedCount
));
}
if
(
missingTotal
>
0
)
{
sb
.
append
(
String
.
format
(
" 字段待核对 %d 项。"
,
missingTotal
));
}
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
);
}
}
}
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalMailServiceImpl.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
api
.
service
.
impl
;
import
com.yd.csf.api.service.RenewalMailService
;
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.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.InternetAddress
;
import
javax.mail.internet.MimeMessage
;
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
);
}
}
@Override
public
void
sendHtml
(
String
to
,
List
<
String
>
ccList
,
String
subject
,
String
htmlContent
)
{
try
{
MimeMessage
message
=
javaMailSender
.
createMimeMessage
();
MimeMessageHelper
helper
=
new
MimeMessageHelper
(
message
,
false
,
StandardCharsets
.
UTF_8
.
name
());
helper
.
setFrom
(
new
InternetAddress
(
resolveFrom
(),
"续期保费通知"
,
StandardCharsets
.
UTF_8
.
name
()));
helper
.
setTo
(
to
);
if
(
CollectionUtils
.
isNotEmpty
(
ccList
))
{
helper
.
setCc
(
ccList
.
toArray
(
new
String
[
0
]));
}
helper
.
setSubject
(
subject
);
helper
.
setText
(
htmlContent
,
true
);
javaMailSender
.
send
(
message
);
log
.
info
(
"[续期提醒-邮件] HTML发送成功,To={}, CC={}, 主题={}"
,
to
,
ccList
,
subject
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[续期提醒-邮件] HTML发送失败,To={}, 主题={}"
,
to
,
subject
,
e
);
throw
new
RuntimeException
(
"续期提醒HTML邮件发送失败: "
+
e
.
getMessage
(),
e
);
}
}
private
String
resolveFrom
()
{
String
from
=
StringUtils
.
defaultIfBlank
(
mailFrom
,
mailUsername
);
if
(
StringUtils
.
isBlank
(
from
))
{
throw
new
IllegalStateException
(
"未配置发件人:renewal.reminder.mail-from 或 spring.mail.username"
);
}
return
from
;
}
}
yd-csf-api/src/main/resources/logback-spring.xml.bak
View file @
b1cd81e8
...
...
@@ -13,6 +13,8 @@
<rollingPolicy
class=
"ch.qos.logback.core.rolling.TimeBasedRollingPolicy"
>
<fileNamePattern>
${BASE_LOG_DIR}/%d{yyyy-MM-dd}/app.log
</fileNamePattern>
<maxHistory>
30
</maxHistory>
<!-- 日志已 bind mount 到宿主机,限制总大小防止高频部署写满磁盘 -->
<totalSizeCap>
2GB
</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
...
...
yd-csf-service/src/main/java/com/yd/csf/service/component/TemplateStyleStrategy.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
;
import
com.alibaba.excel.write.handler.CellWriteHandler
;
import
com.alibaba.excel.write.handler.SheetWriteHandler
;
import
com.alibaba.excel.write.handler.context.CellWriteHandlerContext
;
import
com.alibaba.excel.write.handler.context.SheetWriteHandlerContext
;
import
org.apache.poi.ss.usermodel.BorderStyle
;
import
org.apache.poi.ss.usermodel.Cell
;
import
org.apache.poi.ss.usermodel.CellStyle
;
import
org.apache.poi.ss.usermodel.Font
;
import
org.apache.poi.ss.usermodel.HorizontalAlignment
;
import
org.apache.poi.ss.usermodel.IndexedColors
;
import
org.apache.poi.ss.usermodel.Sheet
;
import
org.apache.poi.ss.usermodel.VerticalAlignment
;
import
org.apache.poi.ss.usermodel.Workbook
;
/**
* 严格复刻 policy报表模版.xlsx 的样式:
* 宋体 11 不加粗、四边细边框、垂直居中;对齐与数字格式按列区分;冻结首行;按模板列宽。
* 模板中表头与数据使用同一套样式。
*/
public
class
TemplateStyleStrategy
implements
SheetWriteHandler
,
CellWriteHandler
{
/** 文本:常规左对齐(模板 cellXfs 8) */
private
static
final
int
TEXT_LEFT
=
0
;
/** 居中(模板 9:D/U/V) */
private
static
final
int
CENTER
=
1
;
/** 数值 #,##0.00 左对齐(模板 10:J 每期保费) */
private
static
final
int
NUMBER_LEFT
=
2
;
/** 常规右对齐(模板 13:L/M/S) */
private
static
final
int
GENERAL_RIGHT
=
3
;
/** 日期 yyyy/MM/dd 右对齐(模板 12:N/O/Q;P/R 写入真实日期需日期格式) */
private
static
final
int
DATE_RIGHT
=
4
;
/** 30 列(A..AD)对应的样式类别 */
private
static
final
int
[]
STYLE_BY_COL
=
{
TEXT_LEFT
,
// A id
TEXT_LEFT
,
// B 保单号
TEXT_LEFT
,
// C 产品名称
CENTER
,
// D PI
TEXT_LEFT
,
// E 保单持有人(中文)
TEXT_LEFT
,
// F 保单持有人(英文)
TEXT_LEFT
,
// G 受保人(中文)
TEXT_LEFT
,
// H 受保人(英文)
TEXT_LEFT
,
// I 状态
NUMBER_LEFT
,
// J 每期保费
TEXT_LEFT
,
// K 保单币种
GENERAL_RIGHT
,
// L 征费
GENERAL_RIGHT
,
// M 总保费及征费
DATE_RIGHT
,
// N 生效日
DATE_RIGHT
,
// O 签单日
DATE_RIGHT
,
// P 缮发日期
DATE_RIGHT
,
// Q 冷静期结束日期
DATE_RIGHT
,
// R 保费到期日
GENERAL_RIGHT
,
// S 缴费年期
TEXT_LEFT
,
// T 缴费频率
CENTER
,
// U 是否预缴
CENTER
,
// V 预缴年期
TEXT_LEFT
,
// W 产品险种
TEXT_LEFT
,
// X 保险公司
TEXT_LEFT
,
// Y 出单经纪公司
TEXT_LEFT
,
// Z 转介人
TEXT_LEFT
,
// AA 签单员
TEXT_LEFT
,
// AB 签单员牌照号码
TEXT_LEFT
,
// AC 预约编号
TEXT_LEFT
// AD 签单地点
};
/** 模板列宽(字符单位);日期列保证最小宽度避免显示 ### */
private
static
final
double
[]
COL_WIDTH
=
{
4.43
,
22.00
,
35.57
,
7.14
,
19.29
,
20.57
,
23.14
,
16.14
,
12.57
,
18.14
,
15.14
,
10.71
,
16.29
,
12.43
,
12.43
,
12.00
,
16.29
,
12.00
,
12.29
,
9.57
,
9.71
,
9.57
,
9.57
,
11.71
,
14.00
,
16.14
,
14.86
,
16.14
,
20.00
,
34.00
};
private
CellStyle
[]
styles
;
/**
* 必须在内置默认样式策略(DefaultStyle,order=0)之后执行,
* 否则其按列设置的单元格样式会被默认样式覆盖回 Calibri 无框。
*/
@Override
public
int
order
()
{
return
Integer
.
MAX_VALUE
;
}
@Override
public
void
afterSheetCreate
(
SheetWriteHandlerContext
context
)
{
Sheet
sheet
=
context
.
getWriteSheetHolder
().
getSheet
();
Workbook
workbook
=
context
.
getWriteWorkbookHolder
().
getWorkbook
();
initStyles
(
workbook
);
for
(
int
i
=
0
;
i
<
COL_WIDTH
.
length
;
i
++)
{
sheet
.
setColumnWidth
(
i
,
(
int
)
Math
.
round
(
COL_WIDTH
[
i
]
*
256
));
}
// 冻结表头首行
sheet
.
createFreezePane
(
0
,
1
);
}
@Override
public
void
afterCellDispose
(
CellWriteHandlerContext
context
)
{
Cell
cell
=
context
.
getCell
();
int
col
=
cell
.
getColumnIndex
();
if
(
styles
==
null
||
col
<
0
||
col
>=
STYLE_BY_COL
.
length
)
{
return
;
}
// 表头与数据使用同一套按列样式
cell
.
setCellStyle
(
styles
[
STYLE_BY_COL
[
col
]]);
}
private
void
initStyles
(
Workbook
workbook
)
{
Font
font
=
workbook
.
createFont
();
font
.
setFontName
(
"宋体"
);
font
.
setFontHeightInPoints
((
short
)
11
);
font
.
setBold
(
false
);
styles
=
new
CellStyle
[
5
];
styles
[
TEXT_LEFT
]
=
baseStyle
(
workbook
,
font
,
HorizontalAlignment
.
LEFT
,
null
);
styles
[
CENTER
]
=
baseStyle
(
workbook
,
font
,
HorizontalAlignment
.
CENTER
,
null
);
styles
[
NUMBER_LEFT
]
=
baseStyle
(
workbook
,
font
,
HorizontalAlignment
.
LEFT
,
"#,##0.00"
);
styles
[
GENERAL_RIGHT
]
=
baseStyle
(
workbook
,
font
,
HorizontalAlignment
.
RIGHT
,
null
);
styles
[
DATE_RIGHT
]
=
baseStyle
(
workbook
,
font
,
HorizontalAlignment
.
RIGHT
,
"yyyy/MM/dd"
);
}
private
CellStyle
baseStyle
(
Workbook
workbook
,
Font
font
,
HorizontalAlignment
align
,
String
fmt
)
{
CellStyle
style
=
workbook
.
createCellStyle
();
style
.
setFont
(
font
);
style
.
setBorderTop
(
BorderStyle
.
THIN
);
style
.
setBorderBottom
(
BorderStyle
.
THIN
);
style
.
setBorderLeft
(
BorderStyle
.
THIN
);
style
.
setBorderRight
(
BorderStyle
.
THIN
);
style
.
setTopBorderColor
(
IndexedColors
.
BLACK
.
getIndex
());
style
.
setBottomBorderColor
(
IndexedColors
.
BLACK
.
getIndex
());
style
.
setLeftBorderColor
(
IndexedColors
.
BLACK
.
getIndex
());
style
.
setRightBorderColor
(
IndexedColors
.
BLACK
.
getIndex
());
style
.
setVerticalAlignment
(
VerticalAlignment
.
CENTER
);
style
.
setAlignment
(
align
);
if
(
fmt
!=
null
)
{
style
.
setDataFormat
(
workbook
.
createDataFormat
().
getFormat
(
fmt
));
}
return
style
;
}
}
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalDueDateCalculator.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
.
renewal
;
import
com.yd.csf.feign.enums.PaymentFrequencyEnum
;
import
lombok.Getter
;
import
org.apache.commons.lang3.StringUtils
;
import
java.math.BigDecimal
;
import
java.time.LocalDate
;
import
java.util.Objects
;
/**
* 保单续期保费到期日计算器。
*
* <p>规则对齐现有 Excel 模板「保费到期日」公式:</p>
* <ul>
* <li>年缴每 12 个月、季缴每 3 个月、月缴每 1 个月为一个缴费日,以生效日为锚点;</li>
* <li>取第一个严格晚于今天的缴费日;</li>
* <li>缴费期满日 = 生效日 + 缴费年期年数 - 1 天,下一缴费日晚于期满日表示已缴完;</li>
* <li>闰日(2/29)在非闰年由 LocalDate.plusMonths/plusYears 自动钳为 2/28。</li>
* </ul>
*/
public
final
class
RenewalDueDateCalculator
{
private
RenewalDueDateCalculator
()
{
}
/**
* 计算单张保单的下一期保费到期日。
*
* @param effectiveDate 生效日
* @param frequency 缴费频率(PaymentFrequencyEnum 的 itemValue:YEAR/SEASON/MONTH/FULL_PAYMENT)
* @param issueNumber 缴费年期(年),库中类型不规整,可能为 Integer/String/BigDecimal
* @param today 当前日期(参数化便于测试)
* @return 计算结果;不纳入时 dueDate 为空并带跳过原因
*/
public
static
Result
calculate
(
LocalDate
effectiveDate
,
String
frequency
,
Object
issueNumber
,
LocalDate
today
)
{
String
freq
=
StringUtils
.
trimToEmpty
(
frequency
);
if
(
PaymentFrequencyEnum
.
FULL_PAYMENT
.
getItemValue
().
equals
(
freq
))
{
return
Result
.
skip
(
"整付保单无续期保费"
,
false
);
}
int
stepMonths
;
if
(
PaymentFrequencyEnum
.
YEAR
.
getItemValue
().
equals
(
freq
))
{
stepMonths
=
12
;
}
else
if
(
PaymentFrequencyEnum
.
SEASON
.
getItemValue
().
equals
(
freq
))
{
stepMonths
=
3
;
}
else
if
(
PaymentFrequencyEnum
.
MONTH
.
getItemValue
().
equals
(
freq
))
{
stepMonths
=
1
;
}
else
if
(
StringUtils
.
isBlank
(
freq
))
{
return
Result
.
skip
(
"缴费频率为空"
,
true
);
}
else
{
return
Result
.
skip
(
"无法识别的缴费频率: "
+
freq
,
true
);
}
if
(
Objects
.
isNull
(
effectiveDate
))
{
return
Result
.
skip
(
"生效日为空"
,
true
);
}
Integer
years
=
parseIssueNumber
(
issueNumber
);
if
(
years
==
null
)
{
return
Result
.
skip
(
"缴费年期为空或无法解析: "
+
issueNumber
,
true
);
}
LocalDate
endDate
=
effectiveDate
.
plusYears
(
years
).
minusDays
(
1
);
LocalDate
candidate
=
effectiveDate
;
while
(!
candidate
.
isAfter
(
today
))
{
candidate
=
candidate
.
plusMonths
(
stepMonths
);
if
(
candidate
.
isAfter
(
endDate
))
{
return
Result
.
skip
(
"保单缴费期已满"
,
false
);
}
}
return
Result
.
included
(
candidate
);
}
/**
* 计算单张保单的当期缴费日:缴费日序列(k=0 首期 = 生效日)中不晚于基准日的最大者。
*
* <p>与 {@link #calculate} 严格互补:当期缴费日 ≤ 基准日 < 下一缴费日。
* 基准日早于首期(生效日)时返回空;缴费期满后的日期不参与序列,保单缴完时
* 返回最后一个有效缴费日(由上层配合宽限期窗口判断是否命中 R2)。</p>
*
* @param effectiveDate 生效日
* @param frequency 缴费频率(PaymentFrequencyEnum 的 itemValue)
* @param issueNumber 缴费年期(年)
* @param baseDate 基准日(执行月 1 号)
* @return 计算结果;无当期缴费日时 dueDate 为空并带跳过原因
*/
public
static
Result
currentDueDate
(
LocalDate
effectiveDate
,
String
frequency
,
Object
issueNumber
,
LocalDate
baseDate
)
{
String
freq
=
StringUtils
.
trimToEmpty
(
frequency
);
if
(
PaymentFrequencyEnum
.
FULL_PAYMENT
.
getItemValue
().
equals
(
freq
))
{
return
Result
.
skip
(
"整付保单无续期保费"
,
false
);
}
int
stepMonths
;
if
(
PaymentFrequencyEnum
.
YEAR
.
getItemValue
().
equals
(
freq
))
{
stepMonths
=
12
;
}
else
if
(
PaymentFrequencyEnum
.
SEASON
.
getItemValue
().
equals
(
freq
))
{
stepMonths
=
3
;
}
else
if
(
PaymentFrequencyEnum
.
MONTH
.
getItemValue
().
equals
(
freq
))
{
stepMonths
=
1
;
}
else
if
(
StringUtils
.
isBlank
(
freq
))
{
return
Result
.
skip
(
"缴费频率为空"
,
true
);
}
else
{
return
Result
.
skip
(
"无法识别的缴费频率: "
+
freq
,
true
);
}
if
(
Objects
.
isNull
(
effectiveDate
))
{
return
Result
.
skip
(
"生效日为空"
,
true
);
}
if
(
Objects
.
isNull
(
baseDate
))
{
return
Result
.
skip
(
"基准日为空"
,
true
);
}
Integer
years
=
parseIssueNumber
(
issueNumber
);
if
(
years
==
null
)
{
return
Result
.
skip
(
"缴费年期为空或无法解析: "
+
issueNumber
,
true
);
}
LocalDate
endDate
=
effectiveDate
.
plusYears
(
years
).
minusDays
(
1
);
LocalDate
last
=
null
;
LocalDate
candidate
=
effectiveDate
;
while
(!
candidate
.
isAfter
(
baseDate
))
{
if
(
candidate
.
isAfter
(
endDate
))
{
break
;
}
last
=
candidate
;
candidate
=
candidate
.
plusMonths
(
stepMonths
);
}
return
last
==
null
?
Result
.
skip
(
"基准日早于保单首期缴费日"
,
false
)
:
Result
.
included
(
last
);
}
/**
* 宽限期到期日 = 缴费日 + 宽限天数 - 1(含头尾:6/20 起计 31 天 = 7/20)。
*
* @param dueDate 缴费日
* @param graceDays 宽限天数(调用方保证为正数,空值已按配置兜底)
* @return 宽限期到期日;入参非法时返回 null
*/
public
static
LocalDate
graceEndDate
(
LocalDate
dueDate
,
int
graceDays
)
{
if
(
dueDate
==
null
||
graceDays
<=
0
)
{
return
null
;
}
return
dueDate
.
plusDays
((
long
)
graceDays
-
1
);
}
/**
* 解析缴费年期为整数年,兼容 "5"、"5.0"、5、5.00(BigDecimal) 等整数存储形态;
* 非正、非整数年(如 5.9,旧逻辑静默截断为 5)一律判空,由上层计异常。
*/
private
static
Integer
parseIssueNumber
(
Object
issueNumber
)
{
if
(
Objects
.
isNull
(
issueNumber
))
{
return
null
;
}
BigDecimal
bd
;
if
(
issueNumber
instanceof
BigDecimal
)
{
bd
=
(
BigDecimal
)
issueNumber
;
}
else
if
(
issueNumber
instanceof
Number
)
{
bd
=
new
BigDecimal
(
issueNumber
.
toString
());
}
else
{
String
s
=
StringUtils
.
trimToNull
(
issueNumber
.
toString
());
if
(
s
==
null
)
{
return
null
;
}
try
{
bd
=
new
BigDecimal
(
s
);
}
catch
(
NumberFormatException
e
)
{
return
null
;
}
}
if
(
bd
.
signum
()
<=
0
||
bd
.
stripTrailingZeros
().
scale
()
>
0
)
{
return
null
;
}
try
{
return
bd
.
intValueExact
();
}
catch
(
ArithmeticException
e
)
{
return
null
;
}
}
/**
* 计算结果:dueDate 非空表示纳入提醒,为空时 skipReason 说明原因。
*/
@Getter
public
static
class
Result
{
private
final
LocalDate
dueDate
;
private
final
String
skipReason
;
/**
* 是否为异常数据跳过(缺字段/无法解析);整付、缴完等正常排除为 false
*/
private
final
boolean
abnormal
;
private
Result
(
LocalDate
dueDate
,
String
skipReason
,
boolean
abnormal
)
{
this
.
dueDate
=
dueDate
;
this
.
skipReason
=
skipReason
;
this
.
abnormal
=
abnormal
;
}
public
static
Result
included
(
LocalDate
dueDate
)
{
return
new
Result
(
dueDate
,
null
,
false
);
}
public
static
Result
skip
(
String
reason
,
boolean
abnormal
)
{
return
new
Result
(
null
,
reason
,
abnormal
);
}
public
boolean
isIncluded
()
{
return
dueDate
!=
null
;
}
}
}
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalDuePolicy.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
.
renewal
;
import
com.yd.csf.service.model.PolicyFollow
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
java.time.LocalDate
;
/**
* 命中续期提醒的保单及其缴费日信息。
* R1 轮次 {@code dueDate} 为下一保费到期日;R2 轮次为当期缴费日。
*/
@Data
@AllArgsConstructor
public
class
RenewalDuePolicy
{
private
PolicyFollow
policyFollow
;
/**
* 保费到期日(R1=下一缴费日,R2=当期缴费日)
*/
private
LocalDate
dueDate
;
/**
* 宽限期到期日(dueDate + 宽限天数 - 1,含头尾)
*/
private
LocalDate
graceEnd
;
/**
* true=保单 grace_period 为空/非正,按配置默认宽限天数估算,邮件需标注「待核对」
*/
private
boolean
graceFallback
;
/**
* 实际使用的宽限天数(兜底时为默认值)
*/
private
int
graceDays
;
}
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalPolicyMailBuilder.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
.
renewal
;
import
com.yd.csf.service.enums.RenewalMailTypeEnum
;
import
com.yd.csf.service.model.PolicyFollow
;
import
com.yd.csf.service.utils.SimpleDateUtils
;
import
lombok.AllArgsConstructor
;
import
lombok.Getter
;
import
org.apache.commons.lang3.StringUtils
;
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
String
EMPHASIS_RED
=
"#FF0000"
;
/**
* 标签对齐宽度(全角字符数,含冒号);最长标签「保单持有人:」为 6
*/
private
static
final
int
LABEL_WIDTH
=
6
;
/**
* 构建一封逐单通知邮件。
*
* @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
);
// 兜底天数取自筛选侧解析结果(item.graceDays),模板不重复持有配置
String
graceNoteSc
=
item
.
isGraceFallback
()
?
"(宽限天数待核对(按默认 "
+
item
.
getGraceDays
()
+
" 天估算))"
:
"(获取保费到期日起计"
+
item
.
getGraceDays
()
+
"天)"
;
String
graceNoteTc
=
item
.
isGraceFallback
()
?
"(寬限天數待核對(按預設 "
+
item
.
getGraceDays
()
+
" 天估算))"
:
"(获取保费到期日起计"
+
item
.
getGraceDays
()
+
"天)"
;
// 主题进入邮件头:剥除控制字符(CR/LF 截断头注入,其余 Cntrl 破坏头编码)
String
subject
=
sanitizeHeader
((
type
==
RenewalMailTypeEnum
.
R2
?
"【宽限期跟进】"
:
""
)
+
"续期保费通知-【"
+
policyNo
+
"】"
);
String
html
=
"<div style=\"font-family:"
+
FONT
+
";font-size:14px;color:#000000;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
;
String
boldDue
=
boldUnderline
(
dueDateText
);
if
(
type
==
RenewalMailTypeEnum
.
R2
)
{
opening
=
l
.
openingR2Prefix
+
boldDue
+
l
.
openingR2Middle
+
boldUnderline
(
graceEndText
)
+
l
.
openingR2Suffix
;
}
else
{
opening
=
l
.
openingR1Prefix
+
boldDue
+
l
.
openingR1Suffix
;
}
StringBuilder
sb
=
new
StringBuilder
();
sb
.
append
(
"<p style=\"margin:0 0 8px 0;font-weight:bold;\">"
).
append
(
l
.
segmentTitle
).
append
(
"</p>"
);
sb
.
append
(
"<p style=\"margin:0 0 10px 0;\">"
).
append
(
opening
).
append
(
"</p>"
);
// 保单资料:每字段单独一行,标签全角补齐对齐
sb
.
append
(
"<div style=\"line-height:2;\">"
);
sb
.
append
(
fieldLine
(
l
.
company
,
markOrValue
(
company
,
missingMark
)));
sb
.
append
(
fieldLine
(
l
.
product
,
markOrValue
(
product
,
missingMark
)));
sb
.
append
(
fieldLine
(
l
.
policyNo
,
policyNo
));
sb
.
append
(
fieldLine
(
l
.
holder
,
markOrValue
(
holder
,
missingMark
)));
sb
.
append
(
fieldLine
(
l
.
insured
,
markOrValue
(
insured
,
missingMark
)));
sb
.
append
(
fieldLine
(
l
.
premiumAndLevy
,
markOrValue
(
money
,
missingMark
)));
sb
.
append
(
fieldLine
(
l
.
effectiveDate
,
markOrValue
(
effectiveText
,
missingMark
)));
sb
.
append
(
"</div>"
);
// 应缴总保费:标签+金额整行红色加粗
sb
.
append
(
"<div style=\"margin-top:14px;color:"
).
append
(
EMPHASIS_RED
)
.
append
(
";font-weight:bold;\">"
)
.
append
(
l
.
totalPremium
).
append
(
":"
).
append
(
markOrValue
(
money
,
missingMark
))
.
append
(
"</div>"
);
// 备注:整行红色、常规字重(繁体段文案按需求稿原文)
sb
.
append
(
"<div style=\"margin-top:4px;color:"
).
append
(
EMPHASIS_RED
).
append
(
";\">"
)
.
append
(
l
.
remark
).
append
(
"</div>"
);
// 到期日区域与备注间空一行
sb
.
append
(
"<div style=\"margin-top:22px;\">"
)
.
append
(
l
.
dueDateLabel
).
append
(
":"
).
append
(
boldDue
)
.
append
(
"</div>"
);
sb
.
append
(
"<div style=\"margin-top:6px;\">"
)
.
append
(
l
.
graceEndLabel
).
append
(
":"
).
append
(
boldUnderline
(
graceEndText
))
.
append
(
" "
).
append
(
graceNote
)
.
append
(
"</div>"
);
return
sb
.
toString
();
}
private
String
fieldLine
(
String
label
,
String
value
)
{
return
"<div>"
+
padLabel
(
label
)
+
value
+
"</div>"
;
}
private
String
boldUnderline
(
String
text
)
{
return
"<span style=\"font-weight:bold;text-decoration:underline;\">"
+
text
+
"</span>"
;
}
/**
* 标签(含全角冒号)右侧补全角空格到固定宽度,使各字段值纵向对齐
*/
private
String
padLabel
(
String
labelWithColon
)
{
StringBuilder
sb
=
new
StringBuilder
(
labelWithColon
);
while
(
sb
.
length
()
<
LABEL_WIDTH
)
{
sb
.
append
(
' '
);
}
return
sb
.
toString
();
}
/**
* 取非空字段值并 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
)
{
// DecimalFormat 非线程安全,局部创建
String
number
=
new
DecimalFormat
(
"#,##0.00"
).
format
(
total
);
return
currency
==
null
?
number
:
esc
(
currency
)
+
" "
+
number
;
}
/**
* 剥除主题中的 ASCII 控制字符(含 CR/LF/Tab),防邮件头注入与非法头。
*/
private
String
sanitizeHeader
(
String
header
)
{
return
header
.
replaceAll
(
"\\p{Cntrl}"
,
""
);
}
/**
* 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
(
"【繁體】"
,
"產品公司:"
,
"計劃名稱:"
,
"保單號碼:"
,
"保單生效日:"
,
"保單持有人:"
,
"受保人:"
,
"保費及征費:"
,
"應繳總保費"
,
// 繁体段备注按需求稿原文(「备注」「客户」保留稿中用字)
"备注:此金額未計入保費優惠,保費優惠待確認後通知客户。"
,
"保費到期日"
,
"寬限期到期日"
,
"以下保單的保費將於"
,
"到期,請在此日期前繳付應繳保費。保單資料如下:"
,
"以下保單的保費已於"
,
"到期,寬限期將於"
,
"屆滿,請盡快跟進確認繳費情況。保單資料如下:"
);
}
}
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalPolicyReportService.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
.
renewal
;
import
com.alibaba.excel.EasyExcel
;
import
com.yd.csf.service.component.TemplateStyleStrategy
;
import
com.yd.csf.service.vo.RenewalPolicyExportDTO
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.springframework.stereotype.Component
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
import
java.time.LocalDate
;
import
java.time.format.DateTimeFormatter
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 续期保单清单 Excel 生成(30 列,对齐 policy报表模版.xlsx)
*/
@Component
public
class
RenewalPolicyReportService
{
private
static
final
String
FILE_NAME_PATTERN
=
"续期保单清单_%s-%s_%s.xlsx"
;
private
static
final
DateTimeFormatter
MONTH_FMT
=
DateTimeFormatter
.
ofPattern
(
"yyyyMM"
);
private
static
final
DateTimeFormatter
DAY_FMT
=
DateTimeFormatter
.
ofPattern
(
"yyyyMMdd"
);
/**
* 生成清单文件字节
*/
public
byte
[]
generate
(
RenewalReminderData
data
)
{
List
<
RenewalPolicyExportDTO
>
rows
=
toRows
(
data
);
try
(
ByteArrayOutputStream
out
=
new
ByteArrayOutputStream
())
{
EasyExcel
.
write
(
out
,
RenewalPolicyExportDTO
.
class
)
.
useDefaultStyle
(
false
)
.
registerWriteHandler
(
new
TemplateStyleStrategy
())
.
sheet
(
"续期保单"
)
.
doWrite
(
rows
);
return
out
.
toByteArray
();
}
catch
(
IOException
e
)
{
throw
new
RuntimeException
(
"生成续期保单清单失败"
,
e
);
}
}
/**
* 文件名:续期保单清单_<窗口首月>-<窗口末月>_<生成日>.xlsx
*/
public
String
buildFileName
(
RenewalReminderData
data
,
LocalDate
today
)
{
return
String
.
format
(
FILE_NAME_PATTERN
,
data
.
getWindowStart
().
format
(
MONTH_FMT
),
data
.
getWindowEnd
().
format
(
MONTH_FMT
),
today
.
format
(
DAY_FMT
));
}
private
List
<
RenewalPolicyExportDTO
>
toRows
(
RenewalReminderData
data
)
{
List
<
RenewalPolicyExportDTO
>
rows
=
new
ArrayList
<>();
if
(
data
==
null
||
CollectionUtils
.
isEmpty
(
data
.
getDuePolicies
()))
{
return
rows
;
}
for
(
RenewalDuePolicy
item
:
data
.
getDuePolicies
())
{
java
.
util
.
Date
dueDate
=
java
.
util
.
Date
.
from
(
item
.
getDueDate
()
.
atStartOfDay
(
java
.
time
.
ZoneId
.
systemDefault
()).
toInstant
());
rows
.
add
(
RenewalPolicyExportDTO
.
toRow
(
item
.
getPolicyFollow
(),
dueDate
));
}
return
rows
;
}
}
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalPremiumUtils.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
.
renewal
;
import
com.yd.csf.service.model.PolicyFollow
;
import
org.apache.commons.lang3.StringUtils
;
import
java.math.BigDecimal
;
/**
* 续期提醒金额口径:Excel 报表 M 列与逐单邮件「应缴总保费」的唯一来源。
*/
public
final
class
RenewalPremiumUtils
{
private
RenewalPremiumUtils
()
{
}
/**
* 应缴总保费 = 期交保费 + 保单征费。
* 期交保费与征费均无值时返回 null(调用方展示「待核对」);征费为空或非数字按 0 处理。
*/
public
static
BigDecimal
totalPremiumAndLevy
(
PolicyFollow
p
)
{
BigDecimal
levy
=
parseAmount
(
p
.
getPolicyLevy
());
if
(
p
.
getEachIssuePremium
()
==
null
&&
levy
==
null
)
{
return
null
;
}
BigDecimal
premium
=
p
.
getEachIssuePremium
()
==
null
?
BigDecimal
.
ZERO
:
p
.
getEachIssuePremium
();
return
premium
.
add
(
levy
==
null
?
BigDecimal
.
ZERO
:
levy
);
}
private
static
BigDecimal
parseAmount
(
String
value
)
{
if
(
StringUtils
.
isBlank
(
value
))
{
return
null
;
}
try
{
return
new
BigDecimal
(
value
.
trim
());
}
catch
(
NumberFormatException
e
)
{
return
null
;
}
}
}
yd-csf-service/src/main/java/com/yd/csf/service/component/renewal/RenewalReminderData.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
component
.
renewal
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
java.time.LocalDate
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.stream.Stream
;
/**
* 续期提醒筛选结果:窗口信息、R1/R2 命中保单、异常跳过明细(policyFollowId -> 原因)
*/
@Data
@AllArgsConstructor
public
class
RenewalReminderData
{
/**
* R1 窗口起(M+1月1号)
*/
private
LocalDate
windowStart
;
/**
* R1 窗口止(M+2月月末)
*/
private
LocalDate
windowEnd
;
/**
* R1 到期通知名单(下一保费到期日落入两月窗口)
*/
private
List
<
RenewalDuePolicy
>
duePolicies
;
/**
* R2 宽限期跟进名单(当期已到期、宽限期在基准日至次月月末内届满)
*/
private
List
<
RenewalDuePolicy
>
dunPolicies
;
/**
* 异常跳过明细:policyFollowId(String) -> 跳过原因(缺字段/无法解析/保单号为空等脏数据)
*/
private
Map
<
String
,
String
>
skipped
;
/**
* 宽限天数缺失/非正、按默认值估算的保单数(R1+R2)
*/
public
long
getGraceFallbackCount
()
{
return
Stream
.
concat
(
duePolicies
.
stream
(),
dunPolicies
.
stream
())
.
filter
(
RenewalDuePolicy:
:
isGraceFallback
)
.
count
();
}
}
yd-csf-service/src/main/java/com/yd/csf/service/config/XxlJobConfig.java
View file @
b1cd81e8
//package com.yd.csf.service.config;
//
//import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
//import lombok.extern.slf4j.Slf4j;
//import org.apache.commons.lang3.StringUtils;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//
//@Slf4j
//@Configuration
//@ConditionalOnClass(XxlJobSpringExecutor.class)
//public class XxlJobConfig {
//
// @Value("${xxl.job.admin.addresses:http://139.224.145.34:8686/xxl-job-admin}")
// private String adminAddresses;
//
// @Value("${xxl.job.executor.appname:csf-executor}")
// private String appname;
//
// @Value("${xxl.job.executor.port:9999}")
// private int port;
//
// @Value("${xxl.job.accessToken:default_token}")
// private String accessToken;
//
// @Bean
// public XxlJobSpringExecutor xxlJobExecutor() {
// log.info(">>>>>>>>>>> xxl-job config init. appname: {}, port: {}, accessToken: {}",
// appname, port, StringUtils.isNotBlank(accessToken) ? "已配置" : "未配置");
//
// XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
// xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
// xxlJobSpringExecutor.setAppname(appname);
// xxlJobSpringExecutor.setPort(port);
// // 设置accessToken
// xxlJobSpringExecutor.setAccessToken(accessToken);
// xxlJobSpringExecutor.setLogRetentionDays(30);
//
// return xxlJobSpringExecutor;
// }
//}
\ No newline at end of file
package
com
.
yd
.
csf
.
service
.
config
;
import
com.xxl.job.core.executor.impl.XxlJobSpringExecutor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.lang3.StringUtils
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnClass
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
/**
* XXL-Job 执行器配置。
* 配置项见 Nacos:xxl.job.admin.addresses / accessToken / executor.*
*/
@Slf4j
@Configuration
@ConditionalOnClass
(
XxlJobSpringExecutor
.
class
)
public
class
XxlJobConfig
{
@Value
(
"${xxl.job.admin.addresses:http://139.224.145.34:8686/xxl-job-admin}"
)
private
String
adminAddresses
;
@Value
(
"${xxl.job.accessToken:default_token}"
)
private
String
accessToken
;
@Value
(
"${xxl.job.executor.appname:csf-executor}"
)
private
String
appname
;
/**
* 容器多网卡/Admin 跨网络时显式指定注册 IP,留空自动获取
*/
@Value
(
"${xxl.job.executor.ip:}"
)
private
String
ip
;
/**
* 执行器内嵌 Netty 端口,默认 9999;注意与业务 HTTP 端口(9202)区分
*/
@Value
(
"${xxl.job.executor.port:9999}"
)
private
int
port
;
@Value
(
"${xxl.job.executor.logpath:/data/applogs/xxl-job/jobhandler}"
)
private
String
logPath
;
@Value
(
"${xxl.job.executor.logretentiondays:30}"
)
private
int
logRetentionDays
;
@Bean
(
initMethod
=
"start"
,
destroyMethod
=
"destroy"
)
public
XxlJobSpringExecutor
xxlJobExecutor
()
{
log
.
info
(
">>>>>>>>>>> xxl-job config init. adminAddresses={}, appname={}, ip={}, port={}"
,
adminAddresses
,
appname
,
StringUtils
.
defaultIfBlank
(
ip
,
"auto"
),
port
);
XxlJobSpringExecutor
executor
=
new
XxlJobSpringExecutor
();
executor
.
setAdminAddresses
(
adminAddresses
);
executor
.
setAppname
(
appname
);
executor
.
setIp
(
ip
);
executor
.
setPort
(
port
);
executor
.
setAccessToken
(
accessToken
);
executor
.
setLogPath
(
logPath
);
executor
.
setLogRetentionDays
(
logRetentionDays
);
return
executor
;
}
}
yd-csf-service/src/main/java/com/yd/csf/service/dao/RenewalRemindBatchMapper.java
0 → 100644
View file @
b1cd81e8
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
>
{
}
yd-csf-service/src/main/java/com/yd/csf/service/dao/RenewalRemindPolicyMailMapper.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
dao
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.yd.csf.service.model.RenewalRemindPolicyMail
;
/**
* 针对表【renewal_remind_policy_mail(保单续期提醒逐单邮件发送记录表)】的数据库操作 Mapper
*/
public
interface
RenewalRemindPolicyMailMapper
extends
BaseMapper
<
RenewalRemindPolicyMail
>
{
}
yd-csf-service/src/main/java/com/yd/csf/service/enums/RenewalMailTypeEnum.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
enums
;
/**
* 续期提醒逐单邮件轮次类型
*/
public
enum
RenewalMailTypeEnum
{
R1
(
"到期通知"
,
"R1"
),
R2
(
"宽限期跟进"
,
"R2"
),
;
//字典项标签(名称)
private
String
itemLabel
;
//字典项值
private
String
itemValue
;
RenewalMailTypeEnum
(
String
itemLabel
,
String
itemValue
)
{
this
.
itemLabel
=
itemLabel
;
this
.
itemValue
=
itemValue
;
}
public
String
getItemLabel
()
{
return
itemLabel
;
}
public
String
getItemValue
()
{
return
itemValue
;
}
}
yd-csf-service/src/main/java/com/yd/csf/service/enums/RenewalRemindStatusEnum.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
enums
;
/**
* 保单续期提醒批次发送状态
*/
public
enum
RenewalRemindStatusEnum
{
SUCCESS
(
"成功"
,
"SUCCESS"
),
FAIL
(
"失败"
,
"FAIL"
),
EMPTY
(
"无到期保单"
,
"EMPTY"
),
;
//字典项标签(名称)
private
String
itemLabel
;
//字典项值
private
String
itemValue
;
RenewalRemindStatusEnum
(
String
itemLabel
,
String
itemValue
)
{
this
.
itemLabel
=
itemLabel
;
this
.
itemValue
=
itemValue
;
}
public
String
getItemLabel
()
{
return
itemLabel
;
}
public
String
getItemValue
()
{
return
itemValue
;
}
}
yd-csf-service/src/main/java/com/yd/csf/service/model/RenewalRemindBatch.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
model
;
import
com.baomidou.mybatisplus.annotation.FieldStrategy
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
lombok.Data
;
import
java.io.Serializable
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
/**
* 保单续期提醒发送批次表
*
* @TableName renewal_remind_batch
*/
@TableName
(
value
=
"renewal_remind_batch"
)
@Data
public
class
RenewalRemindBatch
implements
Serializable
{
/**
* 主键
*/
@TableId
(
type
=
IdType
.
AUTO
)
private
Long
id
;
/**
* 批次业务唯一ID
*/
private
String
batchBizId
;
/**
* 执行月(yyyy-MM)
*/
private
String
bizMonth
;
/**
* 本次提醒窗口起(M+1月1号)
*/
private
LocalDate
windowStart
;
/**
* 本次提醒窗口止(M+2月月末)
*/
private
LocalDate
windowEnd
;
/**
* R1到期通知保单条数
*/
private
Integer
policyCount
;
/**
* R2宽限期跟进发送封数
*/
private
Integer
dunCount
;
/**
* 逐单邮件发送失败数
*/
private
Integer
mailFailCount
;
/**
* 因数据异常跳过的保单条数
*/
private
Integer
skippedCount
;
/**
* 实际收件人邮箱(逗号分隔快照);状态切换时允许置空
*/
@TableField
(
updateStrategy
=
FieldStrategy
.
IGNORED
)
private
String
recipients
;
/**
* 附件文件名;状态切换时允许置空
*/
@TableField
(
updateStrategy
=
FieldStrategy
.
IGNORED
)
private
String
fileName
;
/**
* 发送状态:SUCCESS-成功 FAIL-失败 EMPTY-无到期保单
*/
private
String
status
;
/**
* 失败原因摘要;重跑成功后允许置空
*/
@TableField
(
updateStrategy
=
FieldStrategy
.
IGNORED
)
private
String
errorMsg
;
/**
* 发送时间;FAIL 时允许置空
*/
@TableField
(
updateStrategy
=
FieldStrategy
.
IGNORED
)
private
LocalDateTime
sendTime
;
/**
* 创建时间
*/
private
LocalDateTime
createTime
;
/**
* 更新时间
*/
private
LocalDateTime
updateTime
;
@TableField
(
exist
=
false
)
private
static
final
long
serialVersionUID
=
1L
;
}
yd-csf-service/src/main/java/com/yd/csf/service/model/RenewalRemindPolicyMail.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
model
;
import
com.baomidou.mybatisplus.annotation.FieldStrategy
;
import
com.baomidou.mybatisplus.annotation.IdType
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableName
;
import
lombok.Data
;
import
java.io.Serializable
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
/**
* 保单续期提醒逐单邮件发送记录表
*
* @TableName renewal_remind_policy_mail
*/
@TableName
(
value
=
"renewal_remind_policy_mail"
)
@Data
public
class
RenewalRemindPolicyMail
implements
Serializable
{
/**
* 主键
*/
@TableId
(
type
=
IdType
.
AUTO
)
private
Long
id
;
/**
* 邮件业务唯一ID(RM+随机串)
*/
private
String
mailBizId
;
/**
* 执行月(yyyy-MM)
*/
private
String
bizMonth
;
/**
* policy_follow 主键ID
*/
private
Long
policyFollowId
;
/**
* 保单号(冗余留痕)
*/
private
String
policyNo
;
/**
* 轮次类型:R1-到期通知 R2-宽限期跟进
*/
private
String
mailType
;
/**
* 保费到期日(R2为当期缴费日)
*/
private
LocalDate
dueDate
;
/**
* 宽限期到期日
*/
private
LocalDate
graceEnd
;
/**
* 主送人邮箱快照
*/
private
String
toRecipients
;
/**
* 抄送人邮箱快照(逗号分隔,无抄送为空)
*/
private
String
ccRecipients
;
/**
* 发送状态:SUCCESS-成功 FAIL-失败
*/
private
String
status
;
/**
* 失败原因摘要;重发成功后允许置空
*/
@TableField
(
updateStrategy
=
FieldStrategy
.
IGNORED
)
private
String
errorMsg
;
/**
* 发送时间;FAIL 时允许置空
*/
@TableField
(
updateStrategy
=
FieldStrategy
.
IGNORED
)
private
LocalDateTime
sendTime
;
/**
* 创建时间
*/
private
LocalDateTime
createTime
;
/**
* 更新时间
*/
private
LocalDateTime
updateTime
;
@TableField
(
exist
=
false
)
private
static
final
long
serialVersionUID
=
1L
;
}
yd-csf-service/src/main/java/com/yd/csf/service/service/IRenewalRemindBatchService.java
0 → 100644
View file @
b1cd81e8
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
);
}
yd-csf-service/src/main/java/com/yd/csf/service/service/IRenewalRemindPolicyMailService.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
service
;
import
com.baomidou.mybatisplus.extension.service.IService
;
import
com.yd.csf.service.model.RenewalRemindPolicyMail
;
import
java.util.List
;
/**
* 保单续期提醒逐单邮件发送记录 服务类
*/
public
interface
IRenewalRemindPolicyMailService
extends
IService
<
RenewalRemindPolicyMail
>
{
/**
* 查询某执行月全部逐单邮件留痕(R1+R2),用于重跑时构建已成功集合
*
* @param bizMonth 执行月 yyyy-MM
*/
List
<
RenewalRemindPolicyMail
>
listByBizMonth
(
String
bizMonth
);
/**
* 按唯一键 (biz_month, policy_follow_id, mail_type) upsert:
* 无记录插入,有记录更新同一行(FAIL→SUCCESS 复用原行)。
*/
void
saveOrUpdateByUk
(
RenewalRemindPolicyMail
record
);
}
yd-csf-service/src/main/java/com/yd/csf/service/service/RenewalReminderQueryService.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
service
;
import
com.yd.csf.service.component.renewal.RenewalReminderData
;
import
java.time.LocalDate
;
import
java.time.YearMonth
;
/**
* 续期保单筛选:查询生效保单,同一次构建产出两轮名单。
* R1 到期通知窗口 = [执行月+1 月 1 号, 执行月+2 月月末];
* R2 宽限期跟进窗口 = [基准日(执行月 1 号), 执行月次月月末]。
*/
public
interface
RenewalReminderQueryService
{
/**
* 构建指定执行月的续期提醒数据(R1 + R2)。
*
* @param execMonth 执行月
* @param baseDate 基准日(定时跑与补发均固定传执行月 1 号,保证结果可复现)
*/
RenewalReminderData
build
(
YearMonth
execMonth
,
LocalDate
baseDate
);
}
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalRemindBatchServiceImpl.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
service
.
impl
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
com.yd.csf.service.dao.RenewalRemindBatchMapper
;
import
com.yd.csf.service.model.RenewalRemindBatch
;
import
com.yd.csf.service.service.IRenewalRemindBatchService
;
import
org.springframework.stereotype.Service
;
/**
* 保单续期提醒发送批次 服务实现类
*/
@Service
public
class
RenewalRemindBatchServiceImpl
extends
ServiceImpl
<
RenewalRemindBatchMapper
,
RenewalRemindBatch
>
implements
IRenewalRemindBatchService
{
@Override
public
RenewalRemindBatch
queryByBizMonth
(
String
bizMonth
)
{
return
this
.
baseMapper
.
selectOne
(
new
LambdaQueryWrapper
<
RenewalRemindBatch
>()
.
eq
(
RenewalRemindBatch:
:
getBizMonth
,
bizMonth
)
.
last
(
" limit 1 "
));
}
}
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalRemindPolicyMailServiceImpl.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
service
.
impl
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
;
import
com.yd.common.utils.RandomStringGenerator
;
import
com.yd.csf.service.dao.RenewalRemindPolicyMailMapper
;
import
com.yd.csf.service.model.RenewalRemindPolicyMail
;
import
com.yd.csf.service.service.IRenewalRemindPolicyMailService
;
import
org.springframework.stereotype.Service
;
import
java.time.LocalDateTime
;
import
java.util.List
;
/**
* 保单续期提醒逐单邮件发送记录 服务实现类
*/
@Service
public
class
RenewalRemindPolicyMailServiceImpl
extends
ServiceImpl
<
RenewalRemindPolicyMailMapper
,
RenewalRemindPolicyMail
>
implements
IRenewalRemindPolicyMailService
{
private
static
final
String
MAIL_BIZ_ID_PREFIX
=
"RM"
;
@Override
public
List
<
RenewalRemindPolicyMail
>
listByBizMonth
(
String
bizMonth
)
{
return
this
.
baseMapper
.
selectList
(
new
LambdaQueryWrapper
<
RenewalRemindPolicyMail
>()
.
eq
(
RenewalRemindPolicyMail:
:
getBizMonth
,
bizMonth
));
}
@Override
public
void
saveOrUpdateByUk
(
RenewalRemindPolicyMail
record
)
{
RenewalRemindPolicyMail
exist
=
this
.
baseMapper
.
selectOne
(
new
LambdaQueryWrapper
<
RenewalRemindPolicyMail
>()
.
eq
(
RenewalRemindPolicyMail:
:
getBizMonth
,
record
.
getBizMonth
())
.
eq
(
RenewalRemindPolicyMail:
:
getPolicyFollowId
,
record
.
getPolicyFollowId
())
.
eq
(
RenewalRemindPolicyMail:
:
getMailType
,
record
.
getMailType
())
.
last
(
" limit 1 "
));
LocalDateTime
now
=
LocalDateTime
.
now
();
if
(
exist
==
null
)
{
record
.
setMailBizId
(
RandomStringGenerator
.
generateBizId16
(
MAIL_BIZ_ID_PREFIX
));
record
.
setCreateTime
(
now
);
record
.
setUpdateTime
(
now
);
this
.
baseMapper
.
insert
(
record
);
}
else
{
record
.
setId
(
exist
.
getId
());
record
.
setMailBizId
(
exist
.
getMailBizId
());
record
.
setCreateTime
(
exist
.
getCreateTime
());
record
.
setUpdateTime
(
now
);
this
.
baseMapper
.
updateById
(
record
);
}
}
}
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalReminderQueryServiceImpl.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
service
.
impl
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.yd.csf.service.dao.PolicyFollowMapper
;
import
com.yd.csf.service.component.renewal.RenewalDuePolicy
;
import
com.yd.csf.service.component.renewal.RenewalReminderData
;
import
com.yd.csf.service.enums.PolicyFollowStatusEnum
;
import
com.yd.csf.service.model.PolicyFollow
;
import
com.yd.csf.service.service.RenewalReminderQueryService
;
import
com.yd.csf.service.component.renewal.RenewalDueDateCalculator
;
import
com.yd.csf.service.utils.SimpleDateUtils
;
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.YearMonth
;
import
java.util.ArrayList
;
import
java.util.Comparator
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
/**
* 续期保单筛选实现:同一次 build 产出 R1 到期通知与 R2 宽限期跟进两轮名单。
*/
@Service
@Slf4j
public
class
RenewalReminderQueryServiceImpl
implements
RenewalReminderQueryService
{
/**
* 保单 grace_period 为空/非正时的兜底宽限天数
*/
@Value
(
"${renewal.reminder.default-grace-days:31}"
)
private
Integer
configuredDefaultGraceDays
;
@Resource
private
PolicyFollowMapper
policyFollowMapper
;
@Override
public
RenewalReminderData
build
(
YearMonth
execMonth
,
LocalDate
baseDate
)
{
LocalDate
windowStart
=
execMonth
.
plusMonths
(
1
).
atDay
(
1
);
LocalDate
windowEnd
=
execMonth
.
plusMonths
(
2
).
atEndOfMonth
();
// R2 宽限期窗口:[基准日(执行月1号,含), 执行月次月月末(含)]
LocalDate
dunWindowEnd
=
execMonth
.
plusMonths
(
1
).
atEndOfMonth
();
int
defaultGraceDays
=
resolveDefaultGraceDays
();
List
<
PolicyFollow
>
effectivePolicies
=
queryEffectivePolicies
();
List
<
RenewalDuePolicy
>
duePolicies
=
new
ArrayList
<>();
List
<
RenewalDuePolicy
>
dunPolicies
=
new
ArrayList
<>();
Map
<
String
,
String
>
skipped
=
new
LinkedHashMap
<>();
if
(
CollectionUtils
.
isNotEmpty
(
effectivePolicies
))
{
for
(
PolicyFollow
policy
:
effectivePolicies
)
{
// 保单号是主题与留痕标识,为空无法发信:跳过计异常,批次照常,数据修复后重跑可补
if
(
StringUtils
.
isBlank
(
policy
.
getPolicyNo
()))
{
markSkipped
(
skipped
,
policy
,
"保单号为空"
);
continue
;
}
LocalDate
effectiveDate
=
SimpleDateUtils
.
toLocalDate
(
policy
.
getEffectiveDate
());
// R2 先判定(宽限紧迫,发送顺序也在先)
boolean
r2Hit
=
false
;
RenewalDueDateCalculator
.
Result
currentResult
=
RenewalDueDateCalculator
.
currentDueDate
(
effectiveDate
,
policy
.
getPaymentFrequency
(),
policy
.
getIssueNumber
(),
baseDate
);
if
(!
currentResult
.
isIncluded
())
{
// R2 计算异常不影响 R1(入参相同,正常情况下异常原因一致)
markSkippedIfAbnormal
(
skipped
,
policy
,
currentResult
);
}
else
{
LocalDate
currentDueDate
=
currentResult
.
getDueDate
();
// 首期未付新单(k=0,当期缴费日=生效日)不属续期宽限跟进
// 预缴保单一律不催 R2(宁可不催,不可误催);两者均不影响 R1
if
(!
currentDueDate
.
equals
(
effectiveDate
)
&&
!
Integer
.
valueOf
(
1
).
equals
(
policy
.
getIsPrepay
()))
{
Grace
grace
=
resolveGrace
(
policy
,
currentDueDate
,
defaultGraceDays
);
if
(!
grace
.
graceEnd
.
isBefore
(
baseDate
)
&&
!
grace
.
graceEnd
.
isAfter
(
dunWindowEnd
))
{
dunPolicies
.
add
(
new
RenewalDuePolicy
(
policy
,
currentDueDate
,
grace
.
graceEnd
,
grace
.
fallback
,
grace
.
graceDays
));
r2Hit
=
true
;
}
}
}
// R2 已命中则不再发 R1:月缴且缴费日恰为 1 号时,当期宽限届满日恰等于下一缴费日,
// 同一保单会同时落入两轮,按「只催 R2」剔除 R1
if
(
r2Hit
)
{
continue
;
}
// R1:下一保费到期日落入 [M+1月1号, M+2月月末]
RenewalDueDateCalculator
.
Result
nextResult
=
RenewalDueDateCalculator
.
calculate
(
effectiveDate
,
policy
.
getPaymentFrequency
(),
policy
.
getIssueNumber
(),
baseDate
);
if
(!
nextResult
.
isIncluded
())
{
markSkippedIfAbnormal
(
skipped
,
policy
,
nextResult
);
}
else
{
LocalDate
nextDueDate
=
nextResult
.
getDueDate
();
if
(!
nextDueDate
.
isBefore
(
windowStart
)
&&
!
nextDueDate
.
isAfter
(
windowEnd
))
{
Grace
grace
=
resolveGrace
(
policy
,
nextDueDate
,
defaultGraceDays
);
duePolicies
.
add
(
new
RenewalDuePolicy
(
policy
,
nextDueDate
,
grace
.
graceEnd
,
grace
.
fallback
,
grace
.
graceDays
));
}
}
}
}
// R1 按保费到期日升序、R2 按宽限期到期日升序,同日按保单号
Comparator
<
RenewalDuePolicy
>
policyNoOrder
=
Comparator
.
comparing
(
p
->
p
.
getPolicyFollow
().
getPolicyNo
(),
Comparator
.
nullsLast
(
String:
:
compareTo
));
duePolicies
.
sort
(
Comparator
.
comparing
(
RenewalDuePolicy:
:
getDueDate
).
thenComparing
(
policyNoOrder
));
dunPolicies
.
sort
(
Comparator
.
comparing
(
RenewalDuePolicy:
:
getGraceEnd
).
thenComparing
(
policyNoOrder
));
long
graceFallbackCount
=
duePolicies
.
stream
().
filter
(
RenewalDuePolicy:
:
isGraceFallback
).
count
()
+
dunPolicies
.
stream
().
filter
(
RenewalDuePolicy:
:
isGraceFallback
).
count
();
log
.
info
(
"续期提醒筛选完成,R1窗口=[{} ~ {}],生效保单={},R1命中={},R2命中={},宽限兜底={},异常跳过={}"
,
windowStart
,
windowEnd
,
effectivePolicies
==
null
?
0
:
effectivePolicies
.
size
(),
duePolicies
.
size
(),
dunPolicies
.
size
(),
graceFallbackCount
,
skipped
.
size
());
return
new
RenewalReminderData
(
windowStart
,
windowEnd
,
duePolicies
,
dunPolicies
,
skipped
);
}
private
void
markSkippedIfAbnormal
(
Map
<
String
,
String
>
skipped
,
PolicyFollow
policy
,
RenewalDueDateCalculator
.
Result
result
)
{
if
(
result
.
isAbnormal
())
{
markSkipped
(
skipped
,
policy
,
result
.
getSkipReason
());
}
}
/**
* skipped 以 policyFollowId 为键:保单号本身可能为空(空保单号脏数据也要可统计)。
*/
private
void
markSkipped
(
Map
<
String
,
String
>
skipped
,
PolicyFollow
policy
,
String
reason
)
{
String
key
=
String
.
valueOf
(
policy
.
getId
());
if
(
skipped
.
putIfAbsent
(
key
,
reason
)
==
null
)
{
log
.
warn
(
"续期提醒跳过异常保单,policyFollowId={}, policyNo={}, 原因={}"
,
policy
.
getId
(),
policy
.
getPolicyNo
(),
reason
);
}
}
/**
* 解析宽限期到期日;grace_period 为空/非正时按默认天数估算并标记兜底。
*/
private
Grace
resolveGrace
(
PolicyFollow
policy
,
LocalDate
dueDate
,
int
defaultGraceDays
)
{
Integer
gracePeriod
=
policy
.
getGracePeriod
();
boolean
fallback
=
gracePeriod
==
null
||
gracePeriod
<=
0
;
int
graceDays
=
fallback
?
defaultGraceDays
:
gracePeriod
;
LocalDate
graceEnd
=
RenewalDueDateCalculator
.
graceEndDate
(
dueDate
,
graceDays
);
if
(
fallback
)
{
log
.
info
(
"保单宽限期为空或非正,按默认 {} 天估算,policyNo={}, gracePeriod={}"
,
defaultGraceDays
,
policy
.
getPolicyNo
(),
gracePeriod
);
}
return
new
Grace
(
graceEnd
,
fallback
,
graceDays
);
}
private
int
resolveDefaultGraceDays
()
{
if
(
configuredDefaultGraceDays
==
null
||
configuredDefaultGraceDays
<=
0
)
{
log
.
warn
(
"配置 renewal.reminder.default-grace-days={} 非法,回退 31"
,
configuredDefaultGraceDays
);
return
31
;
}
return
configuredDefaultGraceDays
;
}
/**
* 查询全部生效且未删除的保单,只取报表与计算所需字段
*/
private
List
<
PolicyFollow
>
queryEffectivePolicies
()
{
return
policyFollowMapper
.
selectList
(
new
LambdaQueryWrapper
<
PolicyFollow
>()
.
select
(
PolicyFollow:
:
getId
,
PolicyFollow:
:
getPolicyNo
,
PolicyFollow:
:
getProductName
,
PolicyFollow:
:
getPolicyHolder
,
PolicyFollow:
:
getPolicyHolderEn
,
PolicyFollow:
:
getInsured
,
PolicyFollow:
:
getStatus
,
PolicyFollow:
:
getEachIssuePremium
,
PolicyFollow:
:
getPolicyCurrency
,
PolicyFollow:
:
getPolicyLevy
,
PolicyFollow:
:
getEffectiveDate
,
PolicyFollow:
:
getSignDate
,
PolicyFollow:
:
getIssueDate
,
PolicyFollow:
:
getCoolingOffEndDate
,
PolicyFollow:
:
getIssueNumber
,
PolicyFollow:
:
getPaymentFrequency
,
PolicyFollow:
:
getIsPrepay
,
PolicyFollow:
:
getPrepaidTerm
,
PolicyFollow:
:
getGracePeriod
,
PolicyFollow:
:
getProductCate
,
PolicyFollow:
:
getInsuranceCompany
,
PolicyFollow:
:
getReconciliationCompany
,
PolicyFollow:
:
getFirstBroker
,
PolicyFollow:
:
getSigner
,
PolicyFollow:
:
getPracticeCode
,
PolicyFollow:
:
getAppointmentNo
,
PolicyFollow:
:
getSignLocation
)
.
eq
(
PolicyFollow:
:
getStatus
,
PolicyFollowStatusEnum
.
EFFECTIVE
.
getItemValue
())
.
eq
(
PolicyFollow:
:
getIsDeleted
,
0
));
}
/**
* 宽限期解析中间结果
*/
private
static
class
Grace
{
private
final
LocalDate
graceEnd
;
private
final
boolean
fallback
;
private
final
int
graceDays
;
Grace
(
LocalDate
graceEnd
,
boolean
fallback
,
int
graceDays
)
{
this
.
graceEnd
=
graceEnd
;
this
.
fallback
=
fallback
;
this
.
graceDays
=
graceDays
;
}
}
}
yd-csf-service/src/main/java/com/yd/csf/service/utils/SimpleDateUtils.java
0 → 100644
View file @
b1cd81e8
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
yd-csf-service/src/main/java/com/yd/csf/service/vo/RenewalPolicyExportDTO.java
0 → 100644
View file @
b1cd81e8
package
com
.
yd
.
csf
.
service
.
vo
;
import
com.alibaba.excel.annotation.ExcelProperty
;
import
com.alibaba.excel.annotation.format.DateTimeFormat
;
import
com.yd.csf.service.component.renewal.RenewalPremiumUtils
;
import
com.yd.csf.service.model.PolicyFollow
;
import
lombok.Data
;
import
java.math.BigDecimal
;
import
java.util.Date
;
import
java.util.Objects
;
/**
* 保单清单导出 DTO(续期跑批与新单跟进导出共用),列顺序与表头严格对齐 policy报表模版.xlsx(30 列)。
* 字体/边框/对齐/列宽/冻结统一由 {@link com.yd.csf.service.component.TemplateStyleStrategy} 复刻模板。
*/
@Data
public
class
RenewalPolicyExportDTO
{
@ExcelProperty
(
"id"
)
private
Long
id
;
@ExcelProperty
(
"保单号"
)
private
String
policyNo
;
@ExcelProperty
(
"产品名称"
)
private
String
productName
;
@ExcelProperty
(
"PI"
)
private
String
pi
;
@ExcelProperty
(
"保单持有人(中文)"
)
private
String
policyHolder
;
@ExcelProperty
(
"保单持有人(英文)"
)
private
String
policyHolderEn
;
@ExcelProperty
(
"受保人(中文)"
)
private
String
insured
;
@ExcelProperty
(
"受保人(英文)"
)
private
String
insuredEn
;
@ExcelProperty
(
"状态"
)
private
String
status
;
@ExcelProperty
(
"每期保费"
)
private
BigDecimal
eachIssuePremium
;
@ExcelProperty
(
"保单币种"
)
private
String
policyCurrency
;
@ExcelProperty
(
"征费"
)
private
String
policyLevy
;
@ExcelProperty
(
"总保费及征费"
)
private
String
totalPremiumAndLevy
;
@ExcelProperty
(
"生效日"
)
@DateTimeFormat
(
"yyyy/MM/dd"
)
private
Date
effectiveDate
;
@ExcelProperty
(
"签单日"
)
@DateTimeFormat
(
"yyyy/MM/dd"
)
private
Date
signDate
;
@ExcelProperty
(
"缮发日期"
)
@DateTimeFormat
(
"yyyy/MM/dd"
)
private
Date
issueDate
;
@ExcelProperty
(
"冷静期结束日期"
)
@DateTimeFormat
(
"yyyy/MM/dd"
)
private
Date
coolingOffEndDate
;
@ExcelProperty
(
"保费到期日"
)
@DateTimeFormat
(
"yyyy/MM/dd"
)
private
Date
dueDate
;
@ExcelProperty
(
"缴费年期"
)
private
String
issueNumber
;
@ExcelProperty
(
"缴费频率"
)
private
String
paymentFrequency
;
@ExcelProperty
(
"是否预缴"
)
private
Integer
isPrepay
;
@ExcelProperty
(
"预缴年期"
)
private
Integer
prepaidTerm
;
@ExcelProperty
(
"产品险种"
)
private
String
productCate
;
@ExcelProperty
(
"保险公司"
)
private
String
insuranceCompany
;
@ExcelProperty
(
"出单经纪公司"
)
private
String
reconciliationCompany
;
@ExcelProperty
(
"转介人"
)
private
String
firstBroker
;
@ExcelProperty
(
"签单员"
)
private
String
signer
;
@ExcelProperty
(
"签单员牌照号码"
)
private
String
practiceCode
;
@ExcelProperty
(
"预约编号"
)
private
String
appointmentNo
;
@ExcelProperty
(
"签单地点"
)
private
String
signLocation
;
/**
* 按 PolicyFollow 字段搬运生成一行;PI/受保人英文无数据源留空,状态保留字典码。
*
* @param p 保单记录
* @param dueDate 保费到期日(R 列),无下一期缴费日时传 null
*/
public
static
RenewalPolicyExportDTO
toRow
(
PolicyFollow
p
,
Date
dueDate
)
{
RenewalPolicyExportDTO
row
=
new
RenewalPolicyExportDTO
();
row
.
setId
(
p
.
getId
());
row
.
setPolicyNo
(
p
.
getPolicyNo
());
row
.
setProductName
(
p
.
getProductName
());
row
.
setPolicyHolder
(
p
.
getPolicyHolder
());
row
.
setPolicyHolderEn
(
p
.
getPolicyHolderEn
());
row
.
setInsured
(
p
.
getInsured
());
row
.
setStatus
(
p
.
getStatus
());
row
.
setEachIssuePremium
(
p
.
getEachIssuePremium
());
row
.
setPolicyCurrency
(
p
.
getPolicyCurrency
());
row
.
setPolicyLevy
(
p
.
getPolicyLevy
());
row
.
setTotalPremiumAndLevy
(
buildTotalPremiumAndLevy
(
p
));
row
.
setEffectiveDate
(
p
.
getEffectiveDate
());
row
.
setSignDate
(
p
.
getSignDate
());
row
.
setIssueDate
(
p
.
getIssueDate
());
row
.
setCoolingOffEndDate
(
p
.
getCoolingOffEndDate
());
row
.
setDueDate
(
dueDate
);
row
.
setIssueNumber
(
Objects
.
toString
(
p
.
getIssueNumber
(),
null
));
row
.
setPaymentFrequency
(
p
.
getPaymentFrequency
());
row
.
setIsPrepay
(
p
.
getIsPrepay
());
row
.
setPrepaidTerm
(
p
.
getPrepaidTerm
());
row
.
setProductCate
(
p
.
getProductCate
());
row
.
setInsuranceCompany
(
p
.
getInsuranceCompany
());
row
.
setReconciliationCompany
(
p
.
getReconciliationCompany
());
row
.
setFirstBroker
(
p
.
getFirstBroker
());
row
.
setSigner
(
p
.
getSigner
());
row
.
setPracticeCode
(
p
.
getPracticeCode
());
row
.
setAppointmentNo
(
p
.
getAppointmentNo
());
row
.
setSignLocation
(
p
.
getSignLocation
());
return
row
;
}
/**
* 总保费及征费 = 期交保费 + 保单征费;两者均无值时留空,征费为空或非数字按 0 处理。
* 口径与逐单邮件共用 {@link RenewalPremiumUtils}。
*/
private
static
String
buildTotalPremiumAndLevy
(
PolicyFollow
p
)
{
BigDecimal
total
=
RenewalPremiumUtils
.
totalPremiumAndLevy
(
p
);
return
total
==
null
?
null
:
total
.
toPlainString
();
}
}
yd-csf-service/src/main/resources/mappers/RenewalRemindBatchMapper.xml
0 → 100644
View file @
b1cd81e8
<?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=
"dunCount"
column=
"dun_count"
/>
<result
property=
"mailFailCount"
column=
"mail_fail_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,
dun_count,mail_fail_count,skipped_count,recipients,file_name,
status,error_msg,send_time,create_time,update_time
</sql>
</mapper>
yd-csf-service/src/main/resources/mappers/RenewalRemindPolicyMailMapper.xml
0 → 100644
View file @
b1cd81e8
<?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.RenewalRemindPolicyMailMapper"
>
<resultMap
id=
"BaseResultMap"
type=
"com.yd.csf.service.model.RenewalRemindPolicyMail"
>
<id
property=
"id"
column=
"id"
/>
<result
property=
"mailBizId"
column=
"mail_biz_id"
/>
<result
property=
"bizMonth"
column=
"biz_month"
/>
<result
property=
"policyFollowId"
column=
"policy_follow_id"
/>
<result
property=
"policyNo"
column=
"policy_no"
/>
<result
property=
"mailType"
column=
"mail_type"
/>
<result
property=
"dueDate"
column=
"due_date"
/>
<result
property=
"graceEnd"
column=
"grace_end"
/>
<result
property=
"toRecipients"
column=
"to_recipients"
/>
<result
property=
"ccRecipients"
column=
"cc_recipients"
/>
<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
,mail_biz_id,biz_month,policy_follow_id,policy_no,mail_type,
due_date,grace_end,to_recipients,cc_recipients,status,
error_msg,send_time,create_time,update_time
</sql>
</mapper>
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