Commit 4f5e6437 authored by fangxinjiang's avatar fangxinjiang

提前缴费商险兼容:OpenSpec 规范与 V24 方案,HRO is_new_data 与 EKP fd_new_data 对齐。

包含 V24 开发方案/需求文档、openspec 变更与基线 spec,以及 yifu-insurances 分流实现与 DDL。
Co-authored-by: 's avatarCursor <cursoragent@cursor.com>
parent db54e696
......@@ -141,6 +141,7 @@
| 字段 | 含义 | 见费出单典型值 |
| --- | --- | --- |
| fd_jfcd | 是否见费出单 | 是 |
| fd_new_data | 是否新数据(1是/0否,默认1) | 历史见费出单:`0`;新提前缴费实缴:`1` |
| fd_3b13b2ecc164aa | 是否全部结算 | 是 |
| fd_3af9d11088facc | 实际结算月份 | 空 |
| fd_3af9d1441ef7b6 | 有无预估 | 有 |
......@@ -154,3 +155,4 @@
| 日期 | 说明 |
| --- | --- |
| 2026-06-05 | 初稿:建立 EKP 五模块章节结构,标注待梳理 |
| 2026-06-18 | 附录增 `fd_new_data`;HRO `is_new_data` 与 EKP 同值对齐 |
......@@ -125,7 +125,7 @@
| 术语 | 说明 |
| --- | --- |
| 提前缴费 | 商险业务中在常规流程前预先缴费的数据类型 |
| 历史数据 / 新数据 | 通过新增字段标识;历史数据走既有逻辑,新数据走本需求新逻辑 |
| 历史数据 / 新数据 | HRO `is_new_data` 与 EKP `fd_new_data` **同义同值**(1=新,0=历史,默认1) |
| 预估数据 | 投保前/投保过程中产生的预估收付数据 |
| 实缴数据 | 实际缴纳/import 后的真实收付数据 |
| 差额数据 | 预估与实缴之间的差异数据 |
......@@ -135,3 +135,11 @@
| 收款结算 | EKP 侧对客户/项目的收款结算流程 |
| BPO 项目 | 业务流程外包类项目,应收为 0,走成本归集 |
| 非 BPO 项目 | 常规项目,应收应付均为实缴金额 |
### 字段映射(HRO ↔ EKP)
| HRO `is_new_data` | 含义 | EKP `fd_new_data` | 含义 |
| --- | --- | --- | --- |
| `1` | 新提前缴费 | `1` | 是否新数据=是 |
| `0` | 历史提前缴费 | `0` | 是否新数据=否 |
| `NULL` | 非提前缴费 | — | `fd_jfcd=否` 时不写入 |
......@@ -13,7 +13,7 @@
| 维度 | 历史数据(既有见费出单) | 新数据(本需求) |
| --- | --- | --- |
| 标识 | `isJfcd=0` + `isHistoryAdvancePay=1`(历史) | `isJfcd=0` + `isHistoryAdvancePay=0`(新) |
| 标识 | `isJfcd=0` + `isNewData=0`(历史) | `isJfcd=0` + `isNewData=1`(新) |
| 派单后状态 | 待缴费(6) → EKP 收款 → 待投保(1) | **直接待投保(1)**,跳过待缴费收款环节 |
| 预估 | 派单时生成并推送 EKP | **不生成** |
| 实缴 | 登记保单保费后推送 | 登记保单保费后推送(应收/应付规则见下) |
......@@ -29,87 +29,84 @@
/ \
否 是
| |
普通派单流程 isHistoryAdvancePay ?
普通派单流程 isNewData ?
/ \
是(1)/null 否(0)
否(0)/null 是(1)
| |
【既有逻辑,代码块不动】 【新分支方法,加注释】
```
1. **新增字段**「是否历史提前缴费」区分历史/新,HRO 派单明细与 EKP 推送**同名同义**(见 §2.1)。
1. **新增字段**「是否新数据」区分历史/新,HRO 与 EKP **同义同值**(见 §2.1)。
2. **分流入口**统一用 `AdvancePaymentHelper.isLegacyJfcdData(detail)`,历史分支**整体迁入私有方法或保持原 if 块不动**,新逻辑进 `*NewAdvancePay*` 方法。
3. **禁止**在历史分支内做「顺手重构」;新分支单独注释块:`// ===== 新提前缴费逻辑(isHistoryAdvancePay=0)=====`.
3. **禁止**在历史分支内做「顺手重构」;新分支单独注释块:`// ===== 新提前缴费逻辑(isNewData=1)=====`.
4. 历史路径的单元/集成测试用例**无需重跑**;测试聚焦新标识 + 边界(BPO/非 BPO、退保、EKP 回调)。
---
## 二、数据模型设计
### 2.1 统一字段:是否历史提前缴费
### 2.1 统一字段:是否新数据
> **命名原则**:HRO 商险派单明细、推送 EKP 的**预估明细(派单)**与**实缴明细(登记保单保费)**使用**同一业务含义、同一套取值规则**;各层字段名语义一致,仅按各端命名规范转换大小写/前缀
> **命名原则**:HRO `is_new_data` 与 EKP `fd_new_data` **同义同值**(1=新,0=历史,默认1);Java 属性 `isNewData`
| 层级 | 字段标识 | 类型 | 取值 | 含义 |
| --- | --- | --- | --- | --- |
| **HRO 表** `t_insurance_detail` | `is_history_advance_pay` | `char(1)` | `1` | **是**,历史提前缴费(走既有见费出单逻辑) |
| | | | `0` | **否**新提前缴费(走本需求新逻辑) |
| | | | `NULL` | 非提前缴费(`isJfcd=1`或不适用;若 `isJfcd=0`**视同 `1`(历史)** |
| **Java** Entity / VO / Param | `isHistoryAdvancePay` | String | 同上 | 与库表一一对应 |
| **EKP 表** `ekp_insurances_info` | `fd_is_history_advance_pay` | 文本 | `是` | 历史提前缴费 |
| | | | `否` | 新提前缴费 |
| **推送参数** `EkpInsurancesInfo` / `EkpInteractiveParam` | `isHistoryAdvancePay` → 写入 `fd_is_history_advance_pay` | — | HRO `1`→EKP `是`,HRO `0`→EKP `否` | 转换在 `DoJointInsuranceTask.initValues` 统一封装 |
| **HRO 表** `t_insurance_detail` | `is_new_data` | `char(1)` | `1` | **是**,新提前缴费(走本需求新逻辑) |
| | | | `0` | **否**历史见费出单(走既有逻辑) |
| | | | `NULL` | 非提前缴费(`isJfcd=1`;若 `isJfcd=0` 空值**视同 `0`(历史)** |
| **Java** Entity / VO / Param | `isNewData` | String | 同上 | 与库表一一对应 |
| **EKP 表** `ekp_insurances_info` | `fd_new_data` | `char(1)` | `1` | **是**,新提前缴费(默认初始化值) |
| | | | `0` | **否**,历史见费出单 |
| **推送** | `isNewData``fd_new_data` | — | **同值直传** | `DoJointInsuranceTask.initValues` |
> EKP 表单侧 `fd_is_history_advance_pay` 的表单字段 ID 需与 EKP 联调确认;**业务名称统一为「是否历史提前缴费」**,与 HRO 字段含义一致
> EKP 表单侧 `fd_new_data` 字段 ID 联调确认;业务名称「是否新数据」(1是/0否)
**写入范围(`fd_jfcd=是` 时必写)**
| 场景 | HRO 写入 | 推送 EKP |
| --- | --- | --- |
| 历史见费出单 — 派单推**预估** | `is_history_advance_pay=1` | `fd_is_history_advance_pay=是` |
| 新提前缴费 — 派单 | `is_history_advance_pay=0` | 无预估推送;明细落库仍为 `0` |
| 历史见费出单 — 登记**实缴** | 读明细 `1` | `fd_is_history_advance_pay=是` |
| 新提前缴费 — 登记**实缴** | 读明细 `0` | `fd_is_history_advance_pay=否` |
| 新提前缴费 — 退保负数**实缴** | 读明细 `0` | `fd_is_history_advance_pay=否` |
| 历史 — 差额 / 红冲 | 读明细 `1` | `fd_is_history_advance_pay=是` |
| 历史见费出单 — 派单推**预估** | `is_new_data=0` | `fd_new_data=0` |
| 新提前缴费 — 派单 | `is_new_data=1` | 无预估推送 |
| 历史见费出单 — 登记**实缴** | 读明细 `0` | `fd_new_data=0` |
| 新提前缴费 — 登记**实缴** | 读明细 `1` | `fd_new_data=1` |
| 新提前缴费 — 退保负数**实缴** | 读明细 `1` | `fd_new_data=1` |
| 历史 — 差额 / 红冲 | 读明细 `0` | `fd_new_data=0` |
**同步改造**
- HRO:`TInsuranceDetail`、VO/DTO、Mapper XML、列表/详情出参
- EKP 推送:`DoJointInsuranceTask.initValues``insertInsrancesDetail``EkpInsurancesInfo``EkpInteractiveParam`
- 常量:`InsurancesConstants.IS_HISTORY_ADVANCE_PAY_YES/NO`;EKP 文案 `EKP_IS_HISTORY_ADVANCE_PAY_YES=是` / `NO=否`
- HRO:`TInsuranceDetail.isNewData`、VO/DTO、Mapper XML
- EKP 推送:`DoJointInsuranceTask.initValues``EkpInsurancesInfo.fd_new_data`
- 常量:`InsurancesConstants.IS_NEW_DATA_YES/NO`(与 `EKP_FD_NEW_DATA_*` 同值)
**转换工具方法(建议)**
**工具方法**
```java
/** HRO → EKP:1→是,0→否 */
String toEkpIsHistoryAdvancePay(String isHistoryAdvancePay);
/** EKP → HRO:是→1,否→0(若 EKP 回调需读) */
String fromEkpIsHistoryAdvancePay(String fdValue);
String resolveIsNewDataForNewRecord(); // launch 后 → 1,否则 → 0
String toEkpFdNewData(String isNewData); // 同值直传
```
### 2.2 初始化规则
| 场景 | `isJfcd` | `is_history_advance_pay` |
| 场景 | `isJfcd` | `is_new_data` |
| --- | --- | --- |
| 上线前存量 `isJfcd=0` | 0 | **`1`(是,历史)**(数据脚本) |
| **发布时间之后**新增/批增满足见费出单条件 | 0 | **`0`(否,新)** |
| 上线前存量 `isJfcd=0` | 0 | **`0`(否,历史)**(数据脚本) |
| **发布时间之后**新增/批增满足见费出单条件 | 0 | **`1`(是,新)** |
| 非见费出单 | 1 | NULL |
| 替换 | — | 不参与(现有逻辑不变) |
**见费出单触发条件(不变)**:险种「是否见费出单」= 是,且项目 **`bpoEnable=否`(未启用 BPO 结算模式)**
**已确认**:启用 BPO 结算模式的项目**仍不走**见费出单/提前缴费,与现有 `initJfcd` / `initJfcdBatch` 逻辑一致,无需调整。
**改造点**`initJfcd` / `initJfcdBatch` 在满足见费出单条件时:`isJfcd=0`,并按发布时间写 `isHistoryAdvancePay``1``0`)。
**改造点**`initJfcd` / `initJfcdBatch` 在满足见费出单条件时:`isJfcd=0`,并按发布时间写 `isNewData``0``1`)。
### 2.3 上线切点(已确认)
**以发布时间控制** `is_history_advance_pay=0`(新数据)的写入,不使用配置开关。
**以发布时间控制** `is_new_data=1`(新数据)的写入,不使用配置开关。
| 实现方式 | 说明 |
| --- | --- |
| 配置项 | `advance-pay.new-data-launch-time`(如 `2026-07-01 00:00:00`) |
| 写入规则 | 满足见费出单 **且** `当前时间 >= launchTime``isHistoryAdvancePay=0`;否则 → `isHistoryAdvancePay=1` |
| 写入规则 | 满足见费出单 **且** `当前时间 >= launchTime``isNewData=1`;否则 → `isNewData=0` |
| 存量数据 | 上线前执行 §2.4 DDL + §2.5 迁移脚本 |
### 2.4 DDL 脚本(上线前执行)
......@@ -121,52 +118,52 @@ String fromEkpIsHistoryAdvancePay(String fdValue);
```sql
-- 库:mvp_insurances
ALTER TABLE t_insurance_detail
ADD COLUMN is_history_advance_pay CHAR(1) NULL DEFAULT NULL
COMMENT '是否历史提前缴费:1=是(历史见费出单) 0=否(新提前缴费),仅 is_jfcd=0 时有效'
ADD COLUMN is_new_data CHAR(1) NULL DEFAULT '1'
COMMENT '是否新数据:1是(新提前缴费) 0否(历史见费出单),仅 is_jfcd=0 时有效'
AFTER is_jfcd;
```
| 列名 | 类型 | 说明 |
| --- | --- | --- |
| `is_history_advance_pay` | `CHAR(1)` | `1` 历史 / `0` / `NULL` 非提前缴费 |
| `is_new_data` | `CHAR(1)` | `1` 新 / `0` 历史 / `NULL` 非提前缴费 |
#### 2.4.2 EKP — `ekp_insurances_info` 新增字段
```sql
-- 库:ekp
ALTER TABLE ekp_insurances_info
ADD COLUMN fd_is_history_advance_pay VARCHAR(10) NULL DEFAULT NULL
COMMENT '是否历史提前缴费:是=历史 否=新,与 HRO is_history_advance_pay 对应,fd_jfcd=是 时写入'
ADD COLUMN fd_new_data CHAR(1) NULL DEFAULT '1'
COMMENT '是否新数据:1是 0否,默认1;与 HRO is_new_data 同值,fd_jfcd=是 时写入'
AFTER fd_jfcd;
```
| 列名 | 类型 | 说明 |
| --- | --- | --- |
| `fd_is_history_advance_pay` | `VARCHAR(10)` | `是` 历史 / `否` 新;与 HRO `1`/`0` 通过推送逻辑转换 |
| `fd_new_data` | `CHAR(1)` | `1` 新 / `0` 历史;默认 `1`;与 HRO `is_new_data` **同值直传** |
> **说明**:若 EKP 侧除 `ekp_insurances_info` 外还有商险明细视图/同步表,需 EKP 侧一并加列;HRO 推送写入以 `ekp_insurances_info.fd_is_history_advance_pay` 为准。表单字段 ID 联调后与物理列名保持一致。
> **说明**:若 EKP 侧除 `ekp_insurances_info` 外还有商险明细视图/同步表,需 EKP 侧一并加列;HRO 推送写入以 `ekp_insurances_info.fd_new_data` 为准。表单字段 ID 联调后与物理列名保持一致。
### 2.5 存量数据迁移脚本(DDL 之后执行)
#### 2.5.1 HRO — 历史见费出单刷为「是」
#### 2.5.1 HRO — 存量见费出单刷为「否」(非新数据=历史)
```sql
-- 库:mvp_insurances
UPDATE t_insurance_detail
SET is_history_advance_pay = '1'
SET is_new_data = '0'
WHERE is_jfcd = '0'
AND (is_history_advance_pay IS NULL OR is_history_advance_pay = '');
AND (is_new_data IS NULL OR is_new_data = '' OR is_new_data = '1');
```
#### 2.5.2 EKP — 既有提前缴费明细刷为「是」
#### 2.5.2 EKP — 既有提前缴费明细刷为「否」(非新数据=历史)
```sql
-- 库:ekp
-- 上线前已存在的见费出单明细(预估/实缴/差额)均视为历史数据
-- 上线前已存在的见费出单明细(预估/实缴/差额)均视为历史数据(是否新数据=否)
UPDATE ekp_insurances_info
SET fd_is_history_advance_pay = '是'
SET fd_new_data = '0'
WHERE fd_jfcd = '是'
AND (fd_is_history_advance_pay IS NULL OR fd_is_history_advance_pay = '');
AND (fd_new_data IS NULL OR fd_new_data = '' OR fd_new_data = '1');
```
**执行顺序**`§2.4.1``§2.4.2``§2.5.1``§2.5.2` → 应用发版(含 `advance-pay.new-data-launch-time` 配置)。
......@@ -183,11 +180,11 @@ boolean isJfcd(TInsuranceDetail detail);
/** 是否为历史提前缴费 — 走既有逻辑,改动需回归全量见费出单 */
boolean isLegacyJfcdData(TInsuranceDetail detail);
// isJfcd=0 && (isHistoryAdvancePay==null || isHistoryAdvancePay=="1")
// isJfcd=0 && (isNewData==null || isNewData=="0")
/** 是否为新提前缴费 — 走本需求新逻辑 */
boolean isNewJfcdData(TInsuranceDetail detail);
// isJfcd=0 && isHistoryAdvancePay=="0"
// isJfcd=0 && isNewData=="1"
```
所有涉及见费出单的分支,**先判 `isJfcd`,再判 `isLegacyJfcdData` / `isNewJfcdData`**,避免误伤非见费出单。
......@@ -210,8 +207,8 @@ boolean isNewJfcdData(TInsuranceDetail detail);
现有行为(见 [HRO-见费出单既有逻辑梳理.md](./HRO-见费出单既有逻辑梳理.md) 第四章):
1. `initJfcdInfo` → 待缴费(6)、写预估 settle、`defaultSettleId``is_history_advance_pay=1`
2. `pushList``createInsuranceInfo` + `pushEstime`(EKP 预估带 `fd_is_history_advance_pay=是`
1. `initJfcdInfo` → 待缴费(6)、写预估 settle、`defaultSettleId``is_new_data=0`
2. `pushList``createInsuranceInfo` + `pushEstime`(EKP 预估带 `fd_new_data=0`
3. 待办 `processStatus`**7(待缴费)**
**实施方式**:将 `initJfcdInfo``isJfcd=0`**原样提取**`initLegacyJfcdInfo(...)`,调用处不变;或在 `initJfcdInfo` 顶部:
......@@ -222,7 +219,7 @@ if (AdvancePaymentHelper.isLegacyJfcdData(detail)) {
... 现有逻辑 ...
return;
}
// ===== 新提前缴费逻辑(isHistoryAdvancePay=0)=====
// ===== 新提前缴费逻辑(isNewData=0)=====
initNewAdvancePayInfo(...);
```
......@@ -237,7 +234,7 @@ initNewAdvancePayInfo(...);
| 收入/管理费/风险金 | **派单时不调用** `createInsuranceInfo`**延至** `registeredPolicyPremium` 登记实缴时按项目配置生成并推送 EKP(与实缴金额挂钩) |
| EKP 预估 | **不调用** `pushEstime` |
| 待办 | `processStatus`**3(待投保)**,与非见费出单一致 |
| 是否历史 | 落库 `is_history_advance_pay=0`;登记实缴推 EKP 时 `fd_is_history_advance_pay=否` |
| 是否历史 | 落库 `is_new_data=0`;登记实缴推 EKP 时 `fd_new_data=1` |
`addInsurance` / `batchInsurance` 中现有逻辑:
......@@ -323,21 +320,21 @@ if (AdvancePaymentHelper.isLegacyJfcdData(detail)) {
| BPO 项目 | EKP 应收=**0**,应支=**实缴金额** |
| 非 BPO | 应收=应付=**实缴金额** |
| **有无预估** | **固定为「否」**`fd_3af9d1441ef7b6=否`) |
| **是否历史提前缴费** | **固定「否」**`fd_is_history_advance_pay=否`,与 HRO `is_history_advance_pay=0` 一致) |
| **是否新数据** | **固定「是」**`fd_new_data=1`,与 HRO `is_new_data=0` 一致) |
| 收入/管理费/风险金 | 登记实缴时调用 `createInsuranceInfo`(或等价逻辑),金额基于**实缴保费** |
| 校验 | `registeredPolicyPremiumCheck` 中「EKP 预估结算中/已结算」校验对无 `defaultSettleId` 的新数据**放宽或跳过** |
**EKP 推送改造点**`DoJointInsuranceTask.insertInsrancesDetail` / `initValues`
- 历史:`fd_jfcd=是``fd_is_history_advance_pay=是`,预估/实缴/差额 `有无预估=有`(既有规则)
- 新:`fd_jfcd=是`**`fd_is_history_advance_pay=否`**,实缴 **`有无预估=否`**;应收/应支按 BPO/非 BPO 规则
- 历史:`fd_jfcd=是``fd_new_data=0`,预估/实缴/差额 `有无预估=有`(既有规则)
- 新:`fd_jfcd=是`**`fd_new_data=1`**,实缴 **`有无预估=否`**;应收/应支按 BPO/非 BPO 规则
**`initValues` 改造示例**`fd_jfcd=是` 时写入是否历史,与 HRO 明细一致):
```java
pushParam.setFd_jfcd("是");
pushParam.setFd_is_history_advance_pay(
AdvancePaymentHelper.toEkpIsHistoryAdvancePay(param.getIsHistoryAdvancePay())); // 1→是,0→否
pushParam.setFd_new_data(
AdvancePaymentHelper.toEkpFdNewData(param.getIsNewData())); // HRO/EKP 同值直传
if (AdvancePaymentHelper.isNewJfcdData(param)) {
pushParam.setFd_3af9d1441ef7b6("否"); // 新提前缴费实缴:有无预估=否
} else {
......@@ -392,7 +389,7 @@ if (AdvancePaymentHelper.isNewJfcdData(param)) {
**实现要点**
1. `rollBackInsurance` 入口:先 `isLegacyJfcdData` / `isNewJfcdData` 分流,**前置校验逻辑不可共用**
2. 新数据负数实缴:复制实缴 settle 为 `burden``actualPremium` 取负,`pushEstimate(detail, SIX_INT)`;EKP 带 **`fd_is_history_advance_pay=否`**
2. 新数据负数实缴:复制实缴 settle 为 `burden``actualPremium` 取负,`pushEstimate(detail, SIX_INT)`;EKP 带 **`fd_new_data=1`**
3. HRO 明细:`actualPremium` 清零;**不**清空 `paymentTime`/`paymentStatus`(新数据不经待缴费)。
4. **不生成**负数预估、**不生成**差额数据。
......@@ -423,7 +420,7 @@ if (AdvancePaymentHelper.isNewJfcdData(param)) {
```java
/**
* 撤回 — 仅适用于待缴费(6)的历史见费出单。
* 新提前缴费(isHistoryAdvancePay=0)派单后直接待投保,不经待缴费;前端已禁用撤回按钮,后端无需单独分支。
* 新提前缴费(isNewData=1)派单后直接待投保,不经待缴费;前端已禁用撤回按钮,后端无需单独分支。
*/
```
......@@ -457,11 +454,11 @@ if (CommonConstants.ZERO_STRING.equals(byId.getIsJfcd())) {
}
```
新数据派单后 `isJfcd=0`,与历史见费出单相同,**现有校验已覆盖新数据**,无需按 `isHistoryAdvancePay` 再开分支。
新数据派单后 `isJfcd=0`,与历史见费出单相同,**现有校验已覆盖新数据**,无需按 `isNewData` 再开分支。
| 项 | 说明 |
| --- | --- |
| 后端 | **`deleteInsurance` 方法体不改动**;可选在方法注释中补充:含 `isHistoryAdvancePay=0` 的新提前缴费同样适用 `isJfcd=0` 不可删规则 |
| 后端 | **`deleteInsurance` 方法体不改动**;可选在方法注释中补充:含 `isNewData=0` 的新提前缴费同样适用 `isJfcd=0` 不可删规则 |
| 前端 | 新提前缴费 / 见费出单记录**禁用或隐藏**「删除投保」按钮 |
| 非见费出单 | 不变:须状态为退回(4) 且为创建人本人方可删 |
......@@ -470,7 +467,7 @@ if (CommonConstants.ZERO_STRING.equals(byId.getIsJfcd())) {
```java
/**
* 删除投保 — 非见费出单(isJfcd=1)且退回(4)时可删。
* 提前缴费(isJfcd=0)不可删,含历史(isHistoryAdvancePay=1)与新(isHistoryAdvancePay=0)提前缴费。
* 提前缴费(isJfcd=0)不可删,含历史(isNewData=0)与新(isNewData=1)提前缴费。
*/
```
......@@ -521,14 +518,14 @@ yifu-insurances-biz/
### 6.2 注释模板
```java
// ========== 历史见费出单(isHistoryAdvancePay=1)==========
// ========== 历史见费出单(isNewData=1)==========
// 逻辑版本:v1.7.x 既有实现,对应文档 HRO-见费出单既有逻辑梳理.md
// 变更原则:仅 bugfix,新需求不得修改此块
if (AdvancePaymentHelper.isLegacyJfcdData(detail)) {
...
}
// ========== 新提前缴费(isHistoryAdvancePay=0)==========
// ========== 新提前缴费(isNewData=0)==========
// 需求:HRO-EKP提前缴费商险数据兼容需求.md §1.2、§1.3
// 规则:无预估/差额;派单后直接待投保;实缴按 BPO/非 BPO 推 EKP
else if (AdvancePaymentHelper.isNewJfcdData(detail)) {
......@@ -541,14 +538,13 @@ else if (AdvancePaymentHelper.isNewJfcdData(detail)) {
`InsurancesConstants` 增加:
```java
/** 是否历史提前缴费:是(历史,走既有逻辑) */
String IS_HISTORY_ADVANCE_PAY_YES = "1";
/** 是否历史提前缴费:否(新数据) */
String IS_HISTORY_ADVANCE_PAY_NO = "0";
/** EKP 推送:是 */
String EKP_IS_HISTORY_ADVANCE_PAY_YES = "是";
/** EKP 推送:否 */
String EKP_IS_HISTORY_ADVANCE_PAY_NO = "否";
/** 是否新数据:是(新提前缴费) */
String IS_NEW_DATA_YES = "1";
/** 是否新数据:否(历史见费出单) */
String IS_NEW_DATA_NO = "0";
/** EKP fd_new_data,与 HRO 同值 */
String EKP_FD_NEW_DATA_YES = IS_NEW_DATA_YES;
String EKP_FD_NEW_DATA_NO = IS_NEW_DATA_NO;
```
---
......@@ -598,7 +594,7 @@ stateDiagram-v2
### 9.1 历史数据 — **冒烟即可**
- 派单 → 待缴费 → EKP 回调 → 待投保 → 投保成功 → 登记实缴+差额 → 退保
- 确认 `isHistoryAdvancePay=1` 后行为与上线前**完全一致**(选 2~3 条典型用例)
- 确认 `isNewData=0` 后行为与上线前**完全一致**(选 2~3 条典型用例)
### 9.2 新数据 — **全量场景**
......@@ -615,9 +611,9 @@ stateDiagram-v2
| 9 | EKP 回调 / `pullPaymentStatus` 对新数据不触发状态变更(含 BPO 场景) |
| 10 | 前端 `getPaymentSettleStatusFromEkp` 支出中/已支出时禁用退回按钮 |
| 11 | 新提前缴费「删除投保」禁用/隐藏,后端返回 `NOT_JFCD_ERROR`(若误调) |
| 12 | 发布时间切点:`launchTime` 前写 `isHistoryAdvancePay=1`,之后写 `0` |
| 12 | 发布时间切点:`launchTime` 前写 `isNewData=0`,之后写 `1` |
| 13 | `bpoEnable=是` 项目仍不走见费出单(与现网一致) |
| 14 | EKP 预估/实缴推送均带 `fd_is_history_advance_pay`,与 HRO `is_history_advance_pay` 一致 |
| 14 | EKP 预估/实缴推送均带 `fd_new_data`,与 HRO `is_new_data` 同值 |
---
......@@ -627,7 +623,7 @@ stateDiagram-v2
2. ~~**新数据派单时是否生成收入/管理费/风险金**~~ **已确认**:派单**不生成****延至实缴登记**`registeredPolicyPremium`)时按实缴金额生成。
3. ~~**新数据已实缴且 EKP 已结算后退保**~~ **已确认**:结算中/已结算且**未支出**→ 允许退回并**生成负数实缴****支出中/已支出**→ 禁止退回(前端提示)。与历史逻辑(`payStatus=已结算/结算中` 即拦截)**分流处理**
4. ~~**EKP 实缴推送字段**~~ **已确认**:新数据 **`有无预估` 固定「否」**;见费出单条件不变(**`bpoEnable=否`**),启用 BPO 结算模式的项目**仍不走**见费出单。
5. ~~**上线切点**~~ **已确认**:以**发布时间**控制 `isHistoryAdvancePay=0` 写入,不用配置开关。
5. ~~**上线切点**~~ **已确认**:以**发布时间**控制 `isNewData=1` 写入,不用配置开关。
6. ~~**新数据撤回(待缴费)**~~ **已确认**:前端禁用撤回按钮;后端 `callBack` **不改逻辑**,仅加注释说明。
7. ~~**新数据删除投保**~~ **已确认****不可删**;现有 `isJfcd=0` 校验已覆盖,**不改逻辑**,可选加注释;前端禁用删除按钮。
......@@ -637,13 +633,13 @@ stateDiagram-v2
| 逻辑 | 类 / 方法 | 改造类型 |
| --- | --- | --- |
| 见费出单标识 | `initJfcd` / `initJfcdBatch` | 发布时间切点 + `isHistoryAdvancePay``bpoEnable=否` 不变 |
| 派单核心 | `initJfcdInfo` | **分流**;历史推 EKP 预估带 `fd_is_history_advance_pay=是` |
| 见费出单标识 | `initJfcd` / `initJfcdBatch` | 发布时间切点 + `isNewData``bpoEnable=否` 不变 |
| 派单核心 | `initJfcdInfo` | **分流**;历史推 EKP 预估带 `fd_new_data=0` |
| 派单后续 | `addInsurance` / `batchInsurance` | 新数据不进 `pushList`、不 `createInsuranceInfo` |
| 是否历史字段 | `TInsuranceDetail.isHistoryAdvancePay` | 派单落库;实缴/预估推送 EKP 同源 |
| 是否历史字段 | `TInsuranceDetail.isNewData` | 派单落库;实缴/预估推送 EKP 同源 |
| 收入/管理费/风险金 | `createInsuranceInfo` | 新数据:**登记实缴时**调用 |
| 推 EKP 预估 | `DoJointInsuranceTask.pushEstime` | 历史专用;带 `fd_is_history_advance_pay=是` |
| EKP 费用规则 | `DoJointInsuranceTask.insertInsrancesDetail` / `initValues` | 实缴:`有无预估` + `fd_is_history_advance_pay` |
| 推 EKP 预估 | `DoJointInsuranceTask.pushEstime` | 历史专用;带 `fd_new_data=0` |
| EKP 费用规则 | `DoJointInsuranceTask.insertInsrancesDetail` / `initValues` | 实缴:`有无预估` + `fd_new_data` |
| 缴费同步 | `DoJointInsuranceTask.updateInsuranceSettleStatus` | 新数据入口直接跳过 |
| 手动拉取 | `TInsuranceDetailServiceImpl.pullPaymentStatus` | 查询过滤 + 新数据跳过 |
| 登记实缴/差额 | `registeredPolicyPremium` | 新数据跳过差额 |
......@@ -664,5 +660,6 @@ stateDiagram-v2
| 2026-06-17 | 撤回:新数据前端禁用按钮,后端 callBack 不改逻辑,仅加注释 |
| 2026-06-17 | 删除投保:新提前缴费不可删,isJfcd=0 现有校验已覆盖,不改逻辑 |
| 2026-06-17 | 确认:收入延至实缴登记;EKP 有无预估=否;bpoEnable 条件不变;发布时间切点 |
| 2026-06-17 | 统一字段「是否历史提前缴费」:HRO `is_history_advance_pay` ↔ EKP `fd_is_history_advance_pay` |
| 2026-06-17 | 统一字段「是否新数据」:HRO `is_new_data` ↔ EKP `fd_new_data`(同值) |
| 2026-06-18 | HRO 字段由 `is_history_advance_pay` 更名为 `is_new_data`(1新/0历史,与 EKP 对齐) |
| 2026-06-17 | 补充 HRO/EKP 加列 DDL 及存量刷数 SQL(§2.4、§2.5) |
## Context
HRO 商险见费出单(`isJfcd=0`)当前流程:派单 → 待缴费(6) → 推 EKP 预估 → EKP 收款 → 待投保(1) → 投保成功 → 登记实缴(可生成差额)。EKP 新提前缴费规则要求新数据跳过待缴费与预估,实缴直接按 BPO/非 BPO 推 EKP。
既有代码集中在 `yifu-insurances-biz``TInsuranceDetailServiceImpl`(派单/实缴/退保)和 `DoJointInsuranceTask`(EKP 推送)。参考文档:`V24/HRO-提前缴费商险数据兼容开发方案.md`
## Goals / Non-Goals
**Goals:**
- 通过 `is_history_advance_pay` 字段分流历史/新提前缴费逻辑
- 历史分支(`isHistoryAdvancePay=1` 或 NULL)行为与上线前完全一致
- 新分支实现:派单直接待投保、无预估/差额、实缴登记推 EKP、独立退保规则
- DDL + 存量迁移 + 发布时间切点可配置上线
**Non-Goals:**
- 重构或优化历史见费出单代码块
- 修改 `bpoEnable=是` 不走见费出单的规则
- EKP 侧表单/流程大改(需联调配合,不在 HRO 单端 scope)
- yifu-ui 具体页面改造(仅标注需同步的前端行为)
## Decisions
### D1: 分流入口统一为 `AdvancePaymentHelper`
**选择**: 新增工具类 `AdvancePaymentHelper`,提供 `isLegacyJfcdData` / `isNewJfcdData` / `toEkpFdNewData`
**理由**: 避免散落 `isJfcd` 判断误伤非见费出单;所有分支先判 `isJfcd` 再判历史/新。
**备选**: 直接在 `InsurancesConstants` 加静态方法 — 拒绝,职责不清。
### D2: 历史逻辑「代码块不动」,新逻辑独立 private 方法
**选择**: `initJfcdInfo` 顶部 if-else 分流;历史块原样保留或提取为 `initLegacyJfcdInfo`;新逻辑 `initNewAdvancePayInfo`
**理由**: 最小回归风险;代码 review 时可清晰识别变更边界。
**备选**: 拆到 `advance/` 包下 Handler 类 — 可选,文档建议 P0 可先 private 方法,后续再拆。
### D3: 发布时间切点而非功能开关
**选择**: 配置 `advance-pay.new-data-launch-time`;满足见费出单且 `now >= launchTime``isHistoryAdvancePay=0`
**理由**: 产品已确认;避免误开开关导致新逻辑作用于未就绪环境。
### D4: 数据模型
| 层 | 字段 | 取值 |
|---|---|---|
| HRO `t_insurance_detail` | `is_history_advance_pay` CHAR(1) | `1` 历史 / `0` 新 / NULL 非提前缴费 |
| EKP `ekp_insurances_info` | `fd_new_data` CHAR(1) | `1` 新 / `0` 历史;默认 `1` |
| Java | `TInsuranceDetail.isHistoryAdvancePay` | 与库表一致 |
**写入规则**: 存量 `is_jfcd=0` → 迁移为 `1`;新数据派单写 `0`;EKP 推送在 `DoJointInsuranceTask.initValues` 统一转换。
### D5: 新数据派单行为差异
| 项 | 历史 | 新 |
|---|---|---|
| 投保状态 | 待缴费(6) | 待投保(1) |
| 预估 settle | 创建 + pushEstime | 不创建 |
| createInsuranceInfo | 派单时 | 实缴登记时 |
| 待办 processStatus | 7 待缴费 | 3 待投保 |
| pushList | 加入 | 不加入 |
### D6: 新数据实缴 EKP 规则
- `fd_new_data=1``有无预估=否`
- BPO: 应收=0,应付=实缴;非 BPO: 应收=应付=实缴
- 跳过差额生成;`registeredPolicyPremiumCheck` 对无 `defaultSettleId` 放宽
### D7: 新数据退保 vs 历史
| 拦截条件 | 历史 | 新 |
|---|---|---|
| EKP payStatus | 已结算/结算中 → 禁止 | 支出中/已支出 → 禁止 |
| 负数预估/差额 | 有 | 无 |
| 已结算未支出 | — | 允许,推负数实缴 |
### D8: 不需改逻辑的场景(仅注释)
- `callBack`(撤回):新数据不经待缴费,前端禁用按钮
- `deleteInsurance`:现有 `isJfcd=0` 拦截已覆盖新数据
- `callBackInsuranceBuyHandleStatusSub`:新数据 no-op
## Risks / Trade-offs
| 风险 | 缓解 |
|---|---|
| 历史分支被误改引发回归 | 代码 review 强制检查历史注释块;历史数据冒烟 2~3 条 |
| EKP 字段 ID/联调不一致 | `fd_new_data` 与 EKP 确认表单 ID;P2 联调实缴推送 |
| 自动化/续签待办状态不一致 | P1 验证 `updateEmployeeInsurancePre` / `updateEmployeeInsurancePreRenew` 新分支写 3 |
| 列表/详情无预估 settle 展示异常 | P4 兼容 `defaultSettleId` 为空 |
## Migration Plan
1. **DDL**: HRO `§2.4.1` → EKP `§2.4.2`
2. **迁移**: HRO `§2.5.1` → EKP `§2.5.2`(全量历史刷 HRO `1` / EKP `0`
3. **发版 HRO**: 含 `AdvancePaymentHelper` + 历史分流 + 新分支(可先不开新数据写入,launchTime 设未来)
4. **验证历史**: 冒烟确认 `isHistoryAdvancePay=1` 行为不变
5. **调整 launchTime**: 到达切点后新派单写 `0`,全量测试 §9.2 场景
6. **EKP 联调**: 实缴 `有无预估=否` + `fd_new_data=1`
7. **前端同步**: 禁用撤回/删除;退保前查支出状态
**回滚**: 将 `launchTime` 设为未来时间停止新数据写入;新分支代码可保留(无 `0` 数据不触发);DDL 列可保留(nullable,不影响历史)。
## API / 模块影响
| 模块 | 变更 |
|---|---|
| `yifu-insurances-api` | `TInsuranceDetail` + VO/DTO 增字段 |
| `yifu-insurances-biz` | Mapper XML、ServiceImpl、DoJointInsuranceTask、Constants、Helper |
| `application-*.yml` | `advance-pay.new-data-launch-time` |
| EKP 双数据源 | `EkpInsurancesInfo``fdNewData` |
| yifu-ui(独立仓库) | 撤回/删除按钮禁用;退保前 `getPaymentSettleStatusFromEkp` |
## Open Questions
1. EKP 表单字段 `fd_new_data` 的最终表单 ID(联调确认)
2. `getPaymentSettleStatusFromEkp` 返回的 `payStatus` 枚举值是否与文档一致(实现时对照 EKP 回传)
## 关键代码锚点
| 逻辑 | 类/方法 | 改造 |
|---|---|---|
| 见费出单标识 | `initJfcd` / `initJfcdBatch` | launchTime + 写字段 |
| 派单核心 | `initJfcdInfo` | 分流 |
| 派单后续 | `addInsurance` / `batchInsurance` | 新数据不进 pushList |
| EKP 预估 | `DoJointInsuranceTask.pushEstime` | 历史专用 |
| EKP 实缴 | `initValues` / `insertInsrancesDetail` | 新字段 + 有无预估 |
| 缴费同步 | `updateInsuranceSettleStatus` | 新数据 skip |
| 手动拉取 | `pullPaymentStatus` | 过滤 + skip |
| 登记实缴 | `registeredPolicyPremium` | 新分支无差额 |
| 退保 | `rollBackInsurance` | 新分支独立 |
## Why
HRO 侧见费出单(提前缴费)现有逻辑要求派单后进入待缴费、推送 EKP 预估,与 EKP 新提前缴费业务规则不兼容。需在**不改动历史见费出单行为**的前提下,通过新字段区分历史/新数据,让新提前缴费派单后直接待投保、无预估/差额,实缴按 BPO/非 BPO 规则推送 EKP。
## What Changes
- 新增 `is_history_advance_pay` 字段(HRO `t_insurance_detail` + EKP `ekp_insurances_info.fd_new_data`),HRO `1/0` 与 EKP `0/1` **反向**映射(EKP 是否新数据:1是/0否,默认1)
- 存量 `is_jfcd=0` 数据迁移为历史(`is_history_advance_pay=1`);发布时间之后新增见费出单写 `0`
- 新增 `AdvancePaymentHelper` 分流工具,历史分支代码块原样保留,新逻辑独立方法/注释块
- 新提前缴费派单:跳过待缴费(6)、不推 EKP 预估、不生成预估 settle/差额;待办直接待投保(3)
- 新提前缴费实缴:登记保单保费时推送实缴(有无预估=否),BPO/非 BPO 应收应付规则;收入/管理费/风险金延至实缴登记
- 新提前缴费退保:独立校验(支出中/已支出禁止);不生成负数预估/差额;按实缴结算状态删实缴或推负数实缴
- EKP 回调/手动拉取对新数据跳过;撤回/删除投保逻辑不改,仅加注释(前端禁用按钮)
- 配置项 `advance-pay.new-data-launch-time` 控制新数据写入切点
## Capabilities
### New Capabilities
(无 — 本需求为既有商险域行为扩展,不引入新 spec 域)
### Modified Capabilities
- `insurances`: 见费出单(提前缴费)需支持历史/新两套分流逻辑;新增字段标识、派单/实缴/退保/EKP 推送规则变更
## Impact
- **模块**: `yifu-insurances``*-api` Entity/VO + `*-biz` Service/Mapper/Task)
- **数据库**: HRO `mvp_insurances.t_insurance_detail`;EKP `ekp.ekp_insurances_info`(DDL + 存量迁移)
- **核心类**: `TInsuranceDetailServiceImpl``DoJointInsuranceTask``InsurancesConstants`、新增 `AdvancePaymentHelper`
- **EKP 联调**: `initValues` / `insertInsrancesDetail` 实缴字段(`fd_new_data``有无预估`
- **前端**(yifu-ui,独立仓库): 新数据禁用撤回/删除按钮;退保前查 `getPaymentSettleStatusFromEkp` 禁用支出中/已支出
- **Non-goals**: 不重构历史见费出单代码块;不改动 `bpoEnable=是` 项目不走见费出单的既有规则;EKP 侧大改不在本 change HRO 范围内(需 EKP 联调配合)
## ADDED Requirements
### Requirement: Advance Payment Data Classification
The system SHALL distinguish legacy advance-pay (见费出单) data from new advance-pay data using `is_history_advance_pay` on `TInsuranceDetail`, synchronized to EKP as `fd_new_data` (`1`历史→`0`, `0`新→`1`;EKP 默认 `1`).
#### Scenario: Legacy data identification
- **WHEN** `isJfcd=0` and (`isHistoryAdvancePay` is `1` or NULL)
- **THEN** the record MUST follow legacy advance-pay workflow unchanged from pre-release behavior
- **AND** EKP pushes for estimate/actual/difference MUST set `fd_new_data=0`
#### Scenario: New data identification after launch time
- **WHEN** `isJfcd=0`, `isHistoryAdvancePay=0`, and record created at or after `advance-pay.new-data-launch-time`
- **THEN** the record MUST follow new advance-pay workflow defined in this change
- **AND** EKP actual-premium pushes MUST set `fd_new_data=1`
#### Scenario: Stock data migration
- **WHEN** existing records have `is_jfcd=0` before release
- **THEN** migration MUST set `is_history_advance_pay=1`
- **AND** behavior MUST remain identical to pre-migration legacy advance-pay
### Requirement: New Advance-Pay Dispatch Workflow
For new advance-pay data (`isHistoryAdvancePay=0`), the system SHALL skip pending-payment state and EKP estimate push on dispatch.
#### Scenario: Dispatch directly to pending insurance
- **WHEN** new advance-pay insurance is created via add or batch add
- **THEN** `buyHandleStatus` MUST remain pending insurance (1), not pending payment (6)
- **AND** `processStatus` for pre-tasks MUST be pending insurance (3), not pending payment (7)
#### Scenario: No estimate on dispatch
- **WHEN** new advance-pay dispatch completes
- **THEN** the system MUST NOT create estimate `TInsuranceSettle`, MUST NOT set `defaultSettleId`, and MUST NOT call `pushEstime`
- **AND** the system MUST NOT call `createInsuranceInfo` at dispatch time
#### Scenario: BPO project exclusion unchanged
- **WHEN** project has `bpoEnable=是`
- **THEN** advance-pay (`isJfcd=0`) MUST NOT be triggered, same as existing `initJfcd` rules
### Requirement: New Advance-Pay Actual Premium Registration
For new advance-pay insured records, actual premium registration SHALL push EKP actual settlement without difference generation.
#### Scenario: Register actual premium without difference
- **WHEN** `registeredPolicyPremium` is invoked for new advance-pay data with valid actual premium
- **THEN** the system MUST push actual premium to EKP with `有无预估=否`
- **AND** the system MUST NOT generate difference settle records
#### Scenario: BPO actual premium EKP amounts
- **WHEN** new advance-pay actual premium is registered for a BPO project
- **THEN** EKP receivable MUST be 0 and payable MUST equal actual premium amount
#### Scenario: Non-BPO actual premium EKP amounts
- **WHEN** new advance-pay actual premium is registered for a non-BPO project
- **THEN** EKP receivable and payable MUST both equal actual premium amount
#### Scenario: Deferred revenue on actual premium
- **WHEN** new advance-pay actual premium is successfully registered
- **THEN** `createInsuranceInfo` (or equivalent) MUST run based on actual premium amount
- **AND** revenue/management-fee/risk-fund MUST NOT have been created at dispatch time
### Requirement: New Advance-Pay Rollback Rules
New advance-pay rollback SHALL NOT generate negative estimates or differences and SHALL use expenditure-based guard rules distinct from legacy.
#### Scenario: Block rollback when expenditure in progress or completed
- **WHEN** rollback is requested for new advance-pay data and EKP `payStatus` is expenditure-in-progress or expenditure-completed
- **THEN** rollback MUST be rejected with user-visible error
- **AND** frontend MAY disable rollback using `getPaymentSettleStatusFromEkp`
#### Scenario: Rollback without actual premium
- **WHEN** new advance-pay record is pending insurance or in-insurance with no actual premium
- **THEN** rollback MUST set status to rolled-back (4) without negative estimate or actual-premium EKP push
#### Scenario: Rollback with unsettled actual premium
- **WHEN** new advance-pay insured record has actual premium not yet settled in EKP
- **THEN** rollback MUST delete HRO/EKP actual premium data without negative estimate or difference
#### Scenario: Rollback with settled but unexpended actual premium
- **WHEN** new advance-pay insured record has EKP settle status settling or settled but expenditure not started
- **THEN** rollback MUST push negative actual premium to EKP
- **AND** MUST NOT generate negative estimate or difference
### Requirement: EKP Payment Callback Isolation for New Data
EKP payment callbacks and manual payment pull MUST NOT alter state for new advance-pay records that never enter pending payment.
#### Scenario: Skip EKP settle callback for new data
- **WHEN** `updateInsuranceSettleStatus` receives callback for new advance-pay detail
- **THEN** the method MUST return without updating `buyHandleStatus`, `paymentStatus`, or pre-task status
#### Scenario: Exclude new data from manual payment pull
- **WHEN** `pullPaymentStatus` runs
- **THEN** new advance-pay records MUST be excluded from pull list
- **AND** direct pull by new-data ID MUST be skipped without side effects
## MODIFIED Requirements
### Requirement: Insurance Purchase Processing
The system SHALL manage commercial insurance purchase from application through handling success, including add, batch add, replace, and rollback. For advance-pay (`isJfcd=0`) records, behavior MUST branch on `isHistoryAdvancePay` between legacy and new workflows.
#### Scenario: Create insurance application
- GIVEN an employee eligible for commercial insurance under project rules
- WHEN a purchase is submitted via `/insuranceDetail` add or batch-add endpoints
- THEN TInsuranceDetail records are created with pending handling status
- AND operation history is recorded in TInsuranceOperate
- AND advance-pay records MUST set `isHistoryAdvancePay` per launch-time rules
#### Scenario: Complete insurance handling
- GIVEN a pending insurance handling record
- WHEN handling succeeds via import or manual confirmation
- THEN the record moves to insured status with premium and policy linkage
- AND pending payment amounts are updated on the purchase list view for legacy advance-pay only
#### Scenario: Insurance rollback
- GIVEN a purchase in handling state or insured state with advance-pay flag
- WHEN rollback is requested
- THEN legacy advance-pay MUST follow existing rollback (negative estimate + delete EKP actual/difference)
- AND new advance-pay MUST follow new rollback rules without negative estimate or difference
### Requirement: Premium and Policy Registration
The system SHALL support registration of policy premiums, invoice numbers, and policy period maintenance. For new advance-pay data, validation MUST tolerate absence of estimate `defaultSettleId`.
#### Scenario: Register policy premium
- GIVEN a handled insurance record
- WHEN premium registration is submitted
- THEN premium amount and billing metadata are persisted on TInsuranceDetail
- AND invoice number can be registered separately
- AND new advance-pay registration MUST apply BPO/non-BPO EKP amount rules and skip difference generation
#### Scenario: Maintain insurance policy
- GIVEN an insurance company and policy number
- WHEN TInsurancePolicy is configured via `/insurancePolicy`
- THEN policy start/end dates and coverage windows are available for purchase validation
- AND policy detail supports CRUD with permission checks
### Requirement: EKP Integration and Scheduled Tasks
The insurances module SHALL integrate with EKP for settlement push/retry and run scheduled jobs for expiry refresh. EKP push MUST include `fd_new_data` for all advance-pay estimate and actual payloads.
#### Scenario: EKP resend
- GIVEN a failed or pending EKP push
- WHEN TInsuranceEkpController triggers resend
- THEN the insurance settlement event is re-submitted to EKP
- AND push outcome is traceable
#### Scenario: Daily expiry refresh
- GIVEN scheduled task invocation via `@Inner` endpoint `/inner/updateOverdueByTask`
- WHEN the job runs
- THEN overdue insurance records and renewal triggers are refreshed
- AND related pre-tasks or warnings are updated
#### Scenario: EKP field mapping for advance-pay
- GIVEN an advance-pay push to EKP with `fd_jfcd=是`
- WHEN `initValues` builds the push payload
- THEN `fd_new_data` MUST reflect HRO `isHistoryAdvancePay` via standardized conversion
- AND new advance-pay actual push MUST set `有无预估=否` while legacy MUST retain existing estimate flag rules
## 1. 数据层与基础组件(P0)
- [x] 1.1 编写 HRO DDL:`t_insurance_detail` 新增 `is_history_advance_pay``mvp_insurances`
- [x] 1.2 编写 EKP DDL:`ekp_insurances_info` 新增 `fd_new_data``ekp` 库,默认 `1`
- [x] 1.3 编写存量迁移 SQL:HRO `is_jfcd=0``is_history_advance_pay=1`;EKP `fd_jfcd=是``fd_new_data=0`
- [x] 1.4 `yifu-insurances-api``TInsuranceDetail` Entity 增 `isHistoryAdvancePay` 及 VO/DTO/Param 同步
- [x] 1.5 `yifu-insurances-biz`:Mapper XML 增字段映射;列表/详情出参包含新字段
- [x] 1.6 EKP 实体 `EkpInsurancesInfo``fdNewData` 及 Mapper
- [x] 1.7 `InsurancesConstants``IS_HISTORY_ADVANCE_PAY_YES/NO``EKP_FD_NEW_DATA_YES/NO`
- [x] 1.8 新增 `AdvancePaymentHelper``isJfcd``isLegacyJfcdData``isNewJfcdData``toEkpFdNewData`/`fromEkpFdNewData` 转换)
- [x] 1.9 配置项 `advance-pay.new-data-launch-time` 写入 `application.yml` 及各环境 yml
## 2. 见费出单标识与派单分流(P1)
- [x] 2.1 改造 `initJfcd` / `initJfcdBatch`:满足见费出单时写 `isJfcd=0`,按 launchTime 写 `isHistoryAdvancePay``bpoEnable=否` 条件不变)
- [x] 2.2 改造 `initJfcdInfo`:历史分支原样保留(或提取 `initLegacyJfcdInfo`);新增 `initNewAdvancePayInfo`(待投保、无预估 settle、不写 defaultSettleId)
- [x] 2.3 改造 `addInsurance` / `batchInsurance`:历史进 pushList + 待办 7;新数据不进 pushList、待办 3、不调用 createInsuranceInfo
- [x] 2.4 改造 `updateEmployeeInsurancePre` / `updateEmployeeInsurancePreRenew`:新数据待办状态与待投保一致
- [ ] 2.5 验证自动化商险待办 / 续签待办与新分支状态联动
## 3. EKP 推送与缴费同步(P2)
- [x] 3.1 `DoJointInsuranceTask.initValues``fd_jfcd=是` 时写入 `fd_new_data`;新实缴设 `有无预估=否`
- [x] 3.2 `DoJointInsuranceTask.pushEstime`:确认仅历史数据调用;预估带 `fd_new_data=0`
- [x] 3.3 `DoJointInsuranceTask.insertInsrancesDetail`:新实缴 BPO(应收=0/应付=实缴)与非 BPO(应收=应付=实缴)规则
- [x] 3.4 `registeredPolicyPremiumCheck`:新数据无 defaultSettleId 时放宽 EKP 预估结算校验
- [x] 3.5 `registeredPolicyPremium`:历史分支不变;新分支 `registerNewAdvancePayActualPremium`(无差额、实缴登记时 createInsuranceInfo)
- [x] 3.6 `updateInsuranceSettleStatus`:新数据入口直接 return
- [x] 3.7 `pullPaymentStatus`:查询过滤 legacy only;按 ID 传入新数据 skip
- [ ] 3.8 EKP 联调:实缴推送字段 `fd_new_data=1``有无预估=否`
## 4. 退保与 EKP 回调(P3)
- [x] 4.1 `rollBackInsurance` 入口分流:历史块不动;新分支 `rollBackNewAdvancePayInsurance`
- [x] 4.2 新退保前置校验:`payStatus` 支出中/已支出禁止(复用或扩展 `getPaymentSettleStatusFromEkp`
- [x] 4.3 新退保:无实缴 / 未结算删实缴 / 已结算未支出推负数实缴(type=6);不生成负数预估与差额
- [x] 4.4 `callBackInsuranceBuyHandleStatusSub`:新数据 no-op
- [x] 4.5 `callBack` / `deleteInsurance`:补充注释说明新数据前端禁用,逻辑不改
- [x] 4.6 `successfulInsurance`:补充历史/新分支注释(跳过推预估说明)
## 5. 展示兼容与测试(P4)
- [x] 5.1 详情/列表:`premiumType`、无预估 settle 时 `defaultSettleId` 为空兼容
- [x] 5.2 查询结算状态接口:无 defaultSettleId 时不报错
- [ ] 5.3 历史数据冒烟:派单→待缴费→EKP 回调→待投保→实缴+差额→退保(2~3 条)
- [ ] 5.4 新数据全量场景测试(§9.2 共 14 条,用例见 `test-plan.md`
- [ ] 5.5 协调 yifu-ui:新数据禁用撤回/删除;退保前查支出状态禁用按钮(用例 UI-01 ~ UI-04)
# HRO 提前缴费商险数据兼容 — 自动化测试用例清单
> 关联 change:`hro-advance-pay-insurance-compat`
> 参考:`V24/HRO-提前缴费商险数据兼容开发方案.md` §9
> 模块:`yifu-insurances`(端口 5005),前端:`yifu-ui`(独立仓库)
## 测试分层建议
| 层级 | 工具建议 | 覆盖目标 | 优先级 |
|------|----------|----------|--------|
| **L1 单元** | JUnit 5 + Mockito | `AdvancePaymentHelper`、launchTime 切点、HRO↔EKP 字段转换 | P0 |
| **L2 服务** | Spring Boot Test + H2/测试库 | `initJfcdInfo` 分流、`registeredPolicyPremium`、退保分支 | P1 |
| **L3 接口** | MockMvc / RestAssured | Controller 入参出参、权限、状态码 | P1 |
| **L4 集成** | 测试环境 + EKP Mock/联调库 | EKP 推送字段、回调、实缴/差额 | P2 |
| **L5 E2E** | yifu-ui + 测试环境 | 按钮禁用、完整业务流程 | P2 |
> 当前 `yifu-insurances` 尚无 `src/test` 用例,建议先落地 **L1 + L2 核心分流**,再补 L3/L4。
---
## 一、L1 单元测试(可纯自动化,无外部依赖)
### UT-01 `AdvancePaymentHelper.isLegacyJfcdData`
| 用例 ID | 输入 | 期望 |
|---------|------|------|
| UT-01-01 | `isJfcd=1` | `false` |
| UT-01-02 | `isJfcd=0`, `isNewData=null` | `true` |
| UT-01-03 | `isJfcd=0`, `isNewData=0` | `true` |
| UT-01-04 | `isJfcd=0`, `isNewData=1` | `false` |
### UT-02 `AdvancePaymentHelper.isNewJfcdData`
| 用例 ID | 输入 | 期望 |
|---------|------|------|
| UT-02-01 | `isJfcd=0`, `isNewData=1` | `true` |
| UT-02-02 | `isJfcd=0`, `isNewData=0` | `false` |
| UT-02-03 | `isJfcd=0`, `isNewData=null` | `false` |
| UT-02-04 | `isJfcd=1`, `isNewData=1` | `false` |
### UT-03 `AdvancePaymentHelper` 字段转换
| 用例 ID | 方法 | 输入 | 期望 |
|---------|------|------|------|
| UT-03-01 | `toEkpFdNewData` | `0`(历史) | `0` |
| UT-03-02 | `toEkpFdNewData` | `1`(新) | `1` |
| UT-03-03 | `fromEkpFdNewData` | `0` | `0` |
| UT-03-04 | `fromEkpFdNewData` | `1` | `1` |
### UT-04 `resolveIsNewDataForNewRecord`(launchTime 切点)
| 用例 ID | 配置 | 当前时间(Mock) | 期望 |
|---------|------|------------------|------|
| UT-04-01 | `launchTime=2099-01-01` | 2026-06-18 | `0`(历史) |
| UT-04-02 | `launchTime=2026-01-01` | 2026-06-18 | `1`(新) |
| UT-04-03 | `launchTime=2026-06-18 00:00:00` | 同日 00:00:00 | `1` |
| UT-04-04 | 非法配置字符串 | 任意 | `0`(保守走历史) |
**对应方案场景**:§9.2 #12
---
## 二、L2 服务层测试(Mock EKP / 收入服务)
### SVC-01 派单分流 — 新增 `addInsurance`
| 用例 ID | 数据标识 | 前置 | 操作 | 断言 |
|---------|----------|------|------|------|
| SVC-01-01 | 历史 | 险种见费出单=是,`bpoEnable=否`,launchTime 未来 | 新增派单 | `buyHandleStatus=6``defaultSettleId` 非空;`pushEstime` 被调用;待办 `processStatus=7` |
| SVC-01-02 | 新 | 同上,launchTime 已过 | 新增派单 | `buyHandleStatus=1``defaultSettleId` 空;`pushEstime` 未调用;`createInsuranceInfo` 未在派单时调用;待办 `processStatus=3` |
| SVC-01-03 | 非见费 | `isJfcd=1` | 新增派单 | `isNewData` 空;走非见费出单逻辑 |
| SVC-01-04 | BPO 项目 | `bpoEnable=是` | 新增派单 | `isJfcd=1`;不走提前缴费 |
**对应方案场景**:§9.2 #1、#2、#13;tasks 2.1–2.3
### SVC-02 派单分流 — 批增 `batchInsurance`
| 用例 ID | 说明 | 断言(同 SVC-01,操作改为批增) |
|---------|------|--------------------------------|
| SVC-02-01 | 历史批增 | 同 SVC-01-01 |
| SVC-02-02 | 新批增 | 同 SVC-01-02 |
### SVC-03 登记保单保费 `registeredPolicyPremium`
| 用例 ID | 数据标识 | 前置 | 操作 | 断言 |
|---------|----------|------|------|------|
| SVC-03-01 | 历史 | 已投保,有预估 settle | 登记实缴 ≠ 预估 | 生成差额 settle;推 EKP 差额 |
| SVC-03-02 | 新 / 非 BPO | 已投保,无 `defaultSettleId` | 登记实缴 | 无差额;`createInsuranceInfo` 调用;EKP 实缴 `有无预估=否``fd_new_data=1`,应收=应付=实缴 |
| SVC-03-03 | 新 / BPO | 项目启用 BPO 结算 | 登记实缴 | EKP 应收=0,应付=实缴 |
| SVC-03-04 | 新 | 无 defaultSettleId | 校验阶段 | 不因「预估结算中」拦截 |
**对应方案场景**:§9.2 #1、#2、#14;tasks 3.4–3.5
### SVC-04 退保 `rollBackInsurance`
| 用例 ID | 数据标识 | 投保状态 | 实缴/EKP 状态 | 期望 |
|---------|----------|----------|---------------|------|
| SVC-04-01 | 新 | 待投保/投保中 | 无实缴 | 退回成功;无负数预估/差额;无 EKP 实缴处理 |
| SVC-04-02 | 新 | 已投保 | 无实缴 | 同 SVC-04-01 |
| SVC-04-03 | 新 | 已投保 | 有实缴,未结算 | 允许;删除 HRO/EKP 实缴;不推红冲 |
| SVC-04-04 | 新 | 已投保 | 实缴结算中/已结算,支出未开始 | 允许;推负数实缴(type=6);无差额 |
| SVC-04-05 | 新 | 已投保 | 支出中/已支出 | **禁止**;返回 `REDUCE_ROLLBACK_PAY_IS_NOT_ALLOW` |
| SVC-04-06 | 历史 | 已投保 | payStatus 已结算/结算中 | **禁止**(历史规则不变) |
| SVC-04-07 | 新 | 任意 | — | 不生成负数预估 settle |
**对应方案场景**:§9.2 #3–#7;tasks 4.1–4.3
### SVC-05 缴费同步(新数据 no-op)
| 用例 ID | 入口 | 数据标识 | 期望 |
|---------|------|----------|------|
| SVC-05-01 | `updateInsuranceSettleStatus` | 新 | 不更新 `buyHandleStatus` / `paymentStatus` |
| SVC-05-02 | `pullPaymentStatus` | 新 | 不出现在待拉取列表;按 ID 传入 skip |
| SVC-05-03 | `callBackInsuranceBuyHandleStatusSub` | 新 | no-op,不回退待缴费 |
| SVC-05-04 | 上述入口 | 历史 | 行为与上线前一致 |
**对应方案场景**:§9.2 #9
### SVC-06 待办状态联动
| 用例 ID | 来源 | 数据标识 | 期望 processStatus |
|---------|------|----------|-------------------|
| SVC-06-01 | 商险待办派单 | 历史 | `7`(待缴费) |
| SVC-06-02 | 商险待办派单 | 新 | `3`(待投保) |
| SVC-06-03 | 续签待办派单 | 历史 | `7` |
| SVC-06-04 | 续签待办派单 | 新 | `3` |
**对应方案场景**:§9.2 #8;tasks 2.5
### SVC-07 不可删 / 撤回(后端)
| 用例 ID | 操作 | 数据标识 | 期望 |
|---------|------|----------|------|
| SVC-07-01 | `deleteInsurance` | 新,`isJfcd=0` | 返回 `NOT_JFCD_ERROR` |
| SVC-07-02 | `deleteInsurance` | 历史,`isJfcd=0` | 同上(现有逻辑) |
| SVC-07-03 | `callBack` | 新,`buyHandleStatus=1` | 前置校验拒绝(非待缴费) |
| SVC-07-04 | `callBack` | 历史,`buyHandleStatus=6` | 原撤回逻辑 |
**对应方案场景**:§9.2 #11
---
## 三、L3 接口测试(MockMvc / API 自动化)
| 用例 ID | 接口 | 方法 | 场景 | 断言要点 |
|---------|------|------|------|----------|
| API-01 | `/insuranceDetail/getInsuranceListPage` | POST | 新派单后列表 | 响应含 `isNewData=1``buyHandleStatus=1` |
| API-02 | `/insuranceDetail/getInsuranceListPage` | POST | 历史派单 | `isNewData=0``buyHandleStatus=6` |
| API-03 | `/insuranceDetail/getPaymentSettleStatusFromEkp` | GET | 新数据无实缴 | 返回 200 + 空 VO,非 500 |
| API-04 | `/insuranceDetail/getPaymentSettleStatusFromEkp` | GET | 实缴支出中 | `payStatus` 为支出中/已支出(与 EKP 一致) |
| API-05 | `/insuranceDetail/rollBackInsurance` | POST | 新 + 支出中 | 业务失败 + 明确 errorMessage |
| API-06 | `/insuranceDetail/deleteInsurance` | DELETE | 新提前缴费 | `NOT_JFCD_ERROR` |
| API-07 | `/insuranceDetail/registeredPolicyPremium` | POST | 新 + 非 BPO 实缴 | 成功;可查 EKP mock 实缴字段 |
| API-08 | `/insuranceDetail/inner/updateOverdueByTask` | POST | 定时任务 | 不因新字段 NPE |
**对应方案场景**:§9.2 #10、#11;tasks 5.1–5.2
---
## 四、L4 EKP 集成 / Mock 测试
| 用例 ID | 推送类型 | 数据标识 | EKP 字段断言 |
|---------|----------|----------|--------------|
| EKP-01 | 预估 pushEstime | 历史 | `fd_jfcd=是``fd_new_data=0``有无预估=有` |
| EKP-02 | 实缴 type=2 | 新 / 非 BPO | `fd_new_data=1``有无预估=否`,应收=应付=实缴 |
| EKP-03 | 实缴 type=2 | 新 / BPO | `fd_new_data=1`,应收=0,应付=实缴 |
| EKP-04 | 实缴 type=2 | 新 | **不**生成差额单据 |
| EKP-05 | 负数实缴 type=6 | 新退保 | `fd_new_data=1` |
| EKP-06 | 任意 | 历史/新 | HRO `is_new_data` 与 EKP `fd_new_data` 同值 |
**对应方案场景**:§9.2 #14;tasks 3.8
**Mock 建议**:对 `EkpSettleService` / `insurancesInfoMapper.insert` 做 ArgumentCaptor,断言写入字段,无需真实 EKP 库。
---
## 五、L5 历史回归冒烟(自动化或半自动)
| 用例 ID | 流程 | 数据 | 自动化建议 |
|---------|------|------|------------|
| REG-01 | 派单 → 待缴费(6) → EKP 收款回调 → 待投保(1) → 导出 → 投保成功 → 登记实缴+差额 → 退保 | `isNewData=1` | 集成脚本一条跑通;对比关键字段快照 |
| REG-02 | 存量迁移后历史明细 | DB 刷数后 | SQL 断言:`is_jfcd=0` 且迁移前记录 → `is_new_data=0` |
| REG-03 | 存量 EKP 明细 | EKP 刷数后 | SQL 断言:`fd_jfcd=是``fd_new_data=0` |
**对应方案场景**:§9.1;tasks 5.3
---
## 六、yifu-ui E2E 用例(前端仓库,需协调)
| 用例 ID | 页面 | 条件 | 期望 UI |
|---------|------|------|---------|
| UI-01 | 投保列表/详情 | 新提前缴费 | 「撤回」按钮隐藏或 disabled |
| UI-02 | 投保列表/详情 | 新提前缴费 | 「删除投保」隐藏或 disabled |
| UI-03 | 退保操作 | `getPaymentSettleStatusFromEkp` 支出中/已支出 | 退回按钮 disabled + 提示文案 |
| UI-04 | 派单成功提示 | 新数据 | 状态展示为「待投保」,非「待缴费」 |
**对应方案场景**:§9.2 #10、#11;tasks 5.5
---
## 七、与方案 §9.2 映射总表
| §9.2 # | 场景摘要 | 本清单用例 ID |
|--------|----------|---------------|
| 1 | 非 BPO 新派单全流程 | SVC-01-02, SVC-03-02, EKP-02 |
| 2 | BPO 新派单实缴 | SVC-01-02, SVC-03-03, EKP-03 |
| 3 | 待投保/投保中退保 | SVC-04-01 |
| 4 | 已投保未实缴退保 | SVC-04-02 |
| 5 | 已实缴未结算退保 | SVC-04-03 |
| 6 | 已结算未支出退保 | SVC-04-04 |
| 7 | 支出中/已支出禁止退保 | SVC-04-05, API-05, UI-03 |
| 8 | 待办/续签状态 | SVC-06-01 ~ 04 |
| 9 | EKP 回调/手动拉取 no-op | SVC-05-01 ~ 04 |
| 10 | 前端退保按钮 | API-04, UI-03 |
| 11 | 删除投保 | SVC-07-01, API-06, UI-02 |
| 12 | launchTime 切点 | UT-04-01 ~ 04 |
| 13 | bpoEnable 不走见费出单 | SVC-01-04 |
| 14 | EKP 字段一致 | UT-03, EKP-01 ~ 06 |
---
## 八、建议实施顺序
1. **第一批(可 CI)**:UT-01 ~ UT-04 → 约 15 个 JUnit 方法,覆盖分流核心
2. **第二批**:SVC-01 ~ SVC-05(Mock `DoJointInsuranceTask``EkpSettleService`
3. **第三批**:API-01 ~ API-08(MockMvc + `@Sql` 测试数据)
4. **第四批**:EKP-01 ~ EKP-06(联调环境或 WireMock)
5. **第五批**:REG-01 ~ 03、UI-01 ~ 04(测试环境手工/Playwright)
---
## 九、测试数据模板
```text
# 历史见费出单
is_jfcd=0, is_new_data=0, buy_handle_status=6
# 新提前缴费(launchTime 之后)
is_jfcd=0, is_new_data=1, buy_handle_status=1, default_settle_id=NULL
# 非见费出单
is_jfcd=1, is_new_data=NULL
# EKP 推送(见费出单时 fd_jfcd=是)
# 历史:fd_new_data=0;新:fd_new_data=1
# 项目条件(派单前)
insurance_type.is_jfcd=0, project.bpo_enable=否
```
---
## 十、通过标准
- [ ] L1 全部通过(CI 必跑)
- [ ] §9.2 映射表 14 条均有对应用例且通过
- [ ] REG-01 历史冒烟 1 条 + 新数据 SVC-04 退保 5 条全通过
- [ ] EKP 联调 EKP-02/03 在测试环境验真
- [ ] yifu-ui UI-01 ~ 04 在 UAT 确认
## Context
当前实现(`hro-advance-pay-insurance-compat`):
| 层 | 历史 | 新 |
|---|---|---|
| HRO `is_history_advance_pay` | `1` | `0` |
| EKP `fd_new_data` | `0` | `1` |
HRO 与 EKP 语义相反,依赖 `AdvancePaymentHelper.toEkpFdNewData` 做翻转。产品侧与 EKP 已统一为「是否新数据」。
## Goals / Non-Goals
**Goals:**
- HRO 列 `is_new_data` 与 EKP `fd_new_data` **同义同值**`1` 新 / `0` 历史,默认 `1`
- 保留 `AdvancePaymentHelper.isLegacyJfcdData` / `isNewJfcdData` 对外语义不变(历史走旧流程、新走新流程)
- 存量 `is_jfcd=0` 数据迁移后行为与改名前完全一致
- 更新 V24 文档与测试计划字段描述
**Non-Goals:**
- 不调整派单/实缴/退保/EKP 回调分支业务规则
- 不修改 EKP DDL
- 不在本 change 内实现 yifu-ui 改造(仅标注)
## Decisions
### D1: 物理列名 `is_new_data`
**选择**: `t_insurance_detail.is_new_data CHAR(1) DEFAULT '1'`,与 `is_jfcd` 命名风格一致。
**备选**: `new_data` 无前缀 — 拒绝,与现有布尔型字段不一致。
### D2: Java 属性 `isNewData`
**选择**: Entity/VO/Param 统一 `isNewData`;MyBatis 列 `IS_NEW_DATA`
**API**: JSON 序列化为 `isNewData`(camelCase)。
### D3: 分流判定(逻辑取反,行为不变)
| 方法 | 旧条件 | 新条件 |
|---|---|---|
| `isLegacyJfcdData` | `isJfcd=0` 且 (`isHistoryAdvancePay` 空或 `1`) | `isJfcd=0` 且 (`isNewData` 空或 `0`) |
| `isNewJfcdData` | `isJfcd=0``isHistoryAdvancePay=0` | `isJfcd=0``isNewData=1` |
| `resolveIsNewDataForNewRecord` | launch 后 → `0`(新) | launch 后 → `1`(新) |
空值:`is_jfcd=0``is_new_data` 空视同历史(`0`),与旧逻辑「空视同 `1` 历史」一致。
### D4: EKP 映射直传
**选择**: `pushParam.setFd_new_data(param.getIsNewData())`;常量 `IS_NEW_DATA_*``EKP_FD_NEW_DATA_*` 可合并为同一组或保留 EKP 前缀别名。
**删除**: `toEkpFdNewData` / `fromEkpFdNewData` 反向转换(或改为 identity 方法留作扩展点)。
### D5: 迁移策略
**已存在 `is_history_advance_pay`**:
```sql
ALTER TABLE t_insurance_detail
ADD COLUMN is_new_data CHAR(1) NULL DEFAULT '1' COMMENT '是否新数据:1是 0否' AFTER is_jfcd;
UPDATE t_insurance_detail SET is_new_data = CASE is_history_advance_pay
WHEN '1' THEN '0' WHEN '0' THEN '1' ELSE NULL END;
UPDATE t_insurance_detail SET is_new_data = '0'
WHERE is_jfcd = '0' AND (is_new_data IS NULL OR is_new_data = '');
ALTER TABLE t_insurance_detail DROP COLUMN is_history_advance_pay;
```
**尚未加列**(直接按 compat 新脚本写 `is_new_data`,存量 `is_jfcd=0``0`)。
### D6: 常量重命名
```java
String IS_NEW_DATA_YES = "1"; // 新提前缴费
String IS_NEW_DATA_NO = "0"; // 历史见费出单
// EKP 可复用相同字面量或保留 EKP_FD_NEW_DATA_YES/NO 指向同值
```
## Risks / Trade-offs
| 风险 | 缓解 |
|---|---|
| 前端仍读 `isHistoryAdvancePay` | proposal 标注 BREAKING;tasks 含 yifu-ui 同步项 |
| 双 change 并行导致字段名混乱 | 本 change 在 compat 之后或同 PR 内一次性替换 |
| 迁移 SQL 值翻转错误 | REG 测试:存量历史明细 `is_new_data=0` 且行为冒烟 |
| 注释/文档残留旧名 | grep 全仓 `is_history_advance_pay` / `isHistoryAdvancePay` |
## Migration Plan
1. 执行 HRO DDL + 值翻转 UPDATE(或合并进 `hro-advance-pay-insurance-compat.sql` 修订版)
2. 发版 Java:Entity/Mapper/Helper/Service 全量替换
3. 验证:历史 `is_new_data=0` 冒烟;launchTime 后新派单 `is_new_data=1`
4. 同步 yifu-ui 字段名(若已对接)
5. 更新 V24 与 `test-plan.md` 测试数据模板
**回滚**: 保留迁移前备份;可逆 SQL 将 `is_new_data` 翻回 `is_history_advance_pay`(仅应急)。
## Open Questions
1. 是否在本 change 内合并修订 `hro-advance-pay-insurance-compat` 的 SQL 文件,还是单独 migration 脚本?(建议:修订 compat SQL + 本 change 提供 ALTER 增量脚本)
2. yifu-ui 是否已对接 `isHistoryAdvancePay`?(决定前端 BREAKING 范围)
## 关键改造锚点
| 位置 | 改造 |
|---|---|
| `InsurancesConstants` | `IS_NEW_DATA_YES/NO` |
| `TInsuranceDetail` + VO/Param | `isNewData` |
| `TInsuranceDetailMapper.xml` | `IS_NEW_DATA` |
| `AdvancePaymentHelper` | 分流 + `resolveIsNewDataForNewRecord` |
| `DoJointInsuranceTask.initValues` | `setFd_new_data(param.getIsNewData())` |
| `TInsuranceDetailServiceImpl` | 注释与 `setIsNewData` 调用 |
| `EkpInteractiveParam` | `isNewData`(若推送 param 带该字段) |
| `V24/HRO-提前缴费商险数据兼容开发方案.md` | HRO 字段表、DDL、示例 |
## Why
`hro-advance-pay-insurance-compat` 已在 EKP 侧采用 `fd_new_data`(是否新数据:1是/0否,默认1),HRO 仍使用 `is_history_advance_pay`(1历史/0新),语义相反,需 `toEkpFdNewData` 反向转换,易混淆且与产品文档不一致。将 HRO 字段统一为 **是否新数据**,与 EKP 同义同值,消除映射歧义。
## What Changes
- **BREAKING(HRO 库表/API 字段)**`t_insurance_detail.is_history_advance_pay` 重命名为 `is_new_data``CHAR(1)`**1=新 / 0=历史**,默认 `'1'`
- Java 全链路:`isHistoryAdvancePay``isNewData`(Entity / VO / Param / Mapper / Service 注释)
- 常量:`IS_HISTORY_ADVANCE_PAY_YES/NO``IS_NEW_DATA_YES/NO``1`/`0`,与 EKP `EKP_FD_NEW_DATA_*` 同值)
- `AdvancePaymentHelper`:分流条件取反(历史=`0` 或空视同历史;新=`1`);`resolveIsNewDataForNewRecord` 在 launchTime 后写 `1`;EKP 推送改为 **直传** `isNewData``fd_new_data`(可删除反向转换)
- 存量迁移:`is_history_advance_pay=1``is_new_data=0``=0``1``is_jfcd=0` 且空 → `0`
- 同步更新 `V24/` 开发方案与 OpenSpec 测试计划中的 HRO 字段描述
## Capabilities
### New Capabilities
(无)
### Modified Capabilities
- `insurances`: 提前缴费数据标识字段由「是否历史提前缴费」改为「是否新数据」,HRO 与 EKP 字段语义对齐;分流/派单/实缴/退保**行为不变**,仅标识读写与映射简化
## Impact
- **模块**: `yifu-insurances``*-api``*-biz`、DDL、Mapper XML)
- **数据库**: HRO `mvp_insurances.t_insurance_detail` 列重命名 + 值翻转迁移(若已上线 `is_history_advance_pay` 则 ALTER + UPDATE + DROP 旧列)
- **API 响应**: 列表/详情 JSON 字段名由 `isHistoryAdvancePay` 变为 `isNewData`**BREAKING** 若前端已对接旧字段)
- **EKP**: 无新需求;推送逻辑简化,仍写 `fd_new_data`
- **前端**(yifu-ui): 若已读 `isHistoryAdvancePay`,需改为 `isNewData` 并反转展示逻辑(1=新,0=历史)
- **Non-goals**: 不改变历史/新提前缴费业务流程;不修改 `advance-pay.new-data-launch-time` 切点机制;不修改 EKP 表结构
- **前置**: 建议在 `hro-advance-pay-insurance-compat` 已合入或同批发布;若库中尚无 `is_history_advance_pay`,可直接 ADD `is_new_data`
## MODIFIED Requirements
### Requirement: Advance Payment Data Classification
The system SHALL distinguish legacy advance-pay (见费出单) data from new advance-pay data using `is_new_data` on `TInsuranceDetail` (`1`=new, `0`=legacy, default `1`), synchronized to EKP as `fd_new_data` with **identical values** (`1`=new, `0`=legacy).
#### Scenario: Legacy data identification
- **WHEN** `isJfcd=0` and (`isNewData` is `0` or NULL)
- **THEN** the record MUST follow legacy advance-pay workflow unchanged from pre-release behavior
- **AND** EKP pushes for estimate/actual/difference MUST set `fd_new_data=0`
#### Scenario: New data identification after launch time
- **WHEN** `isJfcd=0`, `isNewData=1`, and record created at or after `advance-pay.new-data-launch-time`
- **THEN** the record MUST follow new advance-pay workflow defined in advance-pay compat change
- **AND** EKP actual-premium pushes MUST set `fd_new_data=1`
#### Scenario: Stock data migration
- **WHEN** existing records have `is_jfcd=0` before release
- **THEN** migration MUST set `is_new_data=0` (legacy)
- **AND** behavior MUST remain identical to pre-migration legacy advance-pay
### Requirement: New Advance-Pay Dispatch Workflow
For new advance-pay data (`isNewData=1`), the system SHALL skip pending-payment state and EKP estimate push on dispatch.
#### Scenario: Dispatch directly to pending insurance
- **WHEN** new advance-pay insurance is created via add or batch add
- **THEN** `buyHandleStatus` MUST remain pending insurance (1), not pending payment (6)
- **AND** `processStatus` for pre-tasks MUST be pending insurance (3), not pending payment (7)
#### Scenario: No estimate on dispatch
- **WHEN** new advance-pay dispatch completes
- **THEN** the system MUST NOT create estimate `TInsuranceSettle`, MUST NOT set `defaultSettleId`, and MUST NOT call `pushEstime`
- **AND** the system MUST NOT call `createInsuranceInfo` at dispatch time
#### Scenario: BPO project exclusion unchanged
- **WHEN** project has `bpoEnable=是`
- **THEN** advance-pay (`isJfcd=0`) MUST NOT be triggered, same as existing `initJfcd` rules
### Requirement: Insurance Purchase Processing
The system SHALL manage commercial insurance purchase from application through handling success, including add, batch add, replace, and rollback. For advance-pay (`isJfcd=0`) records, behavior MUST branch on `isNewData` between legacy and new workflows.
#### Scenario: Create insurance application
- GIVEN an employee eligible for commercial insurance under project rules
- WHEN a purchase is submitted via `/insuranceDetail` add or batch-add endpoints
- THEN TInsuranceDetail records are created with pending handling status
- AND operation history is recorded in TInsuranceOperate
- AND advance-pay records MUST set `isNewData` per launch-time rules (`1` after launch, `0` before launch for legacy path)
#### Scenario: Complete insurance handling
- GIVEN a pending insurance handling record
- WHEN handling succeeds via import or manual confirmation
- THEN the record moves to insured status with premium and policy linkage
- AND pending payment amounts are updated on the purchase list view for legacy advance-pay only
#### Scenario: Insurance rollback
- GIVEN a purchase in handling state or insured state with advance-pay flag
- WHEN rollback is requested
- THEN legacy advance-pay MUST follow existing rollback (negative estimate + delete EKP actual/difference)
- AND new advance-pay MUST follow new rollback rules without negative estimate or difference
### Requirement: EKP Integration and Scheduled Tasks
The insurances module SHALL integrate with EKP for settlement push/retry and run scheduled jobs for expiry refresh. EKP push MUST include `fd_new_data` for all advance-pay estimate and actual payloads, equal to HRO `isNewData` when `fd_jfcd=是`.
#### Scenario: EKP resend
- GIVEN a failed or pending EKP push
- WHEN TInsuranceEkpController triggers resend
- THEN the insurance settlement event is re-submitted to EKP
- AND push outcome is traceable
#### Scenario: Daily expiry refresh
- GIVEN scheduled task invocation via `@Inner` endpoint `/inner/updateOverdueByTask`
- WHEN the job runs
- THEN overdue insurance records and renewal triggers are refreshed
- AND related pre-tasks or warnings are updated
#### Scenario: EKP field mapping for advance-pay
- GIVEN an advance-pay push to EKP with `fd_jfcd=是`
- WHEN `initValues` builds the push payload
- THEN `fd_new_data` MUST equal HRO `isNewData` without value inversion
- AND new advance-pay actual push MUST set `有无预估=否` while legacy MUST retain existing estimate flag rules
## 1. 数据库与迁移
- [x] 1.1 编写 HRO 迁移 SQL:`is_history_advance_pay``is_new_data`(值翻转:`1``0``0``1``is_jfcd=0` 存量刷 `0`);或修订 `hro-advance-pay-insurance-compat.sql` 直接使用 `is_new_data`
- [x] 1.2 确认 EKP `fd_new_data` DDL 无需变更(与 HRO 同义同值)
## 2. API 层(yifu-insurances-api)
- [x] 2.1 `InsurancesConstants``IS_HISTORY_ADVANCE_PAY_*` 重命名为 `IS_NEW_DATA_YES/NO``1`/`0`);EKP 常量与 HRO 同值或保留别名
- [x] 2.2 `TInsuranceDetail``InsuranceDetailVO``InsuranceListVO``InsuranceAddParam``InsuranceBatchParam``EkpInteractiveParam``isHistoryAdvancePay``isNewData`
- [x] 2.3 更新字段 `@Schema` 描述为「是否新数据:1是 0否」
## 3. Mapper 与持久化
- [x] 3.1 `TInsuranceDetailMapper.xml`:列 `IS_HISTORY_ADVANCE_PAY``IS_NEW_DATA`,result/insert/update 全量替换
- [x] 3.2 全局 grep 确认无残留 `is_history_advance_pay` / `IS_HISTORY_ADVANCE_PAY`
## 4. 业务逻辑(yifu-insurances-biz)
- [x] 4.1 `AdvancePaymentHelper`:分流条件改为 `isNewData``resolveIsHistoryAdvancePayForNewRecord``resolveIsNewDataForNewRecord`(launch 后写 `1`);删除或简化 `toEkpFdNewData`/`fromEkpFdNewData` 为直传
- [x] 4.2 `TInsuranceDetailServiceImpl``initJfcd`/`initJfcdBatch` 及注释块:`setIsNewData` / `getIsNewData`;注释「isHistoryAdvancePay=0」→「isNewData=1」
- [x] 4.3 `DoJointInsuranceTask.initValues``setFd_new_data(param.getIsNewData())` 直传
- [x] 4.4 其他引用点(`pushEstime` 跳过、`updateInsuranceSettleStatus``pullPaymentStatus` 等):Helper 方法名不变,确认分流仍正确
## 5. 文档与测试
- [x] 5.1 同步 `V24/HRO-提前缴费商险数据兼容开发方案.md`:HRO 字段表、DDL、迁移、代码示例改为 `is_new_data`
- [x] 5.2 同步 `V24/HRO-EKP提前缴费商险数据兼容需求.md` 附录映射表(HRO/EKP 同值,无需反向)
- [x] 5.3 更新 `hro-advance-pay-insurance-compat/test-plan.md` 或本 change 测试数据:`is_new_data=0` 历史 / `1` 新;UT-03 改为直传断言
- [x] 5.4 标注 yifu-ui:列表/详情 JSON 字段 `isHistoryAdvancePay``isNewData`(若已对接)→ 见 `ui-sync.md`
## 6. 验证
- [x] 6.1 存量迁移后 SQL 断言:`is_jfcd=0``is_new_data=0`,行为与改名前历史分支一致
- [x] 6.2 launchTime 后新派单:`is_new_data=1`,EKP 推送 `fd_new_data=1`,无反向转换
- [x] 6.3 全仓 grep:`isHistoryAdvancePay``is_history_advance_pay``IS_HISTORY_ADVANCE_PAY` 零残留(compat change 文档除外时可注明 deprecated)
# yifu-ui 同步说明(独立仓库)
**BREAKING**:商险列表/详情 API 响应字段变更。
| 旧字段 | 新字段 | 取值含义 |
| --- | --- | --- |
| `isHistoryAdvancePay` | `isNewData` | `1`=新提前缴费,`0`=历史见费出单 |
展示文案建议统一为「是否新数据」,与 HRO/EKP 一致。
schema: spec-driven
# Project context shown to AI when creating OpenSpec artifacts.
context: |
Project: yifu — Spring Cloud 微服务 HR/人事档案与薪酬社保平台
Tech stack: Java 8, Spring Boot 2.6.7, Spring Cloud 2021, Spring Cloud Alibaba,
OAuth2 RBAC, MyBatis Plus 3.5, Hutool, XXL-Job, Seata, Swagger
Architecture: Maven 多模块 monorepo;各业务域按 *-api / *-biz 拆分
Package: com.yifu.cloud.plus.v1.*
Modules: yifu-auth, yifu-upms, yifu-archives, yifu-salary, yifu-social,
yifu-order, yifu-check, yifu-job, yifu-msg, yifu-business, yifu-visual 等
Deployment: Docker / Kubernetes (k8s-bash/)
Conventions:
- Controller → Service → Mapper 分层;VO/Entity 在 *-api 模块
- 配置分环境:application.yml + application-{dev,test,prd}.yml
- 数据库映射在 *-biz/src/main/resources/mapper/*.xml
- 中文注释与业务术语(员工档案、社保、薪资、商险等)
Spec layout: openspec/specs/<domain>/spec.md 按业务域组织
Existing specs: platform, archives, salary, social, order, upms, check, insurances, csp
rules:
proposal:
- 明确影响的服务模块(如 yifu-archives-biz)
- 包含 Non-goals 与兼容性说明
design:
- 说明 API 变更、数据库变更、跨服务调用
- 标注需同步的前端模块(yifu-ui,独立仓库)
tasks:
- 按 api → biz → mapper → 配置 顺序拆分
- 单任务粒度控制在 2 小时以内
# Archives
## Purpose
yifu-archives(端口 5001)负责员工档案全生命周期管理,包括入职建档、花名册、合同、电子签、证明开具及与外部系统(瓜子、法大大)的集成。
## Requirements
### Requirement: Employee Master Data
The system SHALL maintain employee master records including roster (花名册), basic profile, family, education, work history, professional qualifications, and disability information.
#### Scenario: Query employee roster
- GIVEN an authorized user with data-scope permissions
- WHEN the user queries the personnel roster with filters
- THEN matching employee records are returned with pagination
- AND results respect MenuUtil data-auth SQL constraints
#### Scenario: Maintain employee sub-profile
- GIVEN an existing employee record
- WHEN the user updates family, education, or work history
- THEN the change is persisted via the corresponding Service and Mapper
- AND operation is logged via `@SysLog` where applicable
### Requirement: Pre-Onboarding Workflow
The system SHALL support pre-employment (预入职) and pending archive creation (入职待建档) workflows before an employee becomes fully archived.
#### Scenario: Pre-employment registration
- GIVEN a pending hire candidate
- WHEN pre-employment main data is submitted
- THEN a pre-employment record is created under `TPreEmpMain`
- AND status transitions are tracked in pre-employment logs
### Requirement: Employee Contract Lifecycle
The system SHALL manage employee contracts through apply, audit, and filing (归档) stages with area-based permission configuration.
#### Scenario: Contract application
- GIVEN an authorized user
- WHEN a contract application is submitted
- THEN the contract appears in the apply page with pending audit status
- AND only data within the user's permission scope is visible
#### Scenario: Contract filing
- GIVEN a contract that passed audit
- WHEN the user queries the filing page
- THEN only approved and archivable contracts are listed
- AND filing status is updated upon completion
#### Scenario: Contract renewal alert
- GIVEN a contract approaching expiry
- WHEN the renewal alert job or query runs
- THEN pending renewal tasks appear in contract renewal todo management
### Requirement: Electronic Signature Integration
The system SHALL integrate with Fadada (法大大) for electronic contract signing, including company accounts, templates, seals, and contract attachments.
#### Scenario: Initiate e-sign contract
- GIVEN a configured Fadada company and contract template
- WHEN a signing request is initiated via Fdd/Fasc controllers
- THEN the contract is sent to Fadada for signing
- AND request/response details are recorded in FddReqLog
### Requirement: Certificate and Proof Issuance
The system SHALL support issuance of employment and salary proofs with audit records.
#### Scenario: Issue salary proof
- GIVEN an employee with valid salary and archive data
- WHEN a salary proof is requested via SalaryProveController
- THEN a proof record is created in TCertRecord
- AND the proof can be exported or delivered per business rules
### Requirement: Archive Completeness Monitoring
The system SHALL monitor archive completeness against configurable rules and alert on missing required fields.
#### Scenario: Completeness check
- GIVEN completeness monitor configuration for a domain
- WHEN an employee archive is evaluated
- THEN completeness score and missing items are calculated
- AND incomplete archives appear in completeness monitoring views
### Requirement: Auto Rule Configuration
The system SHALL allow configuration of archive management rules and social-insurance purchase rules (TAutoEmpRuleInfo, TAutoFundRuleInfo, TAutoInsurRuleInfo) with change logs.
#### Scenario: Rule update
- GIVEN an existing auto rule
- WHEN an administrator modifies the rule
- THEN the new rule takes effect for subsequent automated processing
- AND the change is recorded in TAutoRuleLog
### Requirement: Customer and Settlement Domain
The system SHALL manage customer information, settlement domains, and B-side user-customer relationships for multi-tenant data isolation.
#### Scenario: Settlement domain lookup
- GIVEN a user with settlement-domain permissions
- WHEN querying employees or contracts by domain
- THEN only records within authorized settlement domains are returned
### Requirement: Cross-Service Integration
The archives module SHALL expose APIs consumed by salary, social, and other services via Dapr/HTTP, and sync data with the 瓜子 (GZ) external system.
#### Scenario: Internal service call
- GIVEN a request from yifu-salary or yifu-social with `@Inner` or service token
- WHEN archive data (e.g. settlement domain, employee info) is requested
- THEN the archives service returns the required VO without UI-level auth checks where `@Inner` applies
#### Scenario: GZ system push
- GIVEN a pending GZ integration task
- WHEN push entry is processed via TGzPushEntry
- THEN employee archive data is synchronized per task configuration
- AND task follow-up status is updated
# Check
## Purpose
yifu-check(端口 5022)提供身份证、手机号、银行卡等基础信息校验能力,供档案、社保、薪酬等模块在录入时调用。
## Requirements
### Requirement: Identity Card Validation
The system SHALL validate Chinese resident ID card numbers for format and checksum correctness.
#### Scenario: Valid ID card
- GIVEN a well-formed 18-digit ID card number
- WHEN validated via TCheckIdCardController
- THEN the service returns success with parsed metadata (region, birth date, gender) where supported
- AND invalid checksum or format returns a structured error via R
#### Scenario: Invalid ID card
- GIVEN a malformed or checksum-failing ID number
- WHEN validation is requested
- THEN the service rejects the input without persuring invalid data to downstream modules
### Requirement: Mobile Number Validation
The system SHALL validate mobile phone numbers for format and carrier rules configured in the system.
#### Scenario: Mobile format check
- GIVEN a mobile number string
- WHEN validated via TCheckMobileController
- THEN format compliance is verified against domestic mobile patterns
- AND validation result is returned synchronously for form-level feedback
### Requirement: Bank Account Validation
The system SHALL validate bank card numbers (bank account) for format and BIN rules.
#### Scenario: Bank card check
- GIVEN a bank card number
- WHEN validated via TCheckBankNoController
- THEN Luhn or configured BIN rules are applied
- AND invalid cards are rejected before salary bank binding proceeds
### Requirement: Validation Service Contract
Check endpoints SHALL return responses wrapped in `R<T>` consistent with platform API conventions and be callable from other services.
#### Scenario: Cross-service validation call
- GIVEN an internal service invoking check APIs during employee onboarding
- WHEN ID, mobile, or bank validation is requested
- THEN a uniform R response allows the caller to block or warn on failure
- AND validation is stateless with no side effects on check module data
# CSP (Customer Service Platform)
## Purpose
yifu-csp(端口 5088)是客户服务平台(客户端)模块,负责入离职登记、作业自动化待办、操作日志及与档案、商险、EKP 的协同处理。
## Requirements
### Requirement: Employee Onboarding and Offboarding Registration
The system SHALL manage employee registration forms (入离职登记表) for hire and separation events submitted from the client platform and HR workflows.
#### Scenario: Client-side registration list
- GIVEN a CSP client user
- WHEN querying registration list via `/employeeregistration/page`
- THEN only records with dataSource marked as client-origin are returned
- AND results are paginated per search criteria
#### Scenario: Create registration
- GIVEN valid hire or separation information from the client
- WHEN a new EmployeeRegistration is submitted
- THEN the record is persisted with client data source
- AND SysLog captures the creation operation
#### Scenario: HR todo registration list
- GIVEN an internal HR user with data permissions
- WHEN querying `/employeeregistration/hrPage`
- THEN pending HR todo registrations are returned with MenuUtil auth SQL applied
- AND export of todo tasks is supported
### Requirement: Registration Confirmation Workflow
The system SHALL support confirmation, rejection, and batch processing of onboarding registrations.
#### Scenario: Confirm hire reception
- GIVEN a pending onboarding registration
- WHEN confirm reception is executed
- THEN registration status advances to received/processed state
- AND downstream archive or insurance pre-tasks may be triggered
#### Scenario: Reject onboarding
- GIVEN an invalid or incomplete onboarding application
- WHEN reject onboarding is invoked
- THEN the registration is marked rejected with reason
- AND the client can view updated status on subsequent queries
#### Scenario: Pending confirmation queue
- GIVEN registrations awaiting HR confirmation
- WHEN querying onboarding pending-confirmation lists
- THEN only unconfirmed records appear
- AND batch confirm reception is supported
### Requirement: Automated HR Registration Processing
The system SHALL provide dedicated views and exports for automated onboarding/offboarding job flows.
#### Scenario: Automated registration list
- GIVEN an operator in the automation workflow
- WHEN querying `/employeeregistration/hrRegistrationPage`
- THEN automation-scoped registrations are listed with auth filtering
- AND dedicated export is available for automation batches
#### Scenario: Fetch and mark processed
- GIVEN external automation polling received applications
- WHEN get-received endpoint is called with `@Inner` or authorized access
- THEN confirmed registrations are returned and marked as processed atomically
- AND duplicate processing is prevented
### Requirement: Automated Leave Todo Management
The system SHALL manage automated offboarding todos (自动化离职待办) with batch confirmation and count queries.
#### Scenario: Leave todo pagination
- GIVEN pending leave automation tasks
- WHEN querying `/employeeregistrationleave/getEmployeeRegistrationLeaveTablePage`
- THEN EmployeeRegistrationLeaveTableVo results respect leave-specific auth via setLeaveBaseAuth
- AND pending count is available via getRegistrationLeaveCount
#### Scenario: Batch confirm leave
- GIVEN multiple pending leave todos
- WHEN batch sure leave (BatchSureLeaveVo) is submitted
- THEN selected leave tasks are confirmed in batch
- AND EmployeeRegistrationLeaveLog records operation details
### Requirement: Operation Logging
The system SHALL audit CSP operations for onboarding, offboarding, and general service actions.
#### Scenario: General operation log
- GIVEN a CSP user action on registration or related entities
- WHEN the action completes
- THEN TOperationLog captures operator, action type, and target entity
- AND logs are queryable for compliance review
#### Scenario: Leave automation operation log
- GIVEN automated leave workflow steps
- WHEN leave automation actions occur
- THEN EmployeeRegistrationLeaveLog records step-level history separately from general operation logs
### Requirement: File Upload
The CSP module SHALL provide file upload endpoints for registration attachments and supporting documents.
#### Scenario: Upload registration attachment
- GIVEN a registration requiring supporting documents
- WHEN FileUploadController receives a valid file
- THEN the file is stored and linked to the registration workflow
- AND upload failures return structured ErrorMessage via R
### Requirement: Cross-Module Integration
The CSP module SHALL integrate with yifu-archives for project/dept assignment and pre-archive data, with yifu-insurances for insurance pre-tasks, and with EKP for contract/dept information.
#### Scenario: Update customer service assignment
- GIVEN a registration requiring frontend CS user update
- WHEN customer user info is updated on registration
- THEN EmployeeRegistrationCustomerUserUpdateVo changes propagate to linked business records
- AND archives pre-employment entities remain consistent where shared IDs exist
#### Scenario: Project belong dept lookup
- GIVEN a registration linked to project organization
- WHEN querying TEmployeeProjectBelongDept via CSP endpoints
- THEN project-department mapping from archives is returned for CSP UI selection
- AND EkpDeptContractInfoVo supports EKP contract/dept alignment
#### Scenario: Trigger insurance pre-task
- GIVEN a confirmed hire registration meeting insurance rules
- WHEN CSP/archive events propagate to insurances
- THEN TEmployeeInsurancePre may be created in yifu-insurances
- AND CSP registration status reflects insurance todo linkage where configured
### Requirement: Authorization and Data Scope
CSP endpoints SHALL enforce RBAC and data-scope rules consistent with the platform, with separate visibility for client users versus internal HR operators.
#### Scenario: Client versus HR visibility
- GIVEN a client-platform user
- WHEN accessing client list endpoints
- THEN only client-origin registrations for authorized customers are visible
- AND internal HR endpoints require appropriate menu permissions and MenuUtil SQL scope
#### Scenario: Protected delete
- GIVEN a registration eligible for deletion
- WHEN delete is attempted without `csp_employeeregistration_del` permission
- THEN Spring Security denies the operation
- AND no partial side effects occur on related entities
# Commercial Insurance
## Purpose
yifu-insurances(端口 5005)负责商业保险(商险)的全流程管理,包括投保办理、减员退费、保单维护、险种配置、待办续签及与 EKP、档案、CSP 模块的协同。
## Requirements
### Requirement: Insurance Purchase Processing
The system SHALL manage commercial insurance purchase from application through handling success, including add, batch add, replace, and rollback.
#### Scenario: Create insurance application
- GIVEN an employee eligible for commercial insurance under project rules
- WHEN a purchase is submitted via `/insuranceDetail` add or batch-add endpoints
- THEN TInsuranceDetail records are created with pending handling status
- AND operation history is recorded in TInsuranceOperate
#### Scenario: Complete insurance handling
- GIVEN a pending insurance handling record
- WHEN handling succeeds via import or manual confirmation
- THEN the record moves to insured status with premium and policy linkage
- AND pending payment amounts are updated on the purchase list view
#### Scenario: Insurance rollback
- GIVEN a purchase in handling state
- WHEN rollback is requested
- THEN the application returns to the prior workflow state
- AND rollback reason and operator are auditable
### Requirement: Insured and Reduced Employee Management
The system SHALL maintain separate views and workflows for active insured employees and reduced (减员) employees.
#### Scenario: Query insured list
- GIVEN authorized users with area permissions
- WHEN querying insured employees via paginated or non-paginated list endpoints
- THEN only records within TInsuranceAreaRes data scope are returned
- AND settlement month and project changes are supported with change logs
#### Scenario: Process employee reduction
- GIVEN an active insured employee
- WHEN reduction handling is submitted
- THEN the employee moves to reduced status via reduction handling workflow
- AND reduction lists support export and batch import processing
### Requirement: Premium and Policy Registration
The system SHALL support registration of policy premiums, invoice numbers, and policy period maintenance.
#### Scenario: Register policy premium
- GIVEN a handled insurance record
- WHEN premium registration is submitted
- THEN premium amount and billing metadata are persisted on TInsuranceDetail
- AND invoice number can be registered separately
#### Scenario: Maintain insurance policy
- GIVEN an insurance company and policy number
- WHEN TInsurancePolicy is configured via `/insurancePolicy`
- THEN policy start/end dates and coverage windows are available for purchase validation
- AND policy detail supports CRUD with permission checks
### Requirement: Insurance Product Configuration
The system SHALL configure insurance companies, types, purchase standards, and rate tables as master data.
#### Scenario: Configure insurance type
- GIVEN a commercial insurance product line
- WHEN TInsuranceType and TInsuranceTypeStandard are maintained
- THEN purchase eligibility and standard amounts apply to new applications
- AND TInsuranceTypeRate defines premium rate rules per type
#### Scenario: Area permission for insurance
- GIVEN city-level operators
- WHEN TInsuranceAreaRes assigns area permissions
- THEN insurance queries and operations are scoped to authorized cities
### Requirement: Insurance Pre-Tasks and Renewal
The system SHALL generate and track pre-purchase tasks and renewal todos linked to onboarding and contract events.
#### Scenario: Insurance pre-task from onboarding
- GIVEN an employee registration or archive pre-task triggering insurance purchase
- WHEN TEmployeeInsurancePre is created
- THEN the task appears in pending purchase queues filtered by process status
- AND pending purchase counts are available via getListCount
#### Scenario: Renewal todo management
- GIVEN an insurance policy approaching expiry
- WHEN renewal pre-detail is generated
- THEN TInsurancePreRenewDetail appears in renewal todo management
- AND WeCom notification details are tracked in related QwDetail tables
### Requirement: Refund on Reduction
The system SHALL manage refund calculation and handling when insured employees are reduced.
#### Scenario: Query refund list
- GIVEN reduced employees eligible for premium refund
- WHEN querying `/insuranceRefund` paginated list
- THEN RefundListVo results show refund amounts and handling status
- AND export requires `insurance_custserve_reduction_export` permission where configured
### Requirement: Claims and Warnings
The system SHALL record insurance claims (出险) and surface expiry or anomaly warnings.
#### Scenario: Record claim event
- GIVEN an active insured employee with a claim incident
- WHEN claim is registered via insurance detail endpoints
- THEN claim metadata is attached to the insurance record
- AND subsequent premium or renewal rules reflect claim status
#### Scenario: Insurance warning
- GIVEN policies or tasks meeting warning rules
- WHEN TInsurancesWarn queries run
- THEN operators receive actionable warning lists for follow-up
### Requirement: Attachments and Change Audit
The system SHALL support insurance file uploads and maintain change logs for insurance data modifications.
#### Scenario: Upload insurance enclosure
- GIVEN a handling or insured record
- WHEN files are uploaded via TInsuranceEnclosureController
- THEN attachments are linked to the insurance business entity
- AND files are retrievable for audit and EKP submission
#### Scenario: Track field changes
- GIVEN modifications to insured information or project assignment
- WHEN data is updated
- THEN TInsurancesChangeLog records before/after values
- AND project change logs can be exported
### Requirement: EKP Integration and Scheduled Tasks
The insurances module SHALL integrate with EKP for settlement push/retry and run scheduled jobs for expiry refresh.
#### Scenario: EKP resend
- GIVEN a failed or pending EKP push
- WHEN TInsuranceEkpController triggers resend
- THEN the insurance settlement event is re-submitted to EKP
- AND push outcome is traceable
#### Scenario: Daily expiry refresh
- GIVEN scheduled task invocation via `@Inner` endpoint `/inner/updateOverdueByTask`
- WHEN the job runs
- THEN overdue insurance records and renewal triggers are refreshed
- AND related pre-tasks or warnings are updated
### Requirement: Cross-Module Integration
The insurances module SHALL consume employee and onboarding context from yifu-archives and yifu-csp, and expose data to salary risk views where applicable.
#### Scenario: Onboarding-driven purchase task
- GIVEN CSP employee registration confirmed for hire
- WHEN insurance purchase rules match
- THEN TEmployeeInsurancePre is created or updated from archive/CSP events
- AND incomplete onboarding blocks purchase where business rules require it
#### Scenario: Internal service access
- GIVEN `@Inner` calls from job or peer services
- WHEN insurance inner endpoints are invoked with service credentials
- THEN batch updates proceed without UI session auth
- AND responses use platform R wrapper conventions
# Order
## Purpose
yifu-order(端口 5006)负责项目订单的全生命周期管理,以及与 EKP 系统的订单同步。
## Requirements
### Requirement: Project Order Management
The system SHALL manage project orders with list query, detail view, reply tracking, and file attachments.
#### Scenario: Query order list
- GIVEN an authorized user
- WHEN querying orders via `/order/getOrderPageList`
- THEN paginated OrderListVO results are returned filtered by query parameters
- AND non-paginated list is available via `/order/getOrderList`
#### Scenario: View order detail
- GIVEN an existing order ID
- WHEN detail is requested
- THEN OrderDetailVO includes order metadata, status, and related entities
- AND reply history is available via OrderReplyListVO
### Requirement: Order File Management
The system SHALL support order enclosure upload and file sync logging.
#### Scenario: Upload order attachment
- GIVEN an open order
- WHEN a file is uploaded via order endpoints
- THEN the enclosure is stored and linked to the order
- AND TOrderFileSyncLog records sync operations with external systems
### Requirement: EKP Order Integration
The system SHALL synchronize order data with the EKP system via EkpOrderController.
#### Scenario: EKP order sync
- GIVEN an order event requiring EKP notification
- WHEN EkpOrderController processes the sync request
- THEN order state is pushed or pulled per EKP integration rules
- AND sync failures are logged for manual retry
### Requirement: Order Authorization
Order endpoints SHALL enforce RBAC via `@PreAuthorize` consistent with the platform permission model.
#### Scenario: Unauthorized order access
- GIVEN a user without order menu permissions
- WHEN accessing protected order endpoints
- THEN access is denied by Spring Security
- AND no order data is leaked in the response
# Platform
## Purpose
yifu 是基于 Spring Cloud 的人事档案、薪酬、社保、商险等业务管理平台,采用微服务架构与 OAuth2 RBAC 权限体系。
## Requirements
### Requirement: Microservice Module Structure
Each business domain SHALL be organized as a Maven module pair: `*-api` (shared DTOs, entities, Feign interfaces) and `*-biz` (Spring Boot application, controllers, services, mappers).
#### Scenario: New business capability
- GIVEN a new business domain
- WHEN the capability is implemented
- THEN it follows the `yifu-<domain>-api` / `yifu-<domain>-biz` pattern
- AND the biz module exposes REST APIs consumed by yifu-ui or other services
### Requirement: Authentication and Authorization
The system MUST use OAuth2-based RBAC. Protected endpoints SHALL require valid tokens; permissions are managed via yifu-upms.
#### Scenario: Protected API access
- GIVEN an authenticated user with assigned roles
- WHEN the user calls a secured endpoint
- THEN access is granted or denied based on RBAC rules
### Requirement: Data Access Layer
Persistence SHALL use MyBatis Plus with XML mappers under `*-biz/src/main/resources/mapper/`. Services MUST NOT bypass the Mapper layer for database access.
#### Scenario: CRUD operation
- GIVEN a business entity
- WHEN data is read or written
- THEN the operation flows through Service → Mapper → database
### Requirement: Environment Configuration
Each biz module SHALL support environment-specific configuration via `application-{profile}.yml` (dev, test, prd).
#### Scenario: Deploy to production
- GIVEN a service deployed with profile `prd`
- WHEN the application starts
- THEN production datasource and external service URLs from `application-prd.yml` are used
### Requirement: Core Business Domains
The platform SHALL provide distinct microservice modules for auth, user/permission management, employee archives, salary, social insurance, orders, validation, scheduled jobs, and visual tooling.
#### Scenario: Domain module lookup
- GIVEN a business capability such as employee archives
- WHEN a developer locates the implementation
- THEN the code resides in the corresponding `yifu-<domain>-api` and `yifu-<domain>-biz` modules
- AND the module responsibility matches the platform domain table in README
# Salary
## Purpose
yifu-salary(端口 5003)负责薪酬核算、工资报账、薪资配置、审批流程、个税申报统计,以及与档案、社保模块的数据联动。
## Requirements
### Requirement: Salary Standard Management
The system SHALL manage standard salary sheets (标准薪酬) including import, apply, audit, and project association.
#### Scenario: Import standard salary
- GIVEN an uploaded salary file matching configured template
- WHEN the file is imported via SalaryUploadController or TSalaryStandardOriginalController
- THEN salary rows are parsed and stored in TSalaryStandard
- AND import errors are reported to the user
#### Scenario: Standard salary apply workflow
- GIVEN a draft standard salary batch
- WHEN submitted for approval via `/tsalarystandard/pageApply`
- THEN the batch enters the apply workflow with data-scope filtering via MenuUtil
- AND approved records become available for payroll processing
### Requirement: Payroll Account and Payslip
The system SHALL manage payroll accounts (工资报账主表) and line-item details (工资明细) as the core payslip entity.
#### Scenario: Create payroll account
- GIVEN approved standard salary data for a settlement period
- WHEN a payroll account is generated
- THEN TSalaryAccount header and TSalaryAccountItem lines are created
- AND totals reconcile with standard salary source data
#### Scenario: Employee payslip query
- GIVEN a C-side employee with bound mobile
- WHEN querying via TSalaryAccountPhoneController
- THEN the employee sees only their own payslip records
- AND sensitive fields follow access control rules
### Requirement: Salary Configuration
The system SHALL support configurable salary parameters including minimum wage, income tax, bank accounts, payslip column mapping, and project query permissions.
#### Scenario: Minimum wage configuration
- GIVEN regional minimum wage policy
- WHEN an administrator updates TMinSalary or TConfigSalary
- THEN subsequent salary validation uses the new thresholds
- AND SysMessageSalary triggers reminders when violations are detected
#### Scenario: Payslip column configuration
- GIVEN a settlement project
- WHEN payslip display columns are configured via TSalaryStandardSet
- THEN TSalaryStandardSetDetail defines visible fields for that project's payslips
### Requirement: Salary Approval Workflow
The system SHALL route salary batches through multi-node approval with audit records and copy notifications.
#### Scenario: Submit for approval
- GIVEN a completed payroll or standard salary batch
- WHEN submitted to the approval flow
- THEN TAuditRecord and TAuditNodeProocess track node-level progress
- AND TAuditCopyInfo notifies configured copy recipients
#### Scenario: Approval decision
- GIVEN a pending approval node
- WHEN an approver approves or rejects
- THEN the batch status updates accordingly
- AND subsequent nodes or rollback actions follow configured workflow
### Requirement: Salary Employee Registry
The system SHALL maintain a salary-specific employee registry (TSalaryEmployee) synchronized with archive changes.
#### Scenario: Archive change propagation
- GIVEN an employee profile change in yifu-archives
- WHEN the change affects salary-relevant fields
- THEN TSalaryEmpModLog records the modification
- AND salary employee data reflects the update
### Requirement: Tax and Statistical Reporting
The system SHALL support tax declaration statistics including current-period reports, labor remuneration, bonuses, and market return data.
#### Scenario: Current period declaration
- GIVEN a reporting period and declarer configuration
- WHEN generating TStatisticsCurrentReport
- THEN wage, bonus, and labor fee declarations aggregate from approved payroll data
- AND TStatisticsDeclarer links records to declaration targets
### Requirement: Insurance Unpurchase Application
The system SHALL manage applications for projects that do not purchase social insurance due to risk assessment.
#### Scenario: Submit unpurchase apply
- GIVEN a high-risk project requiring insurance waiver
- WHEN TInsuranceUnpurchaseApply is submitted with supporting detail
- THEN the application enters approval workflow
- AND approved waivers are tracked for compliance monitoring
### Requirement: Salary Pause and Zero Declaration
The system SHALL support pausing salary issuance (暂停发放) and zero-declaration (0申报) management for special cases.
#### Scenario: Pause salary
- GIVEN an employee or batch flagged for pause
- WHEN TPauseSalary record is active
- THEN payroll generation excludes affected employees
- AND pause reason and period are auditable
### Requirement: Cross-Module Integration
The salary module SHALL fetch settlement domain and employee context from yifu-archives via Dapr (HttpDaprUtil, DaprArchivesProperties) and expose B-side salary APIs.
#### Scenario: Settlement domain resolution
- GIVEN a salary query scoped by settlement domain
- WHEN domain list is needed
- THEN salary service calls archives APIs to resolve TSettleDomainListVo
- AND results filter payroll data by authorized domains
#### Scenario: B-side salary API
- GIVEN a B-side authenticated client
- WHEN calling BusinessSalaryController endpoints
- THEN salary data is returned per B-side contract and permission model
# Social Insurance
## Purpose
yifu-social(端口 5002)负责社保、公积金的派单购买、缴费管理、基数配置、实缴核验及与 EKP、档案模块的协同。
## Requirements
### Requirement: Social Insurance Dispatch
The system SHALL manage dispatch (派单) workflows for social insurance and provident fund purchase, from pre-dispatch through completion.
#### Scenario: Create dispatch order
- GIVEN an employee eligible for social insurance purchase
- WHEN a dispatch record is created via TDispatchInfoController
- THEN TDispatchInfo captures purchase type, domain, and employee linkage
- AND concurrent dispatch for the same employee is prevented via distributed lock where configured
#### Scenario: Pre-dispatch queue
- GIVEN employees pending insurance purchase
- WHEN listed via TDispatchInfoPre or TPreDispatchInfo
- THEN only employees meeting purchase rules appear
- AND dispatch can be batch-processed per city/date configuration (TDispatchDayConf)
### Requirement: Social Insurance Details
The system SHALL maintain social insurance line-item records (TSocialInfo) with import, export, and change history.
#### Scenario: Query social details
- GIVEN an authorized user with house-hold permissions
- WHEN querying TSocialInfo with filters
- THEN matching social detail records are returned
- AND TSocialLog captures field-level change history
#### Scenario: Import social data
- GIVEN a valid import file
- WHEN uploaded to social detail management
- THEN records are bulk-inserted or updated with validation errors reported per row
### Requirement: Provident Fund Management
The system SHALL manage provident fund (公积金) information separately from social insurance with shared house-hold configuration.
#### Scenario: Provident fund record maintenance
- GIVEN an employee with active provident fund account
- WHEN TProvidentFund data is updated
- THEN fund base, ratio, and status reflect current policy
- AND changes are auditable via social change logs
### Requirement: House-Hold and Base Configuration
The system SHALL configure social insurance houses (社保户), provident fund houses, payment proportions, base amounts, and deadline dates.
#### Scenario: House-hold setup
- GIVEN a city and employer entity
- WHEN SysHouseHoldInfo is configured with SysPayProportion
- THEN dispatch and payment calculations use the configured ratios and bases
- AND THouseHoldLimit enforces cutoff dates per house
#### Scenario: Base amount setting
- GIVEN a policy adjustment period
- WHEN SysBaseSetInfo is updated
- THEN subsequent dispatch uses the new base for premium calculation
- AND TSocialFundHistory records the adjustment history
### Requirement: Payment Library
The system SHALL manage the payment library (缴费库) for consolidated premium records, import logs, and EKP push tracking.
#### Scenario: Payment record import
- GIVEN a payment file from insurer or tax platform
- WHEN imported via TPaymentInfoController
- THEN TPaymentInfo records are created or merged
- AND TPaymentInfoImportLog tracks import batch metadata
#### Scenario: EKP payment push
- GIVEN approved payment records
- WHEN pushed to EKP
- THEN TPaymentSocialPush tracks push IDs
- AND TSendEkpError records failures for retry
### Requirement: Audit Workflow
The system SHALL support audit for social insurance, provident fund, forecast settlements, approval forms, and base changes via TAuditInfo.
#### Scenario: Submit social audit
- GIVEN a dispatch or payment batch requiring approval
- WHEN submitted to audit
- THEN TAuditInfo records audit type, status, and reviewer
- AND approved batches proceed to payment or filing steps
### Requirement: Automated Payment Verification
The system SHALL verify actual payments (实缴) against expected amounts via automated jobs and soldier (社保士兵) integration.
#### Scenario: Auto payment verification
- GIVEN expected premium from dispatch data
- WHEN TAutoPaymentInfo job compares with actual payment
- THEN TAutoPaymentDetail records match/mismatch results
- AND TAutoPaymentError captures discrepancies for operator review
### Requirement: Forecast and Cost Estimation
The system SHALL provide forecast library (预估费用) for premium estimation before actual dispatch.
#### Scenario: Generate forecast
- GIVEN employee roster and base configuration
- WHEN TForecastLibrary calculation runs
- THEN estimated premiums are computed per policy rules
- AND results support settlement preview before dispatch
### Requirement: Alert and Failure Notification
The system SHALL notify operators of dispatch failures, deadline risks, and processing errors.
#### Scenario: Dispatch failure alert
- GIVEN a failed social insurance transaction
- WHEN the failure is recorded
- THEN TSocialAlertInfo notifies responsible users
- AND FailReasonConfig provides standardized failure reason codes
### Requirement: Cross-Module Integration
The social module SHALL integrate with yifu-archives for employee and settlement domain data, and support B-side APIs and tax-friend (税友) document generation.
#### Scenario: Archive data dependency
- GIVEN a dispatch creation request
- WHEN employee or domain validation is required
- THEN social service resolves data from archives via Dapr/HTTP
- AND invalid or incomplete archive blocks dispatch with ErrorMessage
#### Scenario: B-side social API
- GIVEN a B-side client
- WHEN calling DispatchBusinessController
- THEN social status and dispatch operations are exposed per B-side permission model
# UPMS
## Purpose
yifu-upms(端口 4000)是通用用户权限管理模块,提供用户、角色、菜单、部门、字典、数据权限及 OAuth 客户端配置,为全平台 RBAC 提供基础能力。
## Requirements
### Requirement: User Management
The system SHALL manage system users including CRUD, password policies, and department assignment.
#### Scenario: Create user
- GIVEN an administrator with user-management permission
- WHEN a new user is created via UserController
- THEN the user record is persisted with assigned department and roles
- AND the user can authenticate via yifu-auth after activation
#### Scenario: B-side business user
- GIVEN a B-side tenant context
- WHEN managing users via BusinessUserController
- THEN B-side users are scoped to their business relationship
- AND permissions differ from internal admin users
### Requirement: Role and Menu Authorization
The system SHALL bind roles to menus and API permissions for RBAC enforcement across all microservices.
#### Scenario: Assign role menus
- GIVEN a role definition
- WHEN menus are assigned via RoleController and MenuController
- THEN RoleMenuR mappings determine visible menus and `@PreAuthorize` expressions
- AND changes take effect on next token issuance or permission cache refresh
### Requirement: Department Organization
The system SHALL maintain organizational hierarchy via DeptController for data-scope and reporting structure.
#### Scenario: Department tree query
- GIVEN an authenticated user
- WHEN querying the department tree
- THEN hierarchical dept nodes are returned
- AND user-dept permission (SysUserDeptPermission) further restricts visible branches
### Requirement: Dictionary Management
The system SHALL provide system-wide dictionaries (DictController) and external relation dictionaries for cross-system code mapping.
#### Scenario: Dict lookup
- GIVEN a dict type code
- WHEN frontend or services request dict items
- THEN cached dict values are returned from Redis where configured
- AND SysOutRelDict supports external system code translation
### Requirement: Data Authorization
The system SHALL support row-level data authorization via DataAuthController and custom SQL data auth rules.
#### Scenario: Data scope filtering
- GIVEN a user with configured data auth rules
- WHEN a business service calls MenuUtil.setAuthSql
- THEN generated SQL fragments restrict query results to authorized data scope
- AND SysDataAuthDiySql supports custom per-menu SQL templates
### Requirement: OAuth Client Configuration
The system SHALL manage OAuth2 client details (OauthClientDetailsController) consumed by yifu-auth.
#### Scenario: Register OAuth client
- GIVEN a new microservice or frontend application
- WHEN an OAuth client is registered in UPMS
- THEN yifu-auth issues tokens for the client_id with configured scopes and grant types
- AND client secrets are stored securely
### Requirement: System Parameters and Areas
The system SHALL manage public parameters (PublicParamController) and geographic areas (AreaController) as shared reference data.
#### Scenario: Public param lookup
- GIVEN a param key used by business modules
- WHEN PublicParamController is queried
- THEN the current param value is returned
- AND changes propagate without service redeployment where hot-reload is supported
-- HRO 提前缴费商险数据兼容 — DDL + 存量迁移
-- 执行顺序:§1 → §2 → §3 → §4 → 应用发版
-- §1 HRO — t_insurance_detail 新增字段
-- 库:mvp_insurances
-- 若已执行旧版 DDL(is_history_advance_pay),见文件末尾 §1-upgrade 增量脚本。
ALTER TABLE t_insurance_detail
ADD COLUMN is_new_data CHAR(1) NULL DEFAULT '1'
COMMENT '是否新数据:1是(新提前缴费) 0否(历史见费出单),仅 is_jfcd=0 时有效'
AFTER is_jfcd;
-- §2 EKP — ekp_insurances_info 新增字段
-- 库:ekp
-- 若已执行旧版 DDL(fd_is_history_advance_pay),可先加列再转换后删旧列,例如:
-- ALTER TABLE ekp_insurances_info ADD COLUMN fd_new_data CHAR(1) NULL DEFAULT '1' AFTER fd_jfcd;
-- UPDATE ekp_insurances_info SET fd_new_data = CASE fd_is_history_advance_pay WHEN '是' THEN '0' WHEN '否' THEN '1' ELSE '1' END WHERE fd_jfcd = '是';
-- ALTER TABLE ekp_insurances_info DROP COLUMN fd_is_history_advance_pay;
-- 再跳过下方 ADD COLUMN。
ALTER TABLE ekp_insurances_info
ADD COLUMN fd_new_data CHAR(1) NULL DEFAULT '1'
COMMENT '是否新数据:1是 0否,默认1;与 HRO is_new_data 同值,fd_jfcd=是 时写入'
AFTER fd_jfcd;
-- §3 HRO — 存量见费出单刷为「否」(非新数据=历史)
UPDATE t_insurance_detail
SET is_new_data = '0'
WHERE is_jfcd = '0'
AND (is_new_data IS NULL OR is_new_data = '' OR is_new_data = '1');
-- §4 EKP — 既有提前缴费明细刷为「否」(非新数据=历史)
UPDATE ekp_insurances_info
SET fd_new_data = '0'
WHERE fd_jfcd = '是'
AND (fd_new_data IS NULL OR fd_new_data = '' OR fd_new_data = '1');
-- §1-upgrade 从 is_history_advance_pay 升级(仅已执行旧版 §1 的环境)
-- ALTER TABLE t_insurance_detail ADD COLUMN is_new_data CHAR(1) NULL DEFAULT '1'
-- COMMENT '是否新数据:1是 0否' AFTER is_jfcd;
-- UPDATE t_insurance_detail SET is_new_data = CASE is_history_advance_pay
-- WHEN '1' THEN '0' WHEN '0' THEN '1' ELSE NULL END;
-- UPDATE t_insurance_detail SET is_new_data = '0'
-- WHERE is_jfcd = '0' AND (is_new_data IS NULL OR is_new_data = '');
-- ALTER TABLE t_insurance_detail DROP COLUMN is_history_advance_pay;
......@@ -1382,4 +1382,12 @@ public class InsurancesConstants {
public static final String REFUND_ZH_SETTLE = "费用支出中,禁止重复登记";
public static final String REFUND_TYPE = "退费";
public static final String INSURANCE_REPLACE_TAG_REPLACE = "可替换";
/** 是否新数据:是(新提前缴费) */
public static final String IS_NEW_DATA_YES = "1";
/** 是否新数据:否(历史见费出单) */
public static final String IS_NEW_DATA_NO = "0";
/** EKP fd_new_data,与 HRO is_new_data 同值 */
public static final String EKP_FD_NEW_DATA_YES = IS_NEW_DATA_YES;
public static final String EKP_FD_NEW_DATA_NO = IS_NEW_DATA_NO;
}
......@@ -187,5 +187,7 @@ public class EkpInsurancesInfo {
@Schema(description = "是否见费出单")
private String fd_jfcd;
@Schema(description = "是否新数据:1是 0否,默认1;0对应HRO历史提前缴费")
private String fd_new_data;
}
......@@ -426,6 +426,12 @@ public class TInsuranceDetail extends BaseEntity {
@Schema(description = "是否见费出单")
private String isJfcd;
/**
* 是否新数据:1是(新提前缴费) 0否(历史见费出单),仅 isJfcd=0 时有效
*/
@Schema(description = "是否新数据:1是 0否")
private String isNewData;
/**
* 购买周期(格式:4 个月/117 天)
*/
......
......@@ -203,6 +203,9 @@ public class EkpInteractiveParam implements Serializable {
@Schema(description = "是否见费出单")
private String isJfcd;
@Schema(description = "是否新数据:1是 0否")
private String isNewData;
/**
* 差额settleId
*/
......
......@@ -256,6 +256,9 @@ public class InsuranceAddParam implements Serializable {
@Schema(description = "是否见费出单")
private String isJfcd;
@Schema(description = "是否新数据:1是 0否")
private String isNewData;
/**
* 购买周期 (格式:4个月/117天)
*/
......
......@@ -259,6 +259,9 @@ public class InsuranceBatchParam implements Serializable {
@Schema(description = "是否见费出单")
private String isJfcd;
@Schema(description = "是否新数据:1是 0否")
private String isNewData;
/**
* 购买周期 (格式:4个月/117天)
*/
......
......@@ -432,6 +432,9 @@ public class InsuranceDetailVO implements Serializable {
@Schema(description = "是否见费出单")
private String isJfcd;
@Schema(description = "是否新数据:1是 0否")
private String isNewData;
/**
* 购买周期 (格式:4个月/117天)
*/
......
......@@ -197,6 +197,9 @@ public class InsuranceListVO implements Serializable {
@Schema(description = "是否见费出单")
private String isJfcd;
@Schema(description = "是否新数据:1是 0否")
private String isNewData;
/**
* 购买周期 (格式:4个月/117天)
*/
......
......@@ -1090,10 +1090,13 @@ public class TInsuranceDetailController {
if (detail == null) {
return R.failed(CommonConstants.NO_DATA_TO_HANDLE);
}
EkpSettleStatusVo vo = tInsuranceDetailService.getSettleStatusFromEkpInsuranceDetail(detail.getId(),detail.getDefaultSettleId());
if (null != vo){
EkpSettleStatusVo vo = tInsuranceDetailService.getSettleStatusFromEkpInsuranceDetail(detail.getId(), detail.getDefaultSettleId());
if (null != vo) {
return R.ok(vo);
}
if (Common.isEmpty(detail.getDefaultSettleId())) {
return R.ok(new EkpSettleStatusVo());
}
return R.failed();
}
/**
......
......@@ -185,6 +185,9 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
@Autowired
private EkpInsurancesInfoMapper ekpInsurancesInfoMapper;
@Resource
private AdvancePaymentHelper advancePaymentHelper;
/***********************商险办理********************************/
/**
......@@ -474,10 +477,13 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
detailList.add(detail);
sourceIdCardList.add(detail.getEmpIdcardNo());
baseMapper.insert(detail);
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())){
if (advancePaymentHelper.isLegacyJfcdData(detail)){
pushList.add(detail);
updateEmployeeInsurancePre(success.getInsurancePreId(),detail,CommonConstants.SEVEN_STRING);
updateEmployeeInsurancePreRenew(success.getInsurancePreRenewId(),detail,CommonConstants.SEVEN_STRING);
} else if (advancePaymentHelper.isNewJfcdData(detail)) {
updateEmployeeInsurancePre(success.getInsurancePreId(),detail,CommonConstants.THREE_STRING);
updateEmployeeInsurancePreRenew(success.getInsurancePreRenewId(),detail,CommonConstants.THREE_STRING);
}else {
updateEmployeeInsurancePre(success.getInsurancePreId(),detail,CommonConstants.THREE_STRING);
updateEmployeeInsurancePreRenew(success.getInsurancePreRenewId(),detail,CommonConstants.THREE_STRING);
......@@ -611,27 +617,159 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
//购买周期
detail.setPurchaseCycle(purchaseCycle+"个月/"+day+"天");
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())){
//预估缴费状态 0待缴费、1已缴费
detail.setPaymentStatus(CommonConstants.ZERO_STRING);
detail.setBuyHandleStatus(CommonConstants.SIX_INT);
//保费存储
TInsuranceSettle settle = new TInsuranceSettle();
settle.setInsDetailId(detail.getId());
settle.setSettleType(detail.getSettleType());
settle.setSettleHandleStatus(CommonConstants.ONE_STRING);
settle.setCreateTime(LocalDateTime.now());
settle.setEstimatePushTime(LocalDateTime.now());
settle.setEstimatePremium(detail.getEstimatePremium());
//调完ekp接口才会是1
settle.setIsEstimatePush(CommonConstants.ZERO_INT);
settle.setId(RandomStringUtils.randomNumeric(19));
tInsuranceSettleService.save(settle);
detail.setDefaultSettleId(settle.getId());
if (advancePaymentHelper.isNewJfcdData(detail)) {
initNewAdvancePayInfo(detail);
} else {
initLegacyJfcdInfo(detail, settleList);
}
}else {
detail.setEstimatePremium(BigDecimal.ZERO);
}
}
/**
* 历史见费出单(isNewData=0 或空):待缴费 + 预估 settle,逻辑与上线前一致。
*/
private void initLegacyJfcdInfo(TInsuranceDetail detail, List<TInsuranceSettle> settleList) {
if (Common.isEmpty(detail.getIsNewData())) {
detail.setIsNewData(InsurancesConstants.IS_NEW_DATA_NO);
}
//预估缴费状态 0待缴费、1已缴费
detail.setPaymentStatus(CommonConstants.ZERO_STRING);
detail.setBuyHandleStatus(CommonConstants.SIX_INT);
//保费存储
TInsuranceSettle settle = new TInsuranceSettle();
settle.setInsDetailId(detail.getId());
settle.setSettleType(detail.getSettleType());
settle.setSettleHandleStatus(CommonConstants.ONE_STRING);
settle.setCreateTime(LocalDateTime.now());
settle.setEstimatePushTime(LocalDateTime.now());
settle.setEstimatePremium(detail.getEstimatePremium());
//调完ekp接口才会是1
settle.setIsEstimatePush(CommonConstants.ZERO_INT);
settle.setId(RandomStringUtils.randomNumeric(19));
tInsuranceSettleService.save(settle);
detail.setDefaultSettleId(settle.getId());
}
/**
* 新提前缴费(isNewData=1):直接待投保,不创建预估 settle,仅保留展示用预估保费。
*/
private void initNewAdvancePayInfo(TInsuranceDetail detail) {
detail.setBuyHandleStatus(CommonConstants.ONE_INT);
detail.setIsNewData(InsurancesConstants.IS_NEW_DATA_YES);
}
/**
* 新提前缴费退保:无负数预估/差额;未结算删实缴,已结算未支出推负数实缴。
*/
private void rollBackNewAdvancePayInsurance(TInsuranceDetail detail, YifuUser user) {
if (StringUtils.isBlank(detail.getDefaultSettleId())) {
syncRollbackIncome(detail);
return;
}
TInsuranceSettle settle = tInsuranceSettleService.getById(detail.getDefaultSettleId());
if (!Optional.ofNullable(settle).isPresent()) {
syncRollbackIncome(detail);
return;
}
SettleVo settleVo = getInsuranceDetailSettleStatus(detail.getId(), detail.getDefaultSettleId());
if (Common.isEmpty(settleVo)) {
syncRollbackIncome(detail);
return;
}
String actualStatus = settleVo.getActualStatus();
Integer isActualPush = settle.getIsActualPush();
if (isActualPush == null || CommonConstants.ZERO_INT == isActualPush
|| InsurancesConstants.SETTLE_ZERO.equals(actualStatus)) {
ekpSettleService.deleteEkpInsuranceDetailAsso(detail.getId());
detail.setActualPremium(new BigDecimal("0.00"));
} else if (InsurancesConstants.SETTLE_ONE.equals(actualStatus)
|| InsurancesConstants.SETTLE_TWO.equals(actualStatus)) {
TInsuranceSettle burden = new TInsuranceSettle();
BeanCopyUtils.copyProperties(settle, burden);
burden.setId(null);
burden.setCreateTime(LocalDateTime.now());
burden.setUpdateTime(LocalDateTime.now());
if (burden.getActualPremium() != null) {
burden.setActualPremium(burden.getActualPremium().negate());
burden.setActualPushTime(LocalDateTime.now());
}
tInsuranceSettleService.save(burden);
detail.setDefaultSettleId(burden.getId());
detail.setActualPremium(new BigDecimal("0.00"));
String pushResult = pushEstimate(detail, CommonConstants.SIX_INT);
if (StringUtils.isNotBlank(pushResult)) {
detail.setDefaultSettleId(null);
}
}
syncRollbackIncome(detail);
}
private void syncRollbackIncome(TInsuranceDetail detail) {
if (detail.getSignFlag() != CommonConstants.ONE_INT) {
return;
}
try {
TIncomeDetail incomeDetail = new TIncomeDetail();
incomeDetail.setSourceId(detail.getId());
R<TIncomeDetailReturnVo> incomeDetailList = socialDaprUtils.getTIncomeDetailList(incomeDetail);
if (Common.isNotNull(incomeDetailList) && incomeDetailList.getCode() == CommonConstants.SUCCESS) {
for (TIncomeDetail detailIncome : incomeDetailList.getData().getDetailList()) {
TIncomeDetail detail1 = new TIncomeDetail();
BeanCopyUtils.copyProperties(detailIncome, detail1);
detail1.setId(CommonConstants.NULL);
detail1.setRedData(CommonConstants.ONE_STRING);
detail1.setMoney(detail1.getMoney().negate());
socialDaprUtils.createTIncomeDetail(detail1);
}
}
} catch (Exception e) {
log.error("收入数据同步处理失败:" + e);
}
}
/**
* 新提前缴费登记实缴:无差额;实缴登记时 createInsuranceInfo + 推 EKP 实缴。
*/
private void registerNewAdvancePayActualPremium(TInsuranceDetail detail, BigDecimal bigDecimalAct,
Map<String, TSettleDomainSelectVo> mapSelectVo) {
if (StringUtils.isBlank(detail.getDefaultSettleId())) {
createInsuranceInfo(detail, mapSelectVo.get(detail.getDeptNo()));
}
TInsuranceSettle settle;
if (StringUtils.isBlank(detail.getDefaultSettleId())) {
TInsuranceSettle settleNew = new TInsuranceSettle();
settleNew.setInsDetailId(detail.getId());
settleNew.setSettleType(detail.getSettleType());
settleNew.setSettleHandleStatus(CommonConstants.ONE_STRING);
settleNew.setActualPremium(bigDecimalAct);
settleNew.setIsActualPush(CommonConstants.ZERO_INT);
settleNew.setCreateTime(LocalDateTime.now());
tInsuranceSettleService.save(settleNew);
detail.setDefaultSettleId(settleNew.getId());
detail.setActualPremium(bigDecimalAct);
doJointInsuranceTask.asynchronousEkpInfo(detail, settleNew, CommonConstants.TWO_INT);
} else {
settle = tInsuranceSettleService.getById(detail.getDefaultSettleId());
if (Optional.ofNullable(settle).isPresent()) {
if (CommonConstants.ZERO_INT == settle.getIsActualPush()) {
settle.setActualPremium(bigDecimalAct);
settle.setIsActualPush(CommonConstants.ZERO_INT);
settle.setUpdateTime(LocalDateTime.now());
detail.setActualPremium(bigDecimalAct);
doJointInsuranceTask.asynchronousEkpInfo(detail, settle, CommonConstants.TWO_INT);
} else if (detail.getActualPremium() == null || detail.getActualPremium().compareTo(bigDecimalAct) != 0) {
settle.setActualPremium(bigDecimalAct);
settle.setIsActualPush(CommonConstants.ZERO_INT);
settle.setUpdateTime(LocalDateTime.now());
detail.setActualPremium(bigDecimalAct);
doJointInsuranceTask.asynchronousEkpInfo(detail, settle, CommonConstants.FOUR_INT);
}
}
}
}
/**
* 商险批增
*
......@@ -727,7 +865,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
//是否见费出单逻辑处理:预估保费结算、购买周期计算、保单结束日期结算、参保开始日期计算、预计办理日期计算
initJfcdInfo(preDispatchDate, detail,settleList);
baseMapper.insert(detail);
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())){
if (advancePaymentHelper.isLegacyJfcdData(detail)){
//生成收入数据
createInsuranceInfo(detail,mapSelectVo.get(detail.getDeptNo()));
updateEmployeeInsurancePre(success.getInsurancePreId(),detail,CommonConstants.SEVEN_STRING);
......@@ -736,6 +874,9 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
List<TInsuranceDetail> successList = new ArrayList<>();
successList.add(detail);
doJointInsuranceTask.pushEstime(successList);
} else if (advancePaymentHelper.isNewJfcdData(detail)) {
updateEmployeeInsurancePre(success.getInsurancePreId(),detail,CommonConstants.THREE_STRING);
updateEmployeeInsurancePreRenew(success.getInsurancePreRenewId(),detail,CommonConstants.THREE_STRING);
}else {
updateEmployeeInsurancePre(success.getInsurancePreId(),detail,CommonConstants.THREE_STRING);
updateEmployeeInsurancePreRenew(success.getInsurancePreRenewId(),detail,CommonConstants.THREE_STRING);
......@@ -1114,6 +1255,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
* @param id
* @return {@link R<String>}
* @author licancan
* 提前缴费(isJfcd=0)不可删,含历史(isNewData=0)与新(isNewData=1)提前缴费。
*/
@Override
public R<String> deleteInsurance(String id) {
......@@ -1965,16 +2107,32 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
listVO.setErrorMessage(InsurancesConstants.REDUCE_ROLLBACK_REDUCE_IS_NOT_ALLOW);
errorList.add(listVO);
}else{
//判断是否已支出或支出中
EkpSettleStatusVo vo = ekpSettleService.getSettleStatusFromEkpInsuranceDetail(detail.getId());
if (vo != null && (vo.getPayStatus().equals("已结算")
|| vo.getPayStatus().equals("结算中"))) {
InsuranceListVO listVO = new InsuranceListVO();
BeanCopyUtils.copyProperties(detail,listVO);
listVO.setProjectName(detail.getDeptName());
listVO.setErrorMessage(InsurancesConstants.REDUCE_ROLLBACK_PAY_IS_NOT_ALLOW);
errorList.add(listVO);
continue;
if (advancePaymentHelper.isNewJfcdData(detail)) {
if (StringUtils.isNotBlank(detail.getDefaultSettleId())) {
EkpSettleStatusVo payVo = getSettleStatusFromEkpInsuranceDetail(detail.getId(), detail.getDefaultSettleId());
if (payVo != null && Common.isNotNull(payVo.getPayStatus())
&& (InsurancesConstants.SETTLE_ONE.equals(payVo.getPayStatus())
|| InsurancesConstants.SETTLE_TWO.equals(payVo.getPayStatus()))) {
InsuranceListVO listVO = new InsuranceListVO();
BeanCopyUtils.copyProperties(detail,listVO);
listVO.setProjectName(detail.getDeptName());
listVO.setErrorMessage(InsurancesConstants.REDUCE_ROLLBACK_PAY_IS_NOT_ALLOW);
errorList.add(listVO);
continue;
}
}
} else {
//判断是否已支出或支出中(历史见费出单)
EkpSettleStatusVo vo = ekpSettleService.getSettleStatusFromEkpInsuranceDetail(detail.getId());
if (vo != null && (vo.getPayStatus().equals("已结算")
|| vo.getPayStatus().equals("结算中"))) {
InsuranceListVO listVO = new InsuranceListVO();
BeanCopyUtils.copyProperties(detail,listVO);
listVO.setProjectName(detail.getDeptName());
listVO.setErrorMessage(InsurancesConstants.REDUCE_ROLLBACK_PAY_IS_NOT_ALLOW);
errorList.add(listVO);
continue;
}
}
//判断是否是替换类型的退回,如果是,需要将被替换者激活,替换者有效状态为空
if(detail.getBuyType() == CommonConstants.FOUR_INT){
......@@ -2008,7 +2166,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
}
// 记录状态置为「退回」
detail.setBuyHandleStatus(CommonConstants.FOUR_INT);
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())){
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd()) && !advancePaymentHelper.isNewJfcdData(detail)){
detail.setPaymentTime(CommonConstants.EMPTY_STRING);
detail.setPaymentStatus(CommonConstants.ZERO_STRING);
//添加日志
......@@ -2060,6 +2218,17 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
// TInsuranceCompany insuranceCompany;
// TInsuranceType insuranceType;
for (TInsuranceDetail detail : successList) {
if (advancePaymentHelper.isNewJfcdData(detail)) {
rollBackNewAdvancePayInsurance(detail, user);
paramList.forEach(e ->{
if (detail.getId().equals(e.getId())){
operateList.add(e);
}
});
updateEmployeeInsurancePreById(detail,Stream.of("3","4","6").collect(Collectors.toList()),CommonConstants.FIVE_STRING);
updateEmployeeInsurancePreRenewById(detail,Stream.of("3","4","6").collect(Collectors.toList()),CommonConstants.FIVE_STRING, true);
continue;
}
// // 是否地市自购
// insuranceCompany = tInsuranceCompanyService.getOne(Wrappers.<TInsuranceCompany>query().lambda()
// .eq(TInsuranceCompany::getCompanyName, detail.getInsuranceCompanyName())
......@@ -2270,7 +2439,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
List<TInsuranceDetail> successList = new ArrayList<>();
List<TInsuranceDetail> successOne = new ArrayList<>();
List<TInsuranceDetail> successTwo = new ArrayList<>();
//见费出单已经生成预估数据了不用推送了
//见费出单已经生成预估数据了不用推送了(历史见费出单:派单已推预估;新提前缴费:无预估,实缴在登记保单保费处理)
List<TInsuranceDetail> successThree = new ArrayList<>();
List<TInsuranceDetail> successBanlance = new ArrayList<>();
//被替换人标记被替换成功
......@@ -2417,7 +2586,8 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
//生成收入数据
createInsuranceInfo(detail,settleDomain);
}
}else if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())){
}else if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())
&& !advancePaymentHelper.isNewJfcdData(detail)){
//记录状态均置为「已投保」
detail.setBuyHandleStatus(CommonConstants.THREE_INT);
detail.setSignFlag(CommonConstants.ONE_INT);
......@@ -2450,7 +2620,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
successOne.add(detail);
updateEmployeeInsurancePreById(detail, Stream.of("4").collect(Collectors.toList()),CommonConstants.SIX_STRING);
updateEmployeeInsurancePreRenewById(detail, Stream.of("4").collect(Collectors.toList()),CommonConstants.SIX_STRING, false);
//如果是见费出单,不重复生成预估,具体差额在登记保单保费处处理
//历史见费出单派单已推预估;新提前缴费无预估
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())){
successThree.add(detail);
}
......@@ -2758,8 +2928,9 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
//如果保费不为空
if (StringUtils.isNotBlank(success.getActualPremium())){
bigDecimalAct = new BigDecimal(success.getActualPremium());
//如果当前保单为合并结算
if ((detail.getSettleType() == CommonConstants.ZERO_INT || CommonConstants.ZERO_STRING.equals(detail.getIsJfcd()) ) &&
if (advancePaymentHelper.isNewJfcdData(detail)) {
registerNewAdvancePayActualPremium(detail, bigDecimalAct, mapSelectVo);
} else if ((detail.getSettleType() == CommonConstants.ZERO_INT || CommonConstants.ZERO_STRING.equals(detail.getIsJfcd()) ) &&
StringUtils.isNotBlank(detail.getDefaultSettleId())) {
settle = tInsuranceSettleService.getById(detail.getDefaultSettleId());
if (Optional.ofNullable(settle).isPresent()) {
......@@ -2820,7 +2991,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
}
}
//实缴导入时根据条件取生成差额数据,其他情况会在推送数据时去规整 对应的应收和应支
if (CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())) {
if (advancePaymentHelper.isLegacyJfcdData(detail)) {
//登记保单保费时为已投保,已投保无法退回,故直接删除差额重新生成即可
ekpSettleService.callBackBalance(detail.getId());
tInsuranceSettleService.remove(Wrappers.<TInsuranceSettle>query().lambda()
......@@ -3757,6 +3928,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
CommonConstants.ZERO_STRING.equals(insuranceType.getIsJfcd()) &&
CommonConstants.IS_FALSE.equals(settleDomainSelectVo.getBpoEnable())) {
param.setIsJfcd(CommonConstants.ZERO_STRING);
param.setIsNewData(advancePaymentHelper.resolveIsNewDataForNewRecord());
}
}
......@@ -3770,6 +3942,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
CommonConstants.ZERO_STRING.equals(insuranceType.getIsJfcd()) &&
CommonConstants.IS_FALSE.equals(settleDomainSelectVo.getBpoEnable())) {
param.setIsJfcd(CommonConstants.ZERO_STRING);
param.setIsNewData(advancePaymentHelper.resolveIsNewDataForNewRecord());
}
}
......@@ -4973,7 +5146,7 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
param.setErrorMessage(InsurancesConstants.BUY_HANDLE_FOUR_NOT_REGISTERED);
continue;
}
if (StringUtils.isNotBlank(detail.getDefaultSettleId())){
if (!advancePaymentHelper.isNewJfcdData(detail) && StringUtils.isNotBlank(detail.getDefaultSettleId())){
settle = tInsuranceSettleService.getById(detail.getDefaultSettleId());
if (Optional.ofNullable(settle).isPresent()){
settleStatus = getSettleStatusFromEkpInsuranceDetail(detail.getId(), detail.getDefaultSettleId());
......@@ -9108,7 +9281,10 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
*/
@Override
public EkpSettleStatusVo getSettleStatusFromEkpInsuranceDetail(String detailId, String settleId){
String id = detailId+CommonConstants.DOWN_LINE_STRING+settleId;
if (StringUtils.isBlank(settleId)) {
return ekpSettleService.getSettleStatusFromEkpInsuranceDetail(detailId);
}
String id = detailId + CommonConstants.DOWN_LINE_STRING + settleId;
//获取ekp结算信息
return ekpSettleService.getSettleStatusFromEkpInsuranceDetail(id);
}
......@@ -9330,6 +9506,16 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
if (Common.isEmpty(vo)) {
return R.failed(CommonConstants.PARAM_IS_NOT_ERROR);
}
if (Common.isNotNull(vo.getDetailIds())) {
List<String> detailIds = Arrays.asList(vo.getDetailIds().split(CommonConstants.COMMA_STRING));
boolean allNewAdvancePay = detailIds.stream().allMatch(id -> {
TInsuranceDetail detail = baseMapper.selectById(id);
return detail != null && advancePaymentHelper.isNewJfcdData(detail);
});
if (allNewAdvancePay) {
return R.ok();
}
}
try {
log.info("商险状态更新报文"+JSON.toJSONString(vo));
doJointInsuranceTask.updateInsuranceSettleStatus(vo);
......@@ -10335,6 +10521,10 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
* @Param
* @return
**/
/**
* 撤回 — 仅适用于待缴费(6)的历史见费出单。
* 新提前缴费(isNewData=1)派单后直接待投保,不经待缴费;前端已禁用撤回按钮,后端无需单独分支。
*/
@Override
public R callBack(TInsuranceDetail detail) {
String id = detail.getId()+CommonConstants.DOWN_LINE_STRING+detail.getDefaultSettleId();
......@@ -10392,11 +10582,17 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
List<TInsuranceDetail> list = null;
if (Common.isNotNull(idStr)){
list = baseMapper.selectBatchIds(Arrays.asList(idStr.split(CommonConstants.COMMA_STRING)));
if (Common.isNotNull(list)) {
list = list.stream().filter(advancePaymentHelper::isLegacyJfcdData).collect(Collectors.toList());
}
}else {
list = baseMapper.selectList(Wrappers.<TInsuranceDetail>query().lambda()
.eq(TInsuranceDetail::getBuyHandleStatus,CommonConstants.SIX_INT)
.eq(BaseEntity::getCreateBy, user.getId())
.eq(TInsuranceDetail::getIsJfcd,CommonConstants.ZERO_STRING));
if (Common.isNotNull(list)) {
list = list.stream().filter(advancePaymentHelper::isLegacyJfcdData).collect(Collectors.toList());
}
}
if (Common.isNotNull(list)){
......@@ -10488,11 +10684,16 @@ public class TInsuranceDetailServiceImpl extends ServiceImpl<TInsuranceDetailMap
//获取所有的明细id
if (Common.isNotNull(id)) {
detail = baseMapper.selectById(id);
if (!Common.isNotNull(detail)) {
continue;
}
if (advancePaymentHelper.isNewJfcdData(detail)) {
continue;
}
detailOld = new TInsuranceDetail();
BeanCopyUtils.copyProperties(detail,detailOld);
//见费出单且是待投保状态 直接回退到 待缴费 并清空缴费时间和缴费状态
if (Common.isNotNull(detail)
&& CommonConstants.ONE_INT == detail.getBuyHandleStatus().intValue()
if (CommonConstants.ONE_INT == detail.getBuyHandleStatus().intValue()
&& CommonConstants.ZERO_STRING.equals(detail.getIsJfcd())) {
detail.setPaymentTime(CommonConstants.EMPTY_STRING);
detail.setPaymentStatus(CommonConstants.ZERO_STRING);
......
package com.yifu.cloud.plus.v1.yifu.insurances.util;
import com.yifu.cloud.plus.v1.yifu.common.core.constant.CommonConstants;
import com.yifu.cloud.plus.v1.yifu.common.core.util.Common;
import com.yifu.cloud.plus.v1.yifu.insurances.constants.InsurancesConstants;
import com.yifu.cloud.plus.v1.yifu.insurances.entity.TInsuranceDetail;
import com.yifu.cloud.plus.v1.yifu.insurances.vo.EkpInteractiveParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 见费出单(提前缴费)历史/新数据分流工具。
*/
@Component
public class AdvancePaymentHelper {
private static final DateTimeFormatter LAUNCH_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Value("${advance-pay.new-data-launch-time:2099-12-31 23:59:59}")
private String newDataLaunchTime;
public boolean isJfcd(TInsuranceDetail detail) {
return detail != null && CommonConstants.ZERO_STRING.equals(detail.getIsJfcd());
}
public boolean isJfcdParam(String isJfcd) {
return CommonConstants.ZERO_STRING.equals(isJfcd);
}
/** 历史见费出单:isJfcd=0 且 isNewData 为 0 或空 */
public boolean isLegacyJfcdData(TInsuranceDetail detail) {
if (!isJfcd(detail)) {
return false;
}
return isLegacyNewDataFlag(detail.getIsNewData());
}
public boolean isLegacyJfcdParam(EkpInteractiveParam param) {
if (param == null || !isJfcdParam(param.getIsJfcd())) {
return false;
}
return isLegacyNewDataFlag(param.getIsNewData());
}
/** 新提前缴费:isJfcd=0 且 isNewData=1 */
public boolean isNewJfcdData(TInsuranceDetail detail) {
return isJfcd(detail) && InsurancesConstants.IS_NEW_DATA_YES.equals(detail.getIsNewData());
}
public boolean isNewJfcdParam(EkpInteractiveParam param) {
return param != null && isJfcdParam(param.getIsJfcd())
&& InsurancesConstants.IS_NEW_DATA_YES.equals(param.getIsNewData());
}
/**
* 满足见费出单条件时,按发布时间决定是否新数据。
*/
public String resolveIsNewDataForNewRecord() {
try {
LocalDateTime launch = LocalDateTime.parse(newDataLaunchTime.trim(), LAUNCH_TIME_FORMAT);
if (!LocalDateTime.now().isBefore(launch)) {
return InsurancesConstants.IS_NEW_DATA_YES;
}
} catch (Exception ignored) {
// 配置解析失败时保守走历史逻辑
}
return InsurancesConstants.IS_NEW_DATA_NO;
}
/** HRO is_new_data 与 EKP fd_new_data 同值直传 */
public String toEkpFdNewData(String isNewData) {
if (InsurancesConstants.IS_NEW_DATA_YES.equals(isNewData)) {
return InsurancesConstants.IS_NEW_DATA_YES;
}
return InsurancesConstants.IS_NEW_DATA_NO;
}
public String fromEkpFdNewData(String fdValue) {
if (InsurancesConstants.IS_NEW_DATA_YES.equals(fdValue)) {
return InsurancesConstants.IS_NEW_DATA_YES;
}
if (InsurancesConstants.IS_NEW_DATA_NO.equals(fdValue)) {
return InsurancesConstants.IS_NEW_DATA_NO;
}
return null;
}
private boolean isLegacyNewDataFlag(String isNewData) {
return Common.isEmpty(isNewData)
|| InsurancesConstants.IS_NEW_DATA_NO.equals(isNewData);
}
}
......@@ -89,6 +89,9 @@ public class DoJointInsuranceTask {
@Autowired
private TBusinessOperateService tBusinessOperateService;
@Resource
private AdvancePaymentHelper advancePaymentHelper;
/**
* @Description: 商险明细推送
* @Author: huyc
......@@ -248,6 +251,9 @@ public class DoJointInsuranceTask {
if (Common.isNotNull(viewVo.getId())) {
insuranceId = viewVo.getId().substring(0, 19);
detail = insuranceDetailMapper.selectById(insuranceId);
if (Common.isNotNull(detail) && advancePaymentHelper.isNewJfcdData(detail)) {
continue;
}
if (Common.isNotNull(detail)) {
TInsuranceDetail detailOld = new TInsuranceDetail();
BeanCopyUtils.copyProperties(detail,detailOld);
......@@ -587,6 +593,9 @@ public class DoJointInsuranceTask {
}
//根据结算类型是合并结算,推送预估保费到ekp(上面已经算好,这里直接判断有预估保费的就推送)
for (TInsuranceDetail tInsuranceDetail : successList) {
if (!advancePaymentHelper.isLegacyJfcdData(tInsuranceDetail)) {
continue;
}
//如果是合并计算或见费出单则推送至EKP
if (CommonConstants.ZERO_INT == tInsuranceDetail.getSettleType()
|| CommonConstants.ZERO_STRING.equals(tInsuranceDetail.getIsJfcd())) {
......@@ -712,10 +721,22 @@ public class DoJointInsuranceTask {
}
if (InsurancesConstants.ACTUAL_SETTLE_BILL.equals(param.getSettleType())){
ys = 0L;
if (updateFlag){
//应支=0
pushParam.setFd_3adfe6e3911ffe(null==param.getActualPremium()? 0.00:param.getActualPremium().doubleValue());
if (advancePaymentHelper.isNewJfcdParam(param)) {
double actualAmount = null == param.getActualPremium() ? 0.00 : param.getActualPremium().doubleValue();
pushParam.setFd_3af9d1441ef7b6("否");
if ("是".equals(deptInfoVo.getBpoSettle())) {
ys = 0L;
pushParam.setFd_3adfe6e3911ffe(actualAmount);
} else {
ys = actualAmount;
pushParam.setFd_3adfe6e3911ffe(actualAmount);
}
} else {
ys = 0L;
if (updateFlag){
//应支=0
pushParam.setFd_3adfe6e3911ffe(null==param.getActualPremium()? 0.00:param.getActualPremium().doubleValue());
}
}
}
//差额
......@@ -1106,8 +1127,14 @@ public class DoJointInsuranceTask {
//是否见费出单
if (CommonConstants.ZERO_STRING.equals(param.getIsJfcd())){
pushParam.setFd_jfcd("是");
//有无预估
pushParam.setFd_3af9d1441ef7b6("有");
pushParam.setFd_new_data(advancePaymentHelper.toEkpFdNewData(param.getIsNewData()));
if (advancePaymentHelper.isNewJfcdParam(param)
&& InsurancesConstants.ACTUAL_SETTLE_BILL.equals(param.getSettleType())) {
pushParam.setFd_3af9d1441ef7b6("否");
} else {
//有无预估
pushParam.setFd_3af9d1441ef7b6("有");
}
}else {
pushParam.setFd_jfcd("否");
}
......
......@@ -41,4 +41,7 @@ wx:
corpsecret: 16kqEL_eU-ARwYyqLgEBWHgxm8gXVnkzv_eJMLy9NpU
agentid: 1000010
authUrl: https://test-wx.worfu.com/yifu-auth/method/oauth/wxLogin
domainName: https://test-wx.worfu.com
\ No newline at end of file
domainName: https://test-wx.worfu.com
advance-pay:
new-data-launch-time: 2099-12-31 23:59:59
\ No newline at end of file
......@@ -41,4 +41,7 @@ wx:
corpsecret: R0nKkvsY-oF41fuQvUXZ-kFG3_g_Ce0bpZt6mByx524
agentid: 1000009
authUrl: https://wx.worfu.com/yifu-auth/method/oauth/wxLogin
domainName: https://wx.worfu.com
\ No newline at end of file
domainName: https://wx.worfu.com
advance-pay:
new-data-launch-time: 2099-12-31 23:59:59
\ No newline at end of file
......@@ -44,3 +44,5 @@ wx:
domainName: https://test-wx.worfu.com
advance-pay:
new-data-launch-time: 2099-12-31 23:59:59
......@@ -104,3 +104,7 @@ springdoc:
- group: insurances
#按包路径匹配
packagesToScan: com.yifu.cloud.plus.v1.yifu.insurances.controller
# 新提前缴费数据写入切点(满足见费出单且当前时间>=此时间时写 isNewData=1)
advance-pay:
new-data-launch-time: 2099-12-31 23:59:59
......@@ -57,6 +57,8 @@
<result property="fd_3c6f0108e05c84" column="fd_3c6f0108e05c84"/>
<result property="fd_usebpo_settle" column="fd_usebpo_settle"/>
<result property="createTime" column="create_time"/>
<result property="fd_jfcd" column="fd_jfcd"/>
<result property="fd_new_data" column="fd_new_data"/>
</resultMap>
<sql id="Base_Column_List" >
......@@ -112,6 +114,8 @@
,a.fd_3c6f0108e05c84
,a.fd_usebpo_settle
,a.create_time
,a.fd_jfcd
,a.fd_new_data
</sql>
<!-- ekpInsurancesInfo简单分页查询 -->
......
......@@ -75,6 +75,7 @@
<result property="paymentStatus" column="PAYMENT_STATUS" jdbcType="VARCHAR"/>
<result property="paymentTime" column="PAYMENT_TIME" jdbcType="VARCHAR"/>
<result property="isJfcd" column="IS_JFCD" jdbcType="VARCHAR"/>
<result property="isNewData" column="IS_NEW_DATA" jdbcType="VARCHAR"/>
<result property="purchaseCycle" column="PURCHASE_CYCLE" jdbcType="VARCHAR"/>
<result property="preHandleTime" column="PRE_HANDLE_TIME" jdbcType="VARCHAR"/>
......@@ -98,7 +99,7 @@
IS_USE,IS_EFFECT,IS_OVERDUE,REMARK,
CREATE_BY,CREATE_NAME,CREATE_TIME,BPO_FLAG,
UPDATE_BY,UPDATE_TIME,DELETE_FLAG,DEFAULT_SETTLE_ID,HANDLED_BY,HANDLED_TIME,ORDER_NO,CREATE_USER_DEPT_ID,CREATE_USER_DEPT_NAME,
DEPT_NAME,UNIT_ID,UNIT_NAME,UNIT_NO,IS_ADRESS,PAYMENT_STATUS,PAYMENT_TIME,IS_JFCD,PURCHASE_CYCLE,PRE_HANDLE_TIME
DEPT_NAME,UNIT_ID,UNIT_NAME,UNIT_NO,IS_ADRESS,PAYMENT_STATUS,PAYMENT_TIME,IS_JFCD,IS_NEW_DATA,PURCHASE_CYCLE,PRE_HANDLE_TIME
</sql>
......@@ -131,6 +132,7 @@
a.PAYMENT_STATUS as paymentStatus,
a.PAYMENT_TIME as paymentTime,
a.IS_JFCD as isJfcd,
a.IS_NEW_DATA as isNewData,
a.PURCHASE_CYCLE as purchaseCycle,
a.BILLING_TYPE as billingType,
a.PRE_HANDLE_TIME as preHandleTime,
......@@ -241,6 +243,7 @@
a.PAYMENT_STATUS as paymentStatus,
a.PAYMENT_TIME as paymentTime,
a.IS_JFCD as isJfcd,
a.IS_NEW_DATA as isNewData,
<!-- a.PURCHASE_CYCLE as purchaseCycle, -->
a.PURCHASE_CYCLE as purchaseCycle,
a.PRE_HANDLE_TIME as preHandleTime,
......@@ -436,7 +439,7 @@
a.REFUND_ID as refundId,
a.EXPIRE_REMARK,
a.EXPIRE_IGNORE_FLAG,
a.IS_ADRESS as isAdress,a.PAYMENT_STATUS,a.PAYMENT_TIME,a.IS_JFCD,
a.IS_ADRESS as isAdress,a.PAYMENT_STATUS,a.PAYMENT_TIME,a.IS_JFCD,a.IS_NEW_DATA as isNewData,
a.PURCHASE_CYCLE,a.PRE_HANDLE_TIME
from t_insurance_detail a
left join t_insurance_settle tis on a.DEFAULT_SETTLE_ID = tis.ID
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment