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
3cab64d8
Commit
3cab64d8
authored
Sep 16, 2026
by
jianan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
保费到期提醒21-保单邮件逐条通知
parent
7fbe44f9
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
570 additions
and
52 deletions
+570
-52
yd-csf-api/src/main/java/com/yd/csf/api/service/RenewalMailService.java
+10
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalDispatchServiceImpl.java
+0
-0
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalMailServiceImpl.java
+25
-1
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/dto/RenewalDuePolicy.java
+20
-1
yd-csf-service/src/main/java/com/yd/csf/service/dto/RenewalReminderData.java
+25
-1
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/model/RenewalRemindBatch.java
+11
-1
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/IRenewalRemindPolicyMailService.java
+25
-0
yd-csf-service/src/main/java/com/yd/csf/service/service/RenewalReminderQueryService.java
+6
-5
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
+104
-23
yd-csf-service/src/main/java/com/yd/csf/service/utils/RenewalDueDateCalculator.java
+73
-0
yd-csf-service/src/main/java/com/yd/csf/service/utils/RenewalPremiumUtils.java
+39
-0
yd-csf-service/src/main/java/com/yd/csf/service/vo/RenewalPolicyExportDTO.java
+3
-18
yd-csf-service/src/main/resources/mappers/RenewalRemindBatchMapper.xml
+4
-2
yd-csf-service/src/main/resources/mappers/RenewalRemindPolicyMailMapper.xml
+31
-0
No files found.
yd-csf-api/src/main/java/com/yd/csf/api/service/RenewalMailService.java
View file @
3cab64d8
...
@@ -18,4 +18,14 @@ public interface RenewalMailService {
...
@@ -18,4 +18,14 @@ public interface RenewalMailService {
*/
*/
void
sendWithAttachment
(
List
<
String
>
recipients
,
String
subject
,
String
content
,
void
sendWithAttachment
(
List
<
String
>
recipients
,
String
subject
,
String
content
,
String
fileName
,
byte
[]
attachment
);
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
View file @
3cab64d8
This diff is collapsed.
Click to expand it.
yd-csf-api/src/main/java/com/yd/csf/api/service/impl/RenewalMailServiceImpl.java
View file @
3cab64d8
...
@@ -2,6 +2,7 @@ package com.yd.csf.api.service.impl;
...
@@ -2,6 +2,7 @@ package com.yd.csf.api.service.impl;
import
com.yd.csf.api.service.RenewalMailService
;
import
com.yd.csf.api.service.RenewalMailService
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.apache.commons.lang3.StringUtils
;
import
org.apache.commons.lang3.StringUtils
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.core.io.ByteArrayResource
;
import
org.springframework.core.io.ByteArrayResource
;
...
@@ -10,8 +11,8 @@ import org.springframework.mail.javamail.MimeMessageHelper;
...
@@ -10,8 +11,8 @@ import org.springframework.mail.javamail.MimeMessageHelper;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
javax.annotation.Resource
;
import
javax.mail.internet.InternetAddress
;
import
javax.mail.internet.MimeMessage
;
import
javax.mail.internet.MimeMessage
;
import
java.io.UnsupportedEncodingException
;
import
java.nio.charset.StandardCharsets
;
import
java.nio.charset.StandardCharsets
;
import
java.util.List
;
import
java.util.List
;
...
@@ -57,6 +58,29 @@ public class RenewalMailServiceImpl implements RenewalMailService {
...
@@ -57,6 +58,29 @@ public class RenewalMailServiceImpl implements RenewalMailService {
}
}
}
}
@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
()
{
private
String
resolveFrom
()
{
String
from
=
StringUtils
.
defaultIfBlank
(
mailFrom
,
mailUsername
);
String
from
=
StringUtils
.
defaultIfBlank
(
mailFrom
,
mailUsername
);
if
(
StringUtils
.
isBlank
(
from
))
{
if
(
StringUtils
.
isBlank
(
from
))
{
...
...
yd-csf-service/src/main/java/com/yd/csf/service/dao/RenewalRemindPolicyMailMapper.java
0 → 100644
View file @
3cab64d8
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/dto/RenewalDuePolicy.java
View file @
3cab64d8
...
@@ -7,7 +7,8 @@ import lombok.Data;
...
@@ -7,7 +7,8 @@ import lombok.Data;
import
java.time.LocalDate
;
import
java.time.LocalDate
;
/**
/**
* 命中续期提醒窗口的保单及其保费到期日
* 命中续期提醒的保单及其缴费日信息。
* R1 轮次 {@code dueDate} 为下一保费到期日;R2 轮次为当期缴费日。
*/
*/
@Data
@Data
@AllArgsConstructor
@AllArgsConstructor
...
@@ -15,5 +16,23 @@ public class RenewalDuePolicy {
...
@@ -15,5 +16,23 @@ public class RenewalDuePolicy {
private
PolicyFollow
policyFollow
;
private
PolicyFollow
policyFollow
;
/**
* 保费到期日(R1=下一缴费日,R2=当期缴费日)
*/
private
LocalDate
dueDate
;
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/dto/RenewalReminderData.java
View file @
3cab64d8
...
@@ -6,22 +6,46 @@ import lombok.Data;
...
@@ -6,22 +6,46 @@ import lombok.Data;
import
java.time.LocalDate
;
import
java.time.LocalDate
;
import
java.util.List
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.stream.Stream
;
/**
/**
* 续期提醒筛选结果:窗口信息、命中保单、异常跳过明细(保单号 -> 原因)
* 续期提醒筛选结果:窗口信息、
R1/R2
命中保单、异常跳过明细(保单号 -> 原因)
*/
*/
@Data
@Data
@AllArgsConstructor
@AllArgsConstructor
public
class
RenewalReminderData
{
public
class
RenewalReminderData
{
/**
* R1 窗口起(M+1月1号)
*/
private
LocalDate
windowStart
;
private
LocalDate
windowStart
;
/**
* R1 窗口止(M+2月月末)
*/
private
LocalDate
windowEnd
;
private
LocalDate
windowEnd
;
/**
* R1 到期通知名单(下一保费到期日落入两月窗口)
*/
private
List
<
RenewalDuePolicy
>
duePolicies
;
private
List
<
RenewalDuePolicy
>
duePolicies
;
/**
/**
* R2 宽限期跟进名单(当期已到期、宽限期在基准日至次月月末内届满)
*/
private
List
<
RenewalDuePolicy
>
dunPolicies
;
/**
* 异常跳过明细:保单号 -> 跳过原因(仅统计缺字段/无法解析等脏数据)
* 异常跳过明细:保单号 -> 跳过原因(仅统计缺字段/无法解析等脏数据)
*/
*/
private
Map
<
String
,
String
>
skipped
;
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/enums/RenewalMailTypeEnum.java
0 → 100644
View file @
3cab64d8
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/model/RenewalRemindBatch.java
View file @
3cab64d8
...
@@ -47,11 +47,21 @@ public class RenewalRemindBatch implements Serializable {
...
@@ -47,11 +47,21 @@ public class RenewalRemindBatch implements Serializable {
private
LocalDate
windowEnd
;
private
LocalDate
windowEnd
;
/**
/**
*
清单
保单条数
*
R1到期通知
保单条数
*/
*/
private
Integer
policyCount
;
private
Integer
policyCount
;
/**
/**
* R2宽限期跟进发送封数
*/
private
Integer
dunCount
;
/**
* 逐单邮件发送失败数
*/
private
Integer
mailFailCount
;
/**
* 因数据异常跳过的保单条数
* 因数据异常跳过的保单条数
*/
*/
private
Integer
skippedCount
;
private
Integer
skippedCount
;
...
...
yd-csf-service/src/main/java/com/yd/csf/service/model/RenewalRemindPolicyMail.java
0 → 100644
View file @
3cab64d8
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/IRenewalRemindPolicyMailService.java
0 → 100644
View file @
3cab64d8
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
View file @
3cab64d8
...
@@ -6,16 +6,17 @@ import java.time.LocalDate;
...
@@ -6,16 +6,17 @@ import java.time.LocalDate;
import
java.time.YearMonth
;
import
java.time.YearMonth
;
/**
/**
* 续期保单筛选:查询生效保单、计算保费到期日、过滤提醒窗口
* 续期保单筛选:查询生效保单,同一次构建产出两轮名单。
* R1 到期通知窗口 = [执行月+1 月 1 号, 执行月+2 月月末];
* R2 宽限期跟进窗口 = [基准日(执行月 1 号), 执行月次月月末]。
*/
*/
public
interface
RenewalReminderQueryService
{
public
interface
RenewalReminderQueryService
{
/**
/**
* 构建指定执行月的续期提醒数据。
* 构建指定执行月的续期提醒数据(R1 + R2)。
* 窗口 = [执行月+1 月 1 号, 执行月+2 月月末]。
*
*
* @param execMonth 执行月
* @param execMonth 执行月
* @param
today 当前日期(参数化便于测试
)
* @param
baseDate 基准日(定时跑与补发均固定传执行月 1 号,保证结果可复现
)
*/
*/
RenewalReminderData
build
(
YearMonth
execMonth
,
LocalDate
today
);
RenewalReminderData
build
(
YearMonth
execMonth
,
LocalDate
baseDate
);
}
}
yd-csf-service/src/main/java/com/yd/csf/service/service/impl/RenewalRemindPolicyMailServiceImpl.java
0 → 100644
View file @
3cab64d8
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
View file @
3cab64d8
...
@@ -11,6 +11,7 @@ import com.yd.csf.service.utils.RenewalDueDateCalculator;
...
@@ -11,6 +11,7 @@ import com.yd.csf.service.utils.RenewalDueDateCalculator;
import
com.yd.csf.service.utils.SimpleDateUtils
;
import
com.yd.csf.service.utils.SimpleDateUtils
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
javax.annotation.Resource
;
...
@@ -23,59 +24,123 @@ import java.util.List;
...
@@ -23,59 +24,123 @@ import java.util.List;
import
java.util.Map
;
import
java.util.Map
;
/**
/**
* 续期保单筛选实现
* 续期保单筛选实现
:同一次 build 产出 R1 到期通知与 R2 宽限期跟进两轮名单。
*/
*/
@Service
@Service
@Slf4j
@Slf4j
public
class
RenewalReminderQueryServiceImpl
implements
RenewalReminderQueryService
{
public
class
RenewalReminderQueryServiceImpl
implements
RenewalReminderQueryService
{
/**
* 保单 grace_period 为空/非正时的兜底宽限天数
*/
@Value
(
"${renewal.reminder.default-grace-days:31}"
)
private
Integer
configuredDefaultGraceDays
;
@Resource
@Resource
private
PolicyFollowMapper
policyFollowMapper
;
private
PolicyFollowMapper
policyFollowMapper
;
@Override
@Override
public
RenewalReminderData
build
(
YearMonth
execMonth
,
LocalDate
today
)
{
public
RenewalReminderData
build
(
YearMonth
execMonth
,
LocalDate
baseDate
)
{
LocalDate
windowStart
=
execMonth
.
plusMonths
(
1
).
atDay
(
1
);
LocalDate
windowStart
=
execMonth
.
plusMonths
(
1
).
atDay
(
1
);
LocalDate
windowEnd
=
execMonth
.
plusMonths
(
2
).
atEndOfMonth
();
LocalDate
windowEnd
=
execMonth
.
plusMonths
(
2
).
atEndOfMonth
();
// R2 宽限期窗口:[基准日(执行月1号,含), 执行月次月月末(含)]
LocalDate
dunWindowEnd
=
execMonth
.
plusMonths
(
1
).
atEndOfMonth
();
int
defaultGraceDays
=
resolveDefaultGraceDays
();
List
<
PolicyFollow
>
effectivePolicies
=
queryEffectivePolicies
();
List
<
PolicyFollow
>
effectivePolicies
=
queryEffectivePolicies
();
List
<
RenewalDuePolicy
>
duePolicies
=
new
ArrayList
<>();
List
<
RenewalDuePolicy
>
duePolicies
=
new
ArrayList
<>();
List
<
RenewalDuePolicy
>
dunPolicies
=
new
ArrayList
<>();
Map
<
String
,
String
>
skipped
=
new
LinkedHashMap
<>();
Map
<
String
,
String
>
skipped
=
new
LinkedHashMap
<>();
if
(
CollectionUtils
.
isNotEmpty
(
effectivePolicies
))
{
if
(
CollectionUtils
.
isNotEmpty
(
effectivePolicies
))
{
for
(
PolicyFollow
policy
:
effectivePolicies
)
{
for
(
PolicyFollow
policy
:
effectivePolicies
)
{
RenewalDueDateCalculator
.
Result
result
=
RenewalDueDateCalculator
.
calculate
(
LocalDate
effectiveDate
=
SimpleDateUtils
.
toLocalDate
(
policy
.
getEffectiveDate
());
SimpleDateUtils
.
toLocalDate
(
policy
.
getEffectiveDate
()),
policy
.
getPaymentFrequency
(),
policy
.
getIssueNumber
(),
today
);
if
(!
result
.
isIncluded
())
{
// R1:下一保费到期日落入 [M+1月1号, M+2月月末]
if
(
result
.
isAbnormal
())
{
RenewalDueDateCalculator
.
Result
nextResult
=
RenewalDueDateCalculator
.
calculate
(
log
.
warn
(
"续期提醒跳过异常保单,policyNo={}, 原因={}"
,
effectiveDate
,
policy
.
getPaymentFrequency
(),
policy
.
getIssueNumber
(),
baseDate
);
policy
.
getPolicyNo
(),
result
.
getSkipReason
());
if
(!
nextResult
.
isIncluded
())
{
skipped
.
put
(
policy
.
getPolicyNo
(),
result
.
getSkipReason
());
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
));
}
}
continue
;
}
}
LocalDate
dueDate
=
result
.
getDueDate
();
// R2:当期缴费日 ≤ 基准日,且宽限期到期日落在 [基准日, 次月月末]
if
(!
dueDate
.
isBefore
(
windowStart
)
&&
!
dueDate
.
isAfter
(
windowEnd
))
{
RenewalDueDateCalculator
.
Result
currentResult
=
RenewalDueDateCalculator
.
currentDueDate
(
duePolicies
.
add
(
new
RenewalDuePolicy
(
policy
,
dueDate
));
effectiveDate
,
policy
.
getPaymentFrequency
(),
policy
.
getIssueNumber
(),
baseDate
);
if
(!
currentResult
.
isIncluded
())
{
// R2 计算异常不影响 R1(入参相同,正常情况下异常原因一致)
markSkippedIfAbnormal
(
skipped
,
policy
,
currentResult
);
continue
;
}
LocalDate
currentDueDate
=
currentResult
.
getDueDate
();
// 首期未付新单(k=0,当期缴费日=生效日)不属续期宽限跟进,剔除
if
(
currentDueDate
.
equals
(
effectiveDate
))
{
continue
;
}
// 预缴保单一律不催 R2(宁可不催,不可误催);R1 不剔除
if
(
Integer
.
valueOf
(
1
).
equals
(
policy
.
getIsPrepay
()))
{
continue
;
}
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
));
}
}
}
}
}
}
// 清单按保费到期日升序,同日按保单号
// R1 按保费到期日升序、R2 按宽限期到期日升序,同日按保单号
duePolicies
.
sort
(
Comparator
Comparator
<
RenewalDuePolicy
>
policyNoOrder
=
Comparator
.
comparing
(
RenewalDuePolicy:
:
getDueDate
)
.
comparing
(
p
->
p
.
getPolicyFollow
().
getPolicyNo
(),
Comparator
.
nullsLast
(
String:
:
compareTo
));
.
thenComparing
(
p
->
p
.
getPolicyFollow
().
getPolicyNo
()));
duePolicies
.
sort
(
Comparator
.
comparing
(
RenewalDuePolicy:
:
getDueDate
).
thenComparing
(
policyNoOrder
));
dunPolicies
.
sort
(
Comparator
.
comparing
(
RenewalDuePolicy:
:
getGraceEnd
).
thenComparing
(
policyNoOrder
));
log
.
info
(
"续期提醒筛选完成,窗口=[{} ~ {}],生效保单={},命中={},异常跳过={}"
,
long
graceFallbackCount
=
duePolicies
.
stream
().
filter
(
RenewalDuePolicy:
:
isGraceFallback
).
count
()
+
dunPolicies
.
stream
().
filter
(
RenewalDuePolicy:
:
isGraceFallback
).
count
();
log
.
info
(
"续期提醒筛选完成,R1窗口=[{} ~ {}],生效保单={},R1命中={},R2命中={},宽限兜底={},异常跳过={}"
,
windowStart
,
windowEnd
,
windowStart
,
windowEnd
,
effectivePolicies
==
null
?
0
:
effectivePolicies
.
size
(),
effectivePolicies
==
null
?
0
:
effectivePolicies
.
size
(),
duePolicies
.
size
(),
skipped
.
size
());
duePolicies
.
size
(),
dunPolicies
.
size
(),
graceFallbackCount
,
skipped
.
size
());
return
new
RenewalReminderData
(
windowStart
,
windowEnd
,
duePolicies
,
skipped
);
return
new
RenewalReminderData
(
windowStart
,
windowEnd
,
duePolicies
,
dunPolicies
,
skipped
);
}
private
void
markSkippedIfAbnormal
(
Map
<
String
,
String
>
skipped
,
PolicyFollow
policy
,
RenewalDueDateCalculator
.
Result
result
)
{
if
(
result
.
isAbnormal
())
{
log
.
warn
(
"续期提醒跳过异常保单,policyNo={}, 原因={}"
,
policy
.
getPolicyNo
(),
result
.
getSkipReason
());
skipped
.
putIfAbsent
(
policy
.
getPolicyNo
(),
result
.
getSkipReason
());
}
}
/**
* 解析宽限期到期日;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
;
}
}
/**
/**
...
@@ -101,6 +166,7 @@ public class RenewalReminderQueryServiceImpl implements RenewalReminderQueryServ
...
@@ -101,6 +166,7 @@ public class RenewalReminderQueryServiceImpl implements RenewalReminderQueryServ
PolicyFollow:
:
getPaymentFrequency
,
PolicyFollow:
:
getPaymentFrequency
,
PolicyFollow:
:
getIsPrepay
,
PolicyFollow:
:
getIsPrepay
,
PolicyFollow:
:
getPrepaidTerm
,
PolicyFollow:
:
getPrepaidTerm
,
PolicyFollow:
:
getGracePeriod
,
PolicyFollow:
:
getProductCate
,
PolicyFollow:
:
getProductCate
,
PolicyFollow:
:
getInsuranceCompany
,
PolicyFollow:
:
getInsuranceCompany
,
PolicyFollow:
:
getReconciliationCompany
,
PolicyFollow:
:
getReconciliationCompany
,
...
@@ -112,4 +178,19 @@ public class RenewalReminderQueryServiceImpl implements RenewalReminderQueryServ
...
@@ -112,4 +178,19 @@ public class RenewalReminderQueryServiceImpl implements RenewalReminderQueryServ
.
eq
(
PolicyFollow:
:
getStatus
,
PolicyFollowStatusEnum
.
EFFECTIVE
.
getItemValue
())
.
eq
(
PolicyFollow:
:
getStatus
,
PolicyFollowStatusEnum
.
EFFECTIVE
.
getItemValue
())
.
eq
(
PolicyFollow:
:
getIsDeleted
,
0
));
.
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/RenewalDueDateCalculator.java
View file @
3cab64d8
...
@@ -72,6 +72,79 @@ public final class RenewalDueDateCalculator {
...
@@ -72,6 +72,79 @@ public final class RenewalDueDateCalculator {
}
}
/**
/**
* 计算单张保单的当期缴费日:缴费日序列(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.0(BigDecimal) 等存储形态。
* 解析缴费年期为整数年,兼容 "5"、"5.0"、5、5.0(BigDecimal) 等存储形态。
*/
*/
private
static
Integer
parseIssueNumber
(
Object
issueNumber
)
{
private
static
Integer
parseIssueNumber
(
Object
issueNumber
)
{
...
...
yd-csf-service/src/main/java/com/yd/csf/service/utils/RenewalPremiumUtils.java
0 → 100644
View file @
3cab64d8
package
com
.
yd
.
csf
.
service
.
utils
;
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/vo/RenewalPolicyExportDTO.java
View file @
3cab64d8
...
@@ -4,7 +4,6 @@ import com.alibaba.excel.annotation.ExcelProperty;
...
@@ -4,7 +4,6 @@ import com.alibaba.excel.annotation.ExcelProperty;
import
com.alibaba.excel.annotation.format.DateTimeFormat
;
import
com.alibaba.excel.annotation.format.DateTimeFormat
;
import
com.yd.csf.service.model.PolicyFollow
;
import
com.yd.csf.service.model.PolicyFollow
;
import
lombok.Data
;
import
lombok.Data
;
import
org.apache.commons.lang3.StringUtils
;
import
java.math.BigDecimal
;
import
java.math.BigDecimal
;
import
java.util.Date
;
import
java.util.Date
;
...
@@ -153,24 +152,10 @@ public class RenewalPolicyExportDTO {
...
@@ -153,24 +152,10 @@ public class RenewalPolicyExportDTO {
/**
/**
* 总保费及征费 = 期交保费 + 保单征费;两者均无值时留空,征费为空或非数字按 0 处理。
* 总保费及征费 = 期交保费 + 保单征费;两者均无值时留空,征费为空或非数字按 0 处理。
* 口径与逐单邮件共用 {@link com.yd.csf.service.utils.RenewalPremiumUtils}。
*/
*/
private
static
String
buildTotalPremiumAndLevy
(
PolicyFollow
p
)
{
private
static
String
buildTotalPremiumAndLevy
(
PolicyFollow
p
)
{
BigDecimal
premium
=
p
.
getEachIssuePremium
()
==
null
?
BigDecimal
.
ZERO
:
p
.
getEachIssuePremium
();
BigDecimal
total
=
com
.
yd
.
csf
.
service
.
utils
.
RenewalPremiumUtils
.
totalPremiumAndLevy
(
p
);
BigDecimal
levy
=
parseAmount
(
p
.
getPolicyLevy
());
return
total
==
null
?
null
:
total
.
toPlainString
();
if
(
p
.
getEachIssuePremium
()
==
null
&&
levy
==
null
)
{
return
null
;
}
return
premium
.
add
(
levy
==
null
?
BigDecimal
.
ZERO
:
levy
).
toPlainString
();
}
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/resources/mappers/RenewalRemindBatchMapper.xml
View file @
3cab64d8
...
@@ -11,6 +11,8 @@
...
@@ -11,6 +11,8 @@
<result
property=
"windowStart"
column=
"window_start"
/>
<result
property=
"windowStart"
column=
"window_start"
/>
<result
property=
"windowEnd"
column=
"window_end"
/>
<result
property=
"windowEnd"
column=
"window_end"
/>
<result
property=
"policyCount"
column=
"policy_count"
/>
<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=
"skippedCount"
column=
"skipped_count"
/>
<result
property=
"recipients"
column=
"recipients"
/>
<result
property=
"recipients"
column=
"recipients"
/>
<result
property=
"fileName"
column=
"file_name"
/>
<result
property=
"fileName"
column=
"file_name"
/>
...
@@ -24,7 +26,7 @@
...
@@ -24,7 +26,7 @@
<sql
id=
"Base_Column_List"
>
<sql
id=
"Base_Column_List"
>
id
id
,batch_biz_id,biz_month,window_start,window_end,policy_count,
,batch_biz_id,biz_month,window_start,window_end,policy_count,
skipped_count,recipients,file_name,status,error_msg
,
dun_count,mail_fail_count,skipped_count,recipients,file_name
,
send_time,create_time,update_time
s
tatus,error_msg,s
end_time,create_time,update_time
</sql>
</sql>
</mapper>
</mapper>
yd-csf-service/src/main/resources/mappers/RenewalRemindPolicyMailMapper.xml
0 → 100644
View file @
3cab64d8
<?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