|
|
@@ -281,6 +281,19 @@ public class BizDataService {
|
|
|
private volatile long personnelListCacheExpireAt;
|
|
|
private static final long PERSONNEL_LIST_CACHE_TTL_MS = 900_000L;
|
|
|
private final AtomicBoolean personnelRefreshing = new AtomicBoolean(false);
|
|
|
+ /** 写后覆盖层:SWR 全量重拉若读到更旧的 DMS 行,不得盖回待审等旧状态 */
|
|
|
+ private final Map<String, PersonnelWriteOverlay> personnelWriteOverlay =
|
|
|
+ new HashMap<String, PersonnelWriteOverlay>();
|
|
|
+
|
|
|
+ private static final class PersonnelWriteOverlay {
|
|
|
+ final Map<String, Object> row;
|
|
|
+ final long expireAt;
|
|
|
+
|
|
|
+ PersonnelWriteOverlay(Map<String, Object> row, long expireAt) {
|
|
|
+ this.row = row;
|
|
|
+ this.expireAt = expireAt;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* 仅用于清空底数、恢复初始化等必须整表重建的场景。
|
|
|
@@ -320,26 +333,87 @@ public class BizDataService {
|
|
|
|
|
|
/** 把已回写 DMS 的字段合并进缓存中对应的人。 */
|
|
|
public void patchPersonnelCache(String id, Map<String, Object> fields) {
|
|
|
+ patchPersonnelCache(id, fields, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 写后必须留下 overlay。缓存未建或 id 对不上时用 fallbackRow upsert,禁止静默 no-op,
|
|
|
+ * 否则资格审批待审列表会在 SWR 后回档。
|
|
|
+ */
|
|
|
+ public void patchPersonnelCache(String id, Map<String, Object> fields, Map<String, Object> fallbackRow) {
|
|
|
if (!StringUtils.hasText(id) || fields == null || fields.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
synchronized (personnelListLock) {
|
|
|
- if (personnelListCache == null || personnelRowByKey == null) {
|
|
|
- return;
|
|
|
+ Map<String, Object> old = personnelRowByKey == null ? null : personnelRowByKey.get(id);
|
|
|
+ if (old == null && fallbackRow != null) {
|
|
|
+ old = fallbackRow;
|
|
|
}
|
|
|
- Map<String, Object> old = personnelRowByKey.get(id);
|
|
|
- if (old == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
- Map<String, Object> merged = new LinkedHashMap<String, Object>(old);
|
|
|
+ Map<String, Object> merged = old == null
|
|
|
+ ? new LinkedHashMap<String, Object>(fields)
|
|
|
+ : new LinkedHashMap<String, Object>(old);
|
|
|
merged.putAll(fields);
|
|
|
- if (!fields.containsKey("update_time")) {
|
|
|
+ if (!merged.containsKey("update_time")) {
|
|
|
merged.put("update_time", Long.valueOf(System.currentTimeMillis()));
|
|
|
}
|
|
|
- upsertPersonnelCacheLocked(merged);
|
|
|
+ rememberPersonnelOverlayLocked(id, merged);
|
|
|
+ if (personnelListCache != null && (old != null || fallbackRow != null)) {
|
|
|
+ upsertPersonnelCacheLocked(merged);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void rememberPersonnelOverlayLocked(String id, Map<String, Object> merged) {
|
|
|
+ long exp = System.currentTimeMillis() + PERSONNEL_LIST_CACHE_TTL_MS;
|
|
|
+ PersonnelWriteOverlay ov = new PersonnelWriteOverlay(new LinkedHashMap<String, Object>(merged), exp);
|
|
|
+ if (StringUtils.hasText(id)) {
|
|
|
+ personnelWriteOverlay.put(id, ov);
|
|
|
+ }
|
|
|
+ String dmsId = DmsRowUtils.str(merged, "id");
|
|
|
+ String biz = DmsRowUtils.personnelBizId(merged);
|
|
|
+ if (StringUtils.hasText(dmsId)) {
|
|
|
+ personnelWriteOverlay.put(dmsId, ov);
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(biz)) {
|
|
|
+ personnelWriteOverlay.put(biz, ov);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applyPersonnelWriteOverlayLocked() {
|
|
|
+ if (personnelWriteOverlay.isEmpty() || personnelListCache == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ java.util.Iterator<Map.Entry<String, PersonnelWriteOverlay>> it =
|
|
|
+ personnelWriteOverlay.entrySet().iterator();
|
|
|
+ java.util.IdentityHashMap<PersonnelWriteOverlay, Boolean> applied =
|
|
|
+ new java.util.IdentityHashMap<PersonnelWriteOverlay, Boolean>();
|
|
|
+ while (it.hasNext()) {
|
|
|
+ Map.Entry<String, PersonnelWriteOverlay> e = it.next();
|
|
|
+ PersonnelWriteOverlay ov = e.getValue();
|
|
|
+ if (ov == null || ov.expireAt <= now) {
|
|
|
+ it.remove();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (applied.containsKey(ov)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ applied.put(ov, Boolean.TRUE);
|
|
|
+ Map<String, Object> dmsRow = personnelRowByKey == null ? null : personnelRowByKey.get(e.getKey());
|
|
|
+ long dmsUt = dmsRow == null ? 0L : nzLong(DmsRowUtils.longVal(dmsRow, "update_time"));
|
|
|
+ long ovUt = nzLong(DmsRowUtils.longVal(ov.row, "update_time"));
|
|
|
+ if (dmsRow == null || ovUt >= dmsUt) {
|
|
|
+ upsertPersonnelCacheLocked(ov.row);
|
|
|
+ } else {
|
|
|
+ it.remove();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private static long nzLong(Long v) {
|
|
|
+ return v == null ? 0L : v.longValue();
|
|
|
+ }
|
|
|
+
|
|
|
/** 从缓存去掉已销毁/已删除的人(按业务号或 DMS id)。 */
|
|
|
public void removePersonnelFromCache(java.util.Collection<String> ids) {
|
|
|
if (ids == null || ids.isEmpty()) {
|
|
|
@@ -544,6 +618,7 @@ public class BizDataService {
|
|
|
}
|
|
|
personnelListCache = all;
|
|
|
personnelRowByKey = idx;
|
|
|
+ applyPersonnelWriteOverlayLocked();
|
|
|
personnelListCacheExpireAt = System.currentTimeMillis() + PERSONNEL_LIST_CACHE_TTL_MS;
|
|
|
System.out.println("[personnel-cache] loaded " + all.size() + " in "
|
|
|
+ (System.currentTimeMillis() - t0) + "ms");
|
|
|
@@ -669,6 +744,10 @@ public class BizDataService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ // 历史回盘种子用 H606002 等非人员主键写告警/明细,按身份证回挂真实档案
|
|
|
+ if (found == null) {
|
|
|
+ found = resolvePersonnelByLegacyPaymentId(id);
|
|
|
+ }
|
|
|
}
|
|
|
if (found != null) {
|
|
|
enrichMonthlyFromTownStandard(found, new HashMap<String, BigDecimal>());
|
|
|
@@ -676,37 +755,97 @@ public class BizDataService {
|
|
|
return found;
|
|
|
}
|
|
|
|
|
|
+ /** 告警/明细上的历史种子人员 ID,按发放明细身份证对到现有人员。 */
|
|
|
+ private PersonnelVO resolvePersonnelByLegacyPaymentId(String legacyId) {
|
|
|
+ if (!StringUtils.hasText(legacyId) || legacyId.startsWith("P")
|
|
|
+ || legacyId.matches("(?i)^(TB|DB|VB|MB).+")) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<PaymentDetailVO> pays = loadPaymentsByPersonnelId(legacyId);
|
|
|
+ if (pays == null || pays.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ PaymentDetailVO d = pays.get(0);
|
|
|
+ String idNo = d == null ? null : d.getIdNumber();
|
|
|
+ String name = d == null ? null : d.getName();
|
|
|
+ String townId = d == null ? null : d.getTownId();
|
|
|
+ String villageId = d == null ? null : d.getVillageId();
|
|
|
+ PersonnelVO byIdNo = null;
|
|
|
+ PersonnelVO byName = null;
|
|
|
+ for (PersonnelVO p : loadPersonnelListCached()) {
|
|
|
+ if (p == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(idNo) && idNo.equals(p.getIdNumber())) {
|
|
|
+ byIdNo = p;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (byName == null && StringUtils.hasText(name) && name.equals(p.getName())
|
|
|
+ && (!StringUtils.hasText(townId) || townId.equals(p.getTownId()))
|
|
|
+ && (!StringUtils.hasText(villageId) || villageId.equals(p.getVillageId()))) {
|
|
|
+ byName = p;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return byIdNo != null ? byIdNo : byName;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 月补标准以街镇金额标准表当前有效值为准(关联带出),覆盖人员表冗余字段。
|
|
|
- * 优先 YEARLY_ADJUST(土地额),再回退历史 MONTHLY 单行——与 /api/amount-standards/current 一致。
|
|
|
+ * 月补标准取全区统一当前有效值(T_DISTRICT),与街镇无关。
|
|
|
*/
|
|
|
public BigDecimal currentMonthlyStandard(String townId) {
|
|
|
- if (!StringUtils.hasText(townId) || !dmsOn()) {
|
|
|
+ if (!dmsOn()) {
|
|
|
return null;
|
|
|
}
|
|
|
- PageResult<AmountStandardVO> yearly = pageStandards(townId, StandardTypeEnum.YEARLY_ADJUST, true, 1, 1);
|
|
|
- if (yearly.getList() != null && !yearly.getList().isEmpty()
|
|
|
- && yearly.getList().get(0).getAmount() != null) {
|
|
|
- return yearly.getList().get(0).getAmount();
|
|
|
- }
|
|
|
- PageResult<AmountStandardVO> page = pageStandards(townId, StandardTypeEnum.MONTHLY, true, 1, 1);
|
|
|
- if (page.getList() == null || page.getList().isEmpty()) {
|
|
|
+ AmountStandardVO y = resolveDistrictCurrentYearly();
|
|
|
+ return y == null ? null : y.getAmount();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 全区当前年度标准;尚未收口时回退「人员明细统一 1060/24868」,再回退任一当前 YEARLY。 */
|
|
|
+ public AmountStandardVO resolveDistrictCurrentYearly() {
|
|
|
+ if (!dmsOn()) {
|
|
|
return null;
|
|
|
}
|
|
|
- return page.getList().get(0).getAmount();
|
|
|
+ AmountStandardVO official = null;
|
|
|
+ AmountStandardVO anyCurrent = null;
|
|
|
+ for (AmountStandardVO s : loadStandardListCached()) {
|
|
|
+ if (s == null || Boolean.TRUE.equals(s.getDeleted())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (s.getStandardType() != StandardTypeEnum.YEARLY_ADJUST
|
|
|
+ && s.getStandardType() != StandardTypeEnum.MONTHLY) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!Boolean.TRUE.equals(s.getCurrent())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (AmountStandardIds.isDistrict(s.getTownId())
|
|
|
+ && s.getStandardType() == StandardTypeEnum.YEARLY_ADJUST) {
|
|
|
+ return s;
|
|
|
+ }
|
|
|
+ if (s.getStandardType() == StandardTypeEnum.YEARLY_ADJUST && isOfficial1060(s)) {
|
|
|
+ official = s;
|
|
|
+ }
|
|
|
+ if (anyCurrent == null && s.getStandardType() == StandardTypeEnum.YEARLY_ADJUST) {
|
|
|
+ anyCurrent = s;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return official != null ? official : anyCurrent;
|
|
|
+ }
|
|
|
+
|
|
|
+ static boolean isOfficial1060(AmountStandardVO s) {
|
|
|
+ return s != null
|
|
|
+ && s.getAmount() != null && s.getAmount().compareTo(new BigDecimal("1060")) == 0
|
|
|
+ && s.getFuneralAmount() != null && s.getFuneralAmount().compareTo(new BigDecimal("24868")) == 0;
|
|
|
}
|
|
|
|
|
|
private void enrichMonthlyFromTownStandard(PersonnelVO p, Map<String, BigDecimal> cache) {
|
|
|
- if (p == null || !StringUtils.hasText(p.getTownId())) {
|
|
|
+ if (p == null) {
|
|
|
return;
|
|
|
}
|
|
|
- BigDecimal amt = cache.get(p.getTownId());
|
|
|
- if (!cache.containsKey(p.getTownId())) {
|
|
|
- amt = currentMonthlyStandard(p.getTownId());
|
|
|
- cache.put(p.getTownId(), amt);
|
|
|
+ if (!cache.containsKey(AmountStandardIds.DISTRICT_TOWN_ID)) {
|
|
|
+ cache.put(AmountStandardIds.DISTRICT_TOWN_ID, currentMonthlyStandard(null));
|
|
|
}
|
|
|
- // 以街镇标准表为准:有则覆盖;无标准则清空,禁止展示人员表里的陈旧冗余额
|
|
|
- p.setMonthlyStandard(amt);
|
|
|
+ p.setMonthlyStandard(cache.get(AmountStandardIds.DISTRICT_TOWN_ID));
|
|
|
}
|
|
|
|
|
|
private Map<String, String> buildRegionAliasMap() {
|
|
|
@@ -789,9 +928,28 @@ public class BizDataService {
|
|
|
if (!matchBizStatusFilters(p, req)) {
|
|
|
return false;
|
|
|
}
|
|
|
+ if (req.getCreatedSince() != null && p.getCreateTime() != null
|
|
|
+ && p.getCreateTime() < req.getCreatedSince()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(req.getEnjoyStartMonth())) {
|
|
|
+ String want = compactYearMonth(req.getEnjoyStartMonth());
|
|
|
+ String got = compactYearMonth(p.getEnjoyStartMonth());
|
|
|
+ if (!StringUtils.hasText(want) || !want.equals(got)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ private static String compactYearMonth(String raw) {
|
|
|
+ if (!StringUtils.hasText(raw)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ String s = raw.replace("-", "").trim();
|
|
|
+ return s.length() >= 6 ? s.substring(0, 6) : "";
|
|
|
+ }
|
|
|
+
|
|
|
private boolean matchBizStatusFilters(PersonnelVO p, PersonnelQueryRequest req) {
|
|
|
if (Boolean.TRUE.equals(req.getHideDraftUnlessVillage())
|
|
|
&& !StringUtils.hasText(req.getVillageId())
|
|
|
@@ -830,9 +988,17 @@ public class BizDataService {
|
|
|
vo.setName(DmsRowUtils.str(row, "c_name"));
|
|
|
vo.setIdNumber(DmsRowUtils.str(row, "c_id_number"));
|
|
|
vo.setSocialInsuranceCode(DmsRowUtils.str(row, "c_social_insurance_code"));
|
|
|
+ String localOrRemote = DmsRowUtils.firstNonBlank(
|
|
|
+ DmsRowUtils.str(row, "c_local_or_remote"),
|
|
|
+ DmsRowUtils.remarkTag(remarks, "LOCAL_REMOTE"));
|
|
|
+ vo.setLocalOrRemote(localOrRemote);
|
|
|
+ vo.setDeathWarning(isDeathWarning(DmsRowUtils.str(row, "c_death_status"),
|
|
|
+ DmsRowUtils.str(row, "c_death_date"),
|
|
|
+ enumOrNull(PersonnelStatusEnum.class, DmsRowUtils.str(row, "c_biz_status"))));
|
|
|
Object phone = row.get("c_phone_number");
|
|
|
vo.setPhoneNumber(phone == null ? null : String.valueOf(phone));
|
|
|
vo.setAddress(DmsRowUtils.str(row, "c_address"));
|
|
|
+ vo.setHouseholdAddress(DmsRowUtils.str(row, "c_household_address"));
|
|
|
vo.setHouseholdType(DmsRowUtils.str(row, "c_household_type"));
|
|
|
vo.setDistrictId(DmsRowUtils.str(row, "c_district_id"));
|
|
|
vo.setTownId(DmsRowUtils.str(row, "c_town_id"));
|
|
|
@@ -859,6 +1025,7 @@ public class BizDataService {
|
|
|
vo.setPayThisMonth(DmsRowUtils.bool(row, "c_pay_this_month"));
|
|
|
vo.setSource(DmsRowUtils.str(row, "c_source"));
|
|
|
vo.setInputVillageId(DmsRowUtils.str(row, "c_input_village_id"));
|
|
|
+ vo.setDeathStatus(DmsRowUtils.str(row, "c_death_status"));
|
|
|
vo.setDeathDate(DmsRowUtils.str(row, "c_death_date"));
|
|
|
vo.setHeirName(DmsRowUtils.str(row, "c_heir_name"));
|
|
|
vo.setHeirIdNumber(DmsRowUtils.str(row, "c_heir_id_number"));
|
|
|
@@ -867,22 +1034,49 @@ public class BizDataService {
|
|
|
vo.setHeirRelation(DmsRowUtils.firstNonBlank(
|
|
|
DmsRowUtils.str(row, "c_heir_relation"),
|
|
|
DmsRowUtils.remarkTag(remarks, "heirRelation")));
|
|
|
+ vo.setPersonType(DmsRowUtils.str(row, "c_person_type"));
|
|
|
+ vo.setUrbanMedicareConfirm(DmsRowUtils.str(row, "c_urban_medicare_confirm"));
|
|
|
+ vo.setHouseholdDistrict(DmsRowUtils.str(row, "c_household_district"));
|
|
|
+ vo.setHouseholdTown(DmsRowUtils.str(row, "c_household_town"));
|
|
|
+ vo.setHouseholdVillage(DmsRowUtils.str(row, "c_household_village"));
|
|
|
+ vo.setContactName(DmsRowUtils.str(row, "c_contact_name"));
|
|
|
+ vo.setContactPhone(DmsRowUtils.str(row, "c_contact_phone"));
|
|
|
+ vo.setInsuranceJoinDate(DmsRowUtils.str(row, "c_insurance_join_date"));
|
|
|
+ vo.setPersonMark(DmsRowUtils.str(row, "c_person_mark"));
|
|
|
+ vo.setChangeReason(DmsRowUtils.str(row, "c_change_reason"));
|
|
|
+ vo.setBenefitApplyDate(DmsRowUtils.str(row, "c_benefit_apply_date"));
|
|
|
+ vo.setSentenceStart(DmsRowUtils.str(row, "c_sentence_start"));
|
|
|
+ vo.setSentenceEnd(DmsRowUtils.str(row, "c_sentence_end"));
|
|
|
+ vo.setCancelApplyDate(DmsRowUtils.str(row, "c_cancel_apply_date"));
|
|
|
+ vo.setCancelReason(DmsRowUtils.str(row, "c_cancel_reason"));
|
|
|
+ vo.setCancelDeathMonth(DmsRowUtils.str(row, "c_cancel_death_month"));
|
|
|
+ vo.setHasHeir(DmsRowUtils.str(row, "c_has_heir"));
|
|
|
+ vo.setHeirIdType(DmsRowUtils.str(row, "c_heir_id_type"));
|
|
|
+ vo.setHeirBankName(DmsRowUtils.str(row, "c_heir_bank_name"));
|
|
|
+ vo.setHeirHouseholdAddress(DmsRowUtils.str(row, "c_heir_household_address"));
|
|
|
+ vo.setHeirAddress(DmsRowUtils.str(row, "c_heir_address"));
|
|
|
vo.setFuneralDiffAmount(DmsRowUtils.decimal(row, "c_funeral_diff_amount"));
|
|
|
vo.setLandSubsidyIncrease(DmsRowUtils.decimal(row, "c_land_subsidy_increase"));
|
|
|
vo.setRetireQualifyType(resolveRetireQualifyType(
|
|
|
DmsRowUtils.str(row, "c_retire_qualify_type"), remarks));
|
|
|
- // 待提交资格:尚未生效,不展示上次通过留下的有地/无地
|
|
|
- if (vo.getBizStatus() == PersonnelStatusEnum.DRAFT
|
|
|
- && vo.getRetireQualifyType() != RetireQualifyTypeEnum.INELIGIBLE) {
|
|
|
- vo.setRetireQualifyType(null);
|
|
|
- }
|
|
|
- vo.setRemarks(remarks);
|
|
|
+ vo.setRemarks(DmsRowUtils.withoutRemarkTag(remarks, "PROFILE_SYNC"));
|
|
|
vo.setCreateTime(DmsRowUtils.longVal(row, "create_time"));
|
|
|
vo.setUpdateTime(DmsRowUtils.longVal(row, "update_time"));
|
|
|
applyRegionDisplayNames(vo);
|
|
|
return vo;
|
|
|
}
|
|
|
|
|
|
+ /** 死亡预警:大数据/死亡日期提示,不把业务态改成丧葬。 */
|
|
|
+ static boolean isDeathWarning(String deathStatus, String deathDate, PersonnelStatusEnum biz) {
|
|
|
+ if (biz == PersonnelStatusEnum.FUNERAL || biz == PersonnelStatusEnum.EXITED) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if ("已死亡".equals(deathStatus == null ? "" : deathStatus.trim())) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return StringUtils.hasText(deathDate);
|
|
|
+ }
|
|
|
+
|
|
|
/** 人员列表展示用:按区划字典把街镇/村居 ID 转成中文名,避免前端缓存未加载时露出 V_YX_003 */
|
|
|
private void applyRegionDisplayNames(PersonnelVO vo) {
|
|
|
if (vo == null) {
|
|
|
@@ -1094,8 +1288,23 @@ public class BizDataService {
|
|
|
if (needPays.isEmpty()) {
|
|
|
continue;
|
|
|
}
|
|
|
- // 同月全区批只拉一次发放明细,再按镇切片,避免列表页 N 镇 N 次全量 DMS 翻页
|
|
|
- List<PaymentDetailVO> allPays = listPaymentsByBatch(parent, null);
|
|
|
+ // 同月全区批:若本页仅涉及单镇,按镇过滤拉明细,避免整区翻页导致月补贴列表超时
|
|
|
+ String sliceTownId = null;
|
|
|
+ boolean singleTown = true;
|
|
|
+ for (BatchVO b : needPays) {
|
|
|
+ String tid = b.getTownId() == null ? "" : b.getTownId().trim();
|
|
|
+ if (!StringUtils.hasText(tid)) {
|
|
|
+ singleTown = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (sliceTownId == null) {
|
|
|
+ sliceTownId = tid;
|
|
|
+ } else if (!sliceTownId.equals(tid)) {
|
|
|
+ singleTown = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ List<PaymentDetailVO> allPays = listPaymentsByBatch(parent, singleTown ? sliceTownId : null);
|
|
|
Map<String, List<PaymentDetailVO>> paysByTown = new LinkedHashMap<String, List<PaymentDetailVO>>();
|
|
|
if (allPays != null) {
|
|
|
for (PaymentDetailVO d : allPays) {
|
|
|
@@ -1128,48 +1337,20 @@ public class BizDataService {
|
|
|
return parent;
|
|
|
}
|
|
|
|
|
|
- /** 统计表命中时回填镇切片,避免再扫发放明细。 */
|
|
|
+ /** 只认街镇粒度,且月标合计已写入的统计行。村居行和未写月标的回盘行不能覆盖批次头。 */
|
|
|
public BatchStatVO findTownMonthStat(String payMonth, String townId) {
|
|
|
- if (!StringUtils.hasText(payMonth) || !StringUtils.hasText(townId)) {
|
|
|
+ BatchStatVO stat = TownStatAmounts.pickTownStat(loadStatListCached(), payMonth, townId);
|
|
|
+ if (!TownStatAmounts.canApply(stat)) {
|
|
|
return null;
|
|
|
}
|
|
|
- String ym = payMonth.replace("-", "").trim();
|
|
|
- for (BatchStatVO s : loadStatListCached()) {
|
|
|
- if (s == null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- String sm = s.getPayMonth() == null ? "" : s.getPayMonth().replace("-", "").trim();
|
|
|
- if (!ym.equals(sm) || !townId.equals(s.getTownId())) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (s.getShouldPayCount() != null && s.getShouldPayCount() > 0) {
|
|
|
- return s;
|
|
|
- }
|
|
|
- }
|
|
|
- return null;
|
|
|
+ return stat;
|
|
|
}
|
|
|
|
|
|
private boolean applyTownStatToBatch(BatchVO b, BatchStatVO stat) {
|
|
|
- if (b == null || stat == null) {
|
|
|
+ if (b == null || !TownStatAmounts.canApply(stat)) {
|
|
|
return false;
|
|
|
}
|
|
|
- int n = stat.getShouldPayCount() == null ? 0 : stat.getShouldPayCount();
|
|
|
- java.math.BigDecimal sum = stat.getShouldPayAmount() == null
|
|
|
- ? java.math.BigDecimal.ZERO : stat.getShouldPayAmount();
|
|
|
- if (n <= 0 && sum.compareTo(java.math.BigDecimal.ZERO) <= 0) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- b.setPeopleCount(n);
|
|
|
- b.setTotalAmount(sum);
|
|
|
- b.setNormalAmount(stat.getStandardAmount() == null ? java.math.BigDecimal.ZERO : stat.getStandardAmount());
|
|
|
- b.setSupplementAmount(stat.getSupplementAmount() == null ? java.math.BigDecimal.ZERO : stat.getSupplementAmount());
|
|
|
- b.setFuneralAmount(stat.getDeathAmount() == null ? java.math.BigDecimal.ZERO : stat.getDeathAmount());
|
|
|
- java.math.BigDecimal deduct = sum.subtract(
|
|
|
- nz(b.getNormalAmount()).add(nz(b.getSupplementAmount())).add(nz(b.getFuneralAmount())));
|
|
|
- if (deduct.compareTo(java.math.BigDecimal.ZERO) < 0) {
|
|
|
- deduct = java.math.BigDecimal.ZERO;
|
|
|
- }
|
|
|
- b.setDeductAmount(deduct);
|
|
|
+ TownStatAmounts.apply(b, stat);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -1484,14 +1665,11 @@ public class BizDataService {
|
|
|
private final AtomicBoolean batchRefreshing = new AtomicBoolean(false);
|
|
|
|
|
|
public void invalidateBatchCache() {
|
|
|
- batchByIdCacheExpireAt = 0L;
|
|
|
- if (batchByIdCache != null) {
|
|
|
- startCacheRefresh(batchRefreshing, "batch-cache-refresh", batchByIdCacheLock, new Runnable() {
|
|
|
- @Override
|
|
|
- public void run() {
|
|
|
- reloadBatchCacheLocked();
|
|
|
- }
|
|
|
- });
|
|
|
+ // 必须丢弃快照:仅把 expireAt 置 0 仍会走 stale-while-revalidate,审批后列表仍读旧状态
|
|
|
+ synchronized (batchByIdCacheLock) {
|
|
|
+ batchByIdCache = null;
|
|
|
+ batchRowByKey = null;
|
|
|
+ batchByIdCacheExpireAt = 0L;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2081,7 +2259,7 @@ public class BizDataService {
|
|
|
if (slice.rows != null) {
|
|
|
for (Map<String, Object> row : slice.rows) {
|
|
|
PaymentDetailVO d = toPaymentFromRow(row);
|
|
|
- if (d != null && matchesPaymentQueryFilters(d, req)) {
|
|
|
+ if (d != null && !skipTownStatPayment(d, req) && matchesPaymentQueryFilters(d, req)) {
|
|
|
list.add(d);
|
|
|
}
|
|
|
}
|
|
|
@@ -2159,6 +2337,9 @@ public class BizDataService {
|
|
|
}
|
|
|
List<PaymentDetailVO> filtered = new ArrayList<PaymentDetailVO>();
|
|
|
for (PaymentDetailVO d : candidates) {
|
|
|
+ if (skipTownStatPayment(d, req)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
if (matchesPaymentQueryFilters(d, req)) {
|
|
|
filtered.add(d);
|
|
|
}
|
|
|
@@ -2231,6 +2412,7 @@ public class BizDataService {
|
|
|
return queryPaymentsFromDms(search);
|
|
|
}
|
|
|
});
|
|
|
+ java.util.Set<String> seenIds = new java.util.HashSet<String>();
|
|
|
for (PaymentDetailVO d : all) {
|
|
|
if (d == null || isRemovedPayment(d)) {
|
|
|
continue;
|
|
|
@@ -2241,6 +2423,9 @@ public class BizDataService {
|
|
|
if (StringUtils.hasText(townId) && !townId.equals(d.getTownId())) {
|
|
|
continue;
|
|
|
}
|
|
|
+ if (StringUtils.hasText(d.getId()) && !seenIds.add(d.getId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
out.add(d);
|
|
|
}
|
|
|
return out;
|
|
|
@@ -2360,6 +2545,15 @@ public class BizDataService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 镇统计批上的明细是重复挂账,不作为这个人的第二笔发放。显式按该批查询时仍返回,便于清理。 */
|
|
|
+ private boolean skipTownStatPayment(PaymentDetailVO d, PaymentDetailQueryRequest req) {
|
|
|
+ if (d == null || !TownStatAmounts.isTownStatBatchId(d.getBatchId())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String asked = req == null ? null : req.getBatchId();
|
|
|
+ return !TownStatAmounts.isTownStatBatchId(asked);
|
|
|
+ }
|
|
|
+
|
|
|
private boolean isRemovedPayment(PaymentDetailVO d) {
|
|
|
if (d == null) {
|
|
|
return true;
|
|
|
@@ -2498,7 +2692,8 @@ public class BizDataService {
|
|
|
if (Boolean.TRUE.equals(s.getDeleted())) {
|
|
|
return false;
|
|
|
}
|
|
|
- if (StringUtils.hasText(townId) && !townId.equals(s.getTownId())) {
|
|
|
+ // 列表与核算都只认全区行;未收口前 resolveDistrictCurrentYearly 另有回退
|
|
|
+ if (!AmountStandardIds.isDistrict(s.getTownId())) {
|
|
|
return false;
|
|
|
}
|
|
|
if (type != null && s.getStandardType() != type) {
|
|
|
@@ -2527,6 +2722,10 @@ public class BizDataService {
|
|
|
AmountStandardVO vo = new AmountStandardVO();
|
|
|
vo.setId(DmsRowUtils.str(row, "id"));
|
|
|
vo.setTownId(DmsRowUtils.str(row, "c_town_id"));
|
|
|
+ if (AmountStandardIds.isDistrict(vo.getTownId())) {
|
|
|
+ vo.setTownId(AmountStandardIds.DISTRICT_TOWN_ID);
|
|
|
+ vo.setTownName(AmountStandardIds.DISTRICT_TOWN_NAME);
|
|
|
+ }
|
|
|
vo.setAmount(DmsRowUtils.decimal(row, "c_amount"));
|
|
|
vo.setFuneralAmount(DmsRowUtils.decimal(row, "c_funeral_amount"));
|
|
|
vo.setEffectiveTime(DmsRowUtils.longVal(row, "c_date_time"));
|
|
|
@@ -2738,6 +2937,9 @@ public class BizDataService {
|
|
|
if (req.getType() != null && a.getType() != req.getType()) {
|
|
|
return false;
|
|
|
}
|
|
|
+ if (req.getLevel() != null && a.getLevel() != req.getLevel()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
if (req.getHandleStatus() != null && a.getHandleStatus() != req.getHandleStatus()) {
|
|
|
return false;
|
|
|
}
|
|
|
@@ -2851,6 +3053,8 @@ public class BizDataService {
|
|
|
vo.setContent(DmsRowUtils.str(row, "c_content"));
|
|
|
// 历史告警可能未落库批次字段:从正文「批次 VB… / 202608」回填,供列表「批次号」展示
|
|
|
fillAlertBatchFromContent(vo);
|
|
|
+ // 审批超时等批次级:历史误把批次 ID/号写入人员字段,读出时清空以免前端当人员查
|
|
|
+ sanitizeBatchLevelAlertPersonnel(vo);
|
|
|
String code = DmsRowUtils.str(row, "c_fail_reason_code");
|
|
|
if (StringUtils.hasText(code)) {
|
|
|
if (code.startsWith("CODE_")) {
|
|
|
@@ -2890,6 +3094,34 @@ public class BizDataService {
|
|
|
if (needNo) vo.setBatchNo(token);
|
|
|
}
|
|
|
|
|
|
+ /** 批次级告警:人员字段若等于批次 ID/号或呈批次形态,则清空(仅 VO,不回写 DMS) */
|
|
|
+ private static void sanitizeBatchLevelAlertPersonnel(AlertVO vo) {
|
|
|
+ if (vo == null || vo.getType() != AlertTypeEnum.APPROVAL_TIMEOUT) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String pid = vo.getPersonnelId() == null ? "" : vo.getPersonnelId().trim();
|
|
|
+ String pname = vo.getPersonnelName() == null ? "" : vo.getPersonnelName().trim();
|
|
|
+ String bid = vo.getBatchId() == null ? "" : vo.getBatchId().trim();
|
|
|
+ String bno = vo.getBatchNo() == null ? "" : vo.getBatchNo().trim();
|
|
|
+ boolean badId = !StringUtils.hasText(pid)
|
|
|
+ || pid.equals(bid)
|
|
|
+ || pid.equals(bno)
|
|
|
+ || pid.matches("(?i)^(TB|DB|VB|MB).+")
|
|
|
+ || (bno.length() >= 6 && pid.equals(bno));
|
|
|
+ boolean badName = !StringUtils.hasText(pname)
|
|
|
+ || pname.equals(bno)
|
|
|
+ || pname.equals(bid)
|
|
|
+ || pname.equals(pid)
|
|
|
+ || pname.matches("(?i)^(TB|DB|VB|MB).+")
|
|
|
+ || pname.matches("^\\d{6}$");
|
|
|
+ if (badId) {
|
|
|
+ vo.setPersonnelId(null);
|
|
|
+ }
|
|
|
+ if (badName) {
|
|
|
+ vo.setPersonnelName(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// ---------- 统计 / 流程 ----------
|
|
|
private final Object statListLock = new Object();
|
|
|
private volatile List<BatchStatVO> statListCache;
|
|
|
@@ -2909,6 +3141,13 @@ public class BizDataService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 刚写入村级统计后立刻重拉,避免异步刷新窗口内村表仍走发放明细。 */
|
|
|
+ public void reloadStatCacheNow() {
|
|
|
+ synchronized (statListLock) {
|
|
|
+ reloadStatCacheLocked();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private List<BatchStatVO> loadStatListCached() {
|
|
|
List<BatchStatVO> hit = statListCache;
|
|
|
if (hit != null && statListCacheExpireAt > System.currentTimeMillis()) {
|
|
|
@@ -3070,11 +3309,53 @@ public class BizDataService {
|
|
|
return Long.compare(tb, ta);
|
|
|
}
|
|
|
});
|
|
|
+ Map<String, Map<String, Object>> prev = processRowByKey;
|
|
|
+ if (prev != null) {
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ for (Map.Entry<String, Map<String, Object>> entry : prev.entrySet()) {
|
|
|
+ if (entry.getValue() == null || idx.containsKey(entry.getKey())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Long operateAt = DmsRowUtils.longVal(entry.getValue(), "c_operate_time");
|
|
|
+ if (operateAt != null && now - operateAt < 120_000L) {
|
|
|
+ idx.put(entry.getKey(), entry.getValue());
|
|
|
+ all.add(toProcess(entry.getValue()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
processCache = all;
|
|
|
processRowByKey = idx;
|
|
|
processCacheExpireAt = System.currentTimeMillis() + PROCESS_CACHE_TTL_MS;
|
|
|
}
|
|
|
|
|
|
+ /** 新建流程单立刻可被审批读到,避免异步刷新窗口里查不到刚写入的单据。 */
|
|
|
+ public void pinProcessRow(String id, Map<String, Object> content) {
|
|
|
+ if (!StringUtils.hasText(id) || content == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Map<String, Object> row = new LinkedHashMap<String, Object>(content);
|
|
|
+ row.put("id", id.trim());
|
|
|
+ synchronized (processListLock) {
|
|
|
+ Map<String, Map<String, Object>> idx = processRowByKey;
|
|
|
+ if (idx == null) {
|
|
|
+ idx = new HashMap<String, Map<String, Object>>();
|
|
|
+ processRowByKey = idx;
|
|
|
+ }
|
|
|
+ idx.put(id.trim(), row);
|
|
|
+ if (processCache != null) {
|
|
|
+ List<ProcessLogVO> next = new ArrayList<ProcessLogVO>();
|
|
|
+ next.add(toProcess(row));
|
|
|
+ for (ProcessLogVO old : processCache) {
|
|
|
+ if (old == null || id.trim().equals(old.getId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ next.add(old);
|
|
|
+ }
|
|
|
+ processCache = next;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public List<ProcessLogVO> listProcessFiltered(ProcessLogQueryRequest req) {
|
|
|
if (!dmsOn() || req == null) {
|
|
|
return Collections.emptyList();
|
|
|
@@ -3273,7 +3554,7 @@ public class BizDataService {
|
|
|
return PageResult.of(pageNum, pageSize, all.size(), all.subList(from, to));
|
|
|
}
|
|
|
|
|
|
- /** 列表默认:待提交/新增优先,同组内按创建时间或 ID 倒序(新录入排前) */
|
|
|
+ /** 列表默认:待办/新增优先;已退出(丧葬发完)与已归档沉底。同组内按创建时间或 ID 倒序。 */
|
|
|
private void sortPersonnelForList(List<PersonnelVO> list) {
|
|
|
if (list == null || list.size() < 2) {
|
|
|
return;
|
|
|
@@ -3308,8 +3589,20 @@ public class BizDataService {
|
|
|
return 2;
|
|
|
case NEW:
|
|
|
return 3;
|
|
|
- default:
|
|
|
+ case FUNERAL:
|
|
|
+ return 4;
|
|
|
+ case PAUSED:
|
|
|
+ case STOPPED:
|
|
|
+ return 8;
|
|
|
+ case NORMAL:
|
|
|
+ case PENDING_INSURANCE_MATCH:
|
|
|
return 10;
|
|
|
+ case ARCHIVED:
|
|
|
+ return 80;
|
|
|
+ case EXITED:
|
|
|
+ return 90;
|
|
|
+ default:
|
|
|
+ return 15;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -3408,6 +3701,23 @@ public class BizDataService {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ /** 写特殊业务审批结果后立刻改内存行,避免流程缓存 SWR 仍返回「待分管领导审批」。 */
|
|
|
+ public void patchProcessRowCache(String id, Map<String, Object> patch) {
|
|
|
+ if (!StringUtils.hasText(id) || patch == null || patch.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ synchronized (processListLock) {
|
|
|
+ Map<String, Map<String, Object>> idx = processRowByKey;
|
|
|
+ if (idx == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Map<String, Object> row = idx.get(id.trim());
|
|
|
+ if (row != null) {
|
|
|
+ row.putAll(patch);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/** 流程表原始行(按 DMS id)。 */
|
|
|
public Map<String, Object> findProcessRaw(String id) {
|
|
|
if (!StringUtils.hasText(id)) {
|