Compare commits
No commits in common. "4eb9d2cf3bd00fcb27207666231faec081515109" and "00699a1e4a4e1b1089baf6e3c52e30ec64caf732" have entirely different histories.
4eb9d2cf3b
...
00699a1e4a
@ -1,113 +0,0 @@
|
||||
# Late Fee Start Date and Daily Recalculation Evidence (2026-06-30)
|
||||
|
||||
## Scope
|
||||
|
||||
- Backend feature: late-fee start-date calculation for four configured modes, billing-time persistence, and daily recalculation for unpaid charges.
|
||||
- Backend branch: `feat/late-fee-recalc`, merged locally into `develop`
|
||||
- Backend merge commit: `4f1a99b7e Merge branch 'feat/late-fee-recalc' into develop`
|
||||
- Docs branch: `feat/rev004-dict-alignment-docs`
|
||||
|
||||
## Backend Commits
|
||||
|
||||
- `8c2714af2 feat: add late fee begin date calculator`
|
||||
- `1724f0c3c feat: resolve late fee parameter config`
|
||||
- `282c327e7 feat: add late fee daily calculator`
|
||||
- `320e50d73 feat: add late fee recalculation mapper helpers`
|
||||
- `fd5154f83 feat: save late fee begin date during billing`
|
||||
- `f7207fe61 feat: add daily late fee recalculation job`
|
||||
- `af05601f1 fix: harden late fee recalculation batch`
|
||||
- `3722c54fa fix: paginate late fee recalculation batches`
|
||||
- `d551dba84 fix: handle late fee reversal and batch transactions`
|
||||
- `bcc635e81 fix: guard late fee recalculation updates`
|
||||
|
||||
## Implemented Files
|
||||
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeSettingCodes.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeConfig.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeConfigResolver.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeBeginDateCalculator.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeBeginDateApplyService.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeDailyCalculator.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeRecalculationBatchService.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/latefee/LateFeeRecalculationService.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/job/charge/LateFeeRecalculationJob.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/config/ClockConfig.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/charge/ChargeMapper.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/paymentrecorddetail/PaymentRecordDetailMapper.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/latefeereducedetail/LateFeeReduceDetailMapper.java`
|
||||
- `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java`
|
||||
- `script/xxl-job-init.sql`
|
||||
- `sql/rev004/REV004_late_fee_daily_recalculation_index.sql`
|
||||
- Unit tests under `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/latefee/`
|
||||
- `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeLateFeeBeginDateWiringTest.java`
|
||||
- `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffWriteBackServiceTest.java`
|
||||
|
||||
## Review Fixes
|
||||
|
||||
- Paid late-fee deduction excludes original `APPLIED` late-fee details that have a `REVERSED` detail referencing them through `relatedDetailId`.
|
||||
- The daily recalculation batch now selects unpaid charges with non-null `lateFeeBeginDate`, avoiding repeated starvation behind historical rows that cannot be recalculated.
|
||||
- `script/xxl-job-init.sql` registers handler `lateFeeDailyRecalculation` for business-server with cron `0 30 3 * * ?`.
|
||||
- `LateFeeRecalculationServiceTest` includes a defensive scenario where a reversed late-fee detail is returned by the mapper and ignored by aggregation.
|
||||
- The daily recalculation job now uses keyset pagination (`id > lastId`) until no eligible rows remain, so rows after the first page are recalculated in the same run instead of waiting forever behind unchanged earlier rows.
|
||||
- `LateFeeBeginDateApplyService` centralizes `lateFeeBeginDate` persistence and is reused by normal billing, manual charge creation, counter add-reading formalization, and special billing formalization before `chargeMapper.insert`.
|
||||
- Focused tests now cover the shared begin-date apply service, keyset pagination across batches, and direct insert path wiring for counter add-reading and special billing.
|
||||
- Price-diff write-back now applies missing `lateFeeBeginDate` before inserting the replacement unpaid charge.
|
||||
- Daily recalculation now keeps the outer pagination loop non-transactional and executes each page through `LateFeeRecalculationBatchService.recalculateBatch(...)` with its own transaction boundary.
|
||||
- Daily recalculation now updates `lateFee` through a conditional mapper update that still requires `payState=UNPAID` and non-null `lateFeeBeginDate`, so a charge paid after selection is not overwritten or counted.
|
||||
- `LateFeeDailyCalculator` ignores non-positive penalty coefficients defensively.
|
||||
- `REV004_late_fee_daily_recalculation_index.sql` adds `idx_biz_charge_late_fee_recalc` on `(tenant_id, pay_state, id)` for the tenant-scoped unpaid keyset scan.
|
||||
|
||||
## Verification
|
||||
|
||||
### Focused Late-Fee Tests
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am -Dtest=LateFeeBeginDateCalculatorTest,LateFeeBeginDateApplyServiceTest,LateFeeConfigResolverTest,LateFeeDailyCalculatorTest,LateFeeRecalculationServiceTest,ChargeLateFeeBeginDateWiringTest,CounterAddReadingApplicationServiceImplTest,SpecialBillingServiceImplTest,PriceDiffWriteBackServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Result: PASS
|
||||
|
||||
- `LateFeeBeginDateApplyServiceTest`: 2 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `LateFeeRecalculationServiceTest`: 8 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `LateFeeConfigResolverTest`: 3 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `LateFeeBeginDateCalculatorTest`: 7 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `LateFeeDailyCalculatorTest`: 9 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `ChargeLateFeeBeginDateWiringTest`: 5 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `PriceDiffWriteBackServiceTest`: 1 test, 0 failures, 0 errors, 0 skipped
|
||||
- `CounterAddReadingApplicationServiceImplTest`: 24 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `SpecialBillingServiceImplTest`: 4 tests, 0 failures, 0 errors, 0 skipped
|
||||
- Total: 63 tests, 0 failures, 0 errors, 0 skipped
|
||||
|
||||
### Existing Accounting Regression Tests
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am -Dtest=LateFeeDateModeCalculatorTest,PaymentRecordServiceImplTest,AccountingAdjustActionServiceImplTest,ChargeServiceBillingValidationTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Result: PASS
|
||||
|
||||
- `ChargeServiceBillingValidationTest`: 8 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `LateFeeDateModeCalculatorTest`: 4 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `AccountingAdjustActionServiceImplTest`: 19 tests, 0 failures, 0 errors, 0 skipped
|
||||
- `PaymentRecordServiceImplTest`: 18 tests, 0 failures, 0 errors, 0 skipped
|
||||
- Total: 49 tests, 0 failures, 0 errors, 0 skipped
|
||||
|
||||
### Compile
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
mvn -pl sw-business/sw-business-server -am -DskipTests compile
|
||||
```
|
||||
|
||||
Result: PASS
|
||||
|
||||
## Known Warnings
|
||||
|
||||
- Maven reports existing effective-model warnings for duplicate or missing plugin declarations in unrelated modules.
|
||||
- Mockito reports the existing dynamic-agent warning during unit tests.
|
||||
- These warnings did not fail the executed verification commands.
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,278 +0,0 @@
|
||||
# 违约金起算与每日重算设计
|
||||
|
||||
日期:2026-06-30
|
||||
|
||||
## 背景
|
||||
|
||||
本设计用于明确违约金四种起算方式、逾期天数、金额计算、每日任务重算和历史账单处理口径。
|
||||
|
||||
本次范围同时覆盖:
|
||||
|
||||
- 开账时违约金起算日计算与固化
|
||||
- 每日任务按未结清账单重算当前应收违约金
|
||||
- 当前未缴本金、已收违约金、已生效减免金额的扣减口径
|
||||
- 小数、封顶、配置非法值处理
|
||||
|
||||
本设计不改变违约金减免对象本身的提交/审批流程。注意区分:
|
||||
|
||||
- `CALC_LATE_FEE_TYPE`:系统参数,表示违约金起算方式,取值 `1/2/3/4`
|
||||
- `lateFeeType`:账务调整/违约金减免字段,表示减免方式,例如按金额、按日期
|
||||
|
||||
## 相关配置
|
||||
|
||||
核心起算配置:
|
||||
|
||||
- `CALC_LATE_FEE_TYPE`:违约金起算方式
|
||||
- `1`:账单账务年月当月某日
|
||||
- `2`:开账后若干天
|
||||
- `3`:账单账务年月下月某日
|
||||
- `4`:账单账务年月下下月某日
|
||||
- `CALC_LATE_FEE_SETTING`:起算参数值
|
||||
- `type=1/3/4` 时表示日号
|
||||
- `type=2` 时表示宽限天数
|
||||
|
||||
金额相关配置:
|
||||
|
||||
- `LATE_FEE_DECIMAL`:违约金保留小数位数
|
||||
- `LATE_FEE_MORE_PRINCIPAL`:违约金封顶倍数
|
||||
- `CostComponentDO.penaltyCoefficient`:费用项违约金系数
|
||||
|
||||
账单字段:
|
||||
|
||||
- `ChargeDO.billMonth`:账单账务年月,格式 `YYYYMM`
|
||||
- `ChargeDO.billDate`:开账时间
|
||||
- `ChargeDO.lateFeeBeginDate`:实际开始计算违约金的日期
|
||||
- `ChargeDO.lateFee`:当前应收违约金
|
||||
|
||||
## 开账起算日规则
|
||||
|
||||
开账时根据当时系统参数计算并固化 `ChargeDO.lateFeeBeginDate`。系统参数先计算出“宽限截止日”,宽限截止日当天不计违约金,次日才是第 1 个违约金计费日。
|
||||
|
||||
### 月份基准
|
||||
|
||||
`CALC_LATE_FEE_TYPE=1/3/4` 的月份最终以账单 `billMonth` 字段对应自然月为准。
|
||||
|
||||
- `type=1`:`billMonth` 当月某日为宽限截止日
|
||||
- `type=3`:`billMonth` 下月某日为宽限截止日
|
||||
- `type=4`:`billMonth` 下下月某日为宽限截止日
|
||||
|
||||
`BILLINGMONTH_STARTDAY` 负责账务年月生成和解释。本次违约金起算计算不在每日任务中重新按运行日期推导账务周期,而是使用账单已固化的 `billMonth`。
|
||||
|
||||
### 四种起算方式
|
||||
|
||||
`type=1/3/4`:
|
||||
|
||||
1. 读取 `CALC_LATE_FEE_SETTING` 作为目标日号。
|
||||
2. 根据 `billMonth` 和 `type` 得到目标月份。
|
||||
3. 若目标月份没有该日号,例如 2 月没有 30/31 号,则目标日取该月最后一天。
|
||||
4. 目标日为宽限截止日。
|
||||
|
||||
`type=2`:
|
||||
|
||||
1. 读取 `CALC_LATE_FEE_SETTING` 作为宽限天数 `N`。
|
||||
2. 开账日不算第 1 天。
|
||||
3. 宽限截止日为 `billDate.toLocalDate() + N 天`。
|
||||
4. `N=0` 表示开账日就是宽限截止日,开账次日开始计违约金。
|
||||
|
||||
### 实际起算日
|
||||
|
||||
实际违约金起算日为:
|
||||
|
||||
```text
|
||||
lateFeeBeginDate = max(宽限截止日 + 1 天, billDate.toLocalDate() + 1 天)
|
||||
```
|
||||
|
||||
因此,如果配置算出的宽限截止日早于开账日,也不能在开账日前产生违约金。
|
||||
|
||||
### 配置非法处理
|
||||
|
||||
`CALC_LATE_FEE_SETTING` 的处理规则:
|
||||
|
||||
- `type=1/3/4`:有效日号范围为 `1-31`
|
||||
- `type=2`:有效天数范围为 `>=0`
|
||||
- `settingValue` 非法时回退 `settingDefault`
|
||||
- `settingDefault` 仍非法时,不生成 `lateFeeBeginDate`,并记录配置异常
|
||||
- `type=1/3/4` 中有效日号超过目标月份最大天数时,不视为非法,取目标月最后一天
|
||||
|
||||
`billDate` 为空时:
|
||||
|
||||
- 不计算 `lateFeeBeginDate`
|
||||
- 记录数据异常
|
||||
- 不使用当前日期或 `createTime` 兜底
|
||||
|
||||
## 每日重算规则
|
||||
|
||||
每日任务只处理未结清账单:
|
||||
|
||||
- 未缴账单:参与重算
|
||||
- 部分缴费账单:按当前未缴本金参与重算
|
||||
- 已结清账单:不再重算,避免覆盖历史违约金
|
||||
|
||||
每日任务必须幂等。每次根据当前账单状态重新计算并覆盖当前应收违约金,重复执行、失败重试、补跑都不得重复累加。
|
||||
|
||||
每日任务默认使用开账时固化的 `lateFeeBeginDate`。后续系统参数变化只影响新开账账单,不自动改变历史未结清账单的起算日。
|
||||
|
||||
历史未结清账单如果 `lateFeeBeginDate` 为空:
|
||||
|
||||
- 不使用当前系统参数临时推导
|
||||
- 不计算违约金
|
||||
- 记录异常或告警,等待数据修正
|
||||
|
||||
### 逾期天数
|
||||
|
||||
- 当前日期早于 `lateFeeBeginDate`:逾期天数为 `0`
|
||||
- 当前日期达到或晚于 `lateFeeBeginDate`:逾期天数包含当前日期
|
||||
|
||||
公式:
|
||||
|
||||
```text
|
||||
overdueDays = 当前日期 - lateFeeBeginDate.toLocalDate() + 1
|
||||
```
|
||||
|
||||
## 金额计算规则
|
||||
|
||||
本次不判断通知状态。违约金产生的计算前提为:
|
||||
|
||||
- 账单未结清
|
||||
- 存在当前未缴本金
|
||||
- 账单存在有效 `lateFeeBeginDate`
|
||||
- 当前日期达到或晚于 `lateFeeBeginDate`
|
||||
- 费用项 `penaltyCoefficient > 0`
|
||||
|
||||
### 本金基数
|
||||
|
||||
违约金本金基数使用当前未缴本金:
|
||||
|
||||
- 已缴本金不再继续产生违约金
|
||||
- 部分缴费后,剩余未缴本金继续按完整逾期天数计算
|
||||
- 优先使用费用项级剩余未缴本金
|
||||
- 如果系统没有费用项级剩余未缴本金,则按各费用项原始本金比例分摊账单剩余未缴本金
|
||||
- 分摊后的费用项未缴本金之和必须等于账单剩余未缴本金
|
||||
- 分摊尾差落到最后一个参与项或金额最大的参与项
|
||||
|
||||
### 逐费用项计算
|
||||
|
||||
继续采用逐费用项计算后汇总的口径:
|
||||
|
||||
```text
|
||||
单项理论违约金 = 单项未缴本金 × 单项违约金系数 × 逾期天数
|
||||
```
|
||||
|
||||
费用项违约金系数来源:
|
||||
|
||||
```text
|
||||
ChargeDetailDO.costComponentCode -> CostComponentDO.code -> CostComponentDO.penaltyCoefficient
|
||||
```
|
||||
|
||||
每个费用项先按 `LATE_FEE_DECIMAL` 四舍五入,再汇总到账单理论违约金。封顶在账单理论违约金汇总后执行,封顶结果再按 `LATE_FEE_DECIMAL` 规范化一次。
|
||||
|
||||
### 小数位
|
||||
|
||||
`LATE_FEE_DECIMAL` 控制违约金保留小数位数:
|
||||
|
||||
- 优先使用 `settingValue`
|
||||
- `settingValue` 为空、非数字、负数时回退 `settingDefault`
|
||||
- `settingDefault` 仍非法时按 `0`
|
||||
- 取整方式固定四舍五入
|
||||
- `COUNTER_DECIMAL_WAY` 不参与本次违约金计算
|
||||
|
||||
### 封顶
|
||||
|
||||
`LATE_FEE_MORE_PRINCIPAL` 控制违约金封顶:
|
||||
|
||||
- 空值、非数字、负数按 `1`
|
||||
- `=0` 表示不设上限
|
||||
- `>0` 表示理论违约金不得超过当前未缴本金乘以配置倍数
|
||||
|
||||
处理顺序:
|
||||
|
||||
1. 逐费用项计算原始违约金
|
||||
2. 每个费用项先按 `LATE_FEE_DECIMAL` 四舍五入
|
||||
3. 汇总得到账单理论违约金
|
||||
4. 按 `LATE_FEE_MORE_PRINCIPAL` 对账单理论违约金封顶
|
||||
5. 封顶结果再按 `LATE_FEE_DECIMAL` 四舍五入后作为可扣减前理论违约金
|
||||
|
||||
## 已收与减免扣减
|
||||
|
||||
每日任务覆盖账单当前应收违约金时,必须扣减已收违约金和已审批生效的违约金减免金额。
|
||||
|
||||
公式:
|
||||
|
||||
```text
|
||||
当前应收违约金 = max(0, 理论违约金 - 已收违约金 - 已生效违约金减免金额)
|
||||
```
|
||||
|
||||
已收违约金:
|
||||
|
||||
- 从成功且未被撤销/红冲的收费或支付明细汇总
|
||||
- 只汇总实际违约金金额,例如 `late_fee_amount`
|
||||
- 不把本金支付误算为违约金支付
|
||||
|
||||
已生效违约金减免金额:
|
||||
|
||||
- 只统计审批通过且已生效的违约金减免记录
|
||||
- 待审批、驳回、撤销的减免记录不参与扣减
|
||||
|
||||
## 主要影响范围
|
||||
|
||||
后端实现预计涉及:
|
||||
|
||||
- 开账服务:生成账单时计算并保存 `lateFeeBeginDate`
|
||||
- 系统参数读取:读取 `CALC_LATE_FEE_TYPE`、`CALC_LATE_FEE_SETTING`、`LATE_FEE_DECIMAL`、`LATE_FEE_MORE_PRINCIPAL`
|
||||
- 违约金计算器:统一封装起算日和金额计算
|
||||
- 每日任务:扫描未结清账单,幂等重算并覆盖当前应收违约金
|
||||
- 账单明细读取:获取费用项本金、费用项编码和违约金系数
|
||||
- 支付明细汇总:汇总已收违约金
|
||||
- 违约金减免汇总:汇总已审批生效减免金额
|
||||
- 异常记录:配置异常、`billDate` 缺失、`lateFeeBeginDate` 缺失等
|
||||
|
||||
## 测试与验收点
|
||||
|
||||
开账起算日:
|
||||
|
||||
- `type=1` 当月某日起算
|
||||
- `type=2` 开账后 N 天,开账日不算第 1 天
|
||||
- `type=2 setting=0`
|
||||
- `type=3` 下月某日起算
|
||||
- `type=4` 下下月某日起算
|
||||
- 配置日为 29/30/31 时遇到 2 月、4 月等短月
|
||||
- 开账日晚于宽限截止日时,起算日取 `billDate + 1`
|
||||
- `billDate` 为空时不生成起算日并记录异常
|
||||
- `CALC_LATE_FEE_SETTING` 非法时回退默认值
|
||||
- `CALC_LATE_FEE_SETTING` 默认值仍非法时不生成起算日并记录异常
|
||||
|
||||
每日重算:
|
||||
|
||||
- 当前日期早于起算日,逾期天数为 0
|
||||
- 当前日期等于起算日,逾期天数为 1
|
||||
- 当前日期晚于起算日,逾期天数包含当前日期
|
||||
- 未缴账单参与重算
|
||||
- 部分缴费账单按当前未缴本金重算
|
||||
- 已结清账单不重算
|
||||
- `lateFeeBeginDate` 为空的历史未结清账单不计算并记录异常
|
||||
- 重复执行任务结果不变
|
||||
|
||||
金额计算:
|
||||
|
||||
- 逐费用项按不同 `penaltyCoefficient` 计算并汇总
|
||||
- 没有费用项级剩余本金时按比例分摊账单剩余未缴本金
|
||||
- 分摊尾差不造成汇总不平
|
||||
- `LATE_FEE_DECIMAL` 控制逐项舍入和最终结果
|
||||
- `LATE_FEE_DECIMAL` 非法时回退默认值,默认值非法时按 0
|
||||
- `LATE_FEE_MORE_PRINCIPAL` 空值按 1
|
||||
- `LATE_FEE_MORE_PRINCIPAL=0` 不封顶
|
||||
- `LATE_FEE_MORE_PRINCIPAL>0` 按当前未缴本金倍数封顶
|
||||
- 逐项舍入、汇总、封顶、最终舍入的顺序固定
|
||||
- 已收违约金扣减
|
||||
- 已审批生效违约金减免扣减
|
||||
- 扣减后结果不小于 0
|
||||
|
||||
## 非目标
|
||||
|
||||
本次不覆盖:
|
||||
|
||||
- 违约金减免流程重构
|
||||
- 通知状态判断
|
||||
- 已结清账单历史金额重算
|
||||
- 参数变更后自动追溯修改历史未结清账单起算日
|
||||
- 查询实时计算替代落库金额
|
||||
Loading…
x
Reference in New Issue
Block a user