fix(cash): 智享通凭证行级幂等键剔除派生period + period_conflict留痕 + 批内行序防吞行 (dama-zxt-idemkey-period-20260911)
缺陷: 行级幂等键含 period(日期派生属性),summary口径切主表VoucherDate后判重失效→重复入库(实测V17756 6行)。修: 幂等键=(entity_id,voucher_no,subject_code,debit,credit,summary);命中且period不同→period_conflict计数仅留痕不覆盖;同键多行按载荷内出现次序编号防跨批次吞行;返回体/日志补计数。回归测试10项 + 真实快照41830行复推实证(测试库)。
This commit is contained in:
@@ -23,9 +23,12 @@
|
||||
b) 无 → 查请求体 subject_map {CwAccountID: {subject_code, subject_name}}
|
||||
或 DB 表 zxt_account_dict(CwAccountID, subject_code, subject_name)(若已建)
|
||||
c) 未命中 → 该行拒收 reason="科目字典未命中 CwAccountID=x"(不写脏数据)
|
||||
6. 幂等:快照每10分钟全量重推同一内容。batch = zxt-{日期}-{行集sha1前10},
|
||||
6. 幂等:快照每10分钟全量重推同一内容。batch = zxt-{行集sha1前10},
|
||||
导入前查 import_logs 已存在同 batch → 直接返回 already_imported(不再入库);
|
||||
行级再以 (entity_id, voucher_no, subject_code, debit, credit, summary, period) 防重复。
|
||||
行级再以业务键 (entity_id, voucher_no, subject_code, debit, credit, summary) 防重复
|
||||
—— period 是 voucher_date 派生属性,**不进幂等键**(2026-09-11 加固:日期口径
|
||||
在 summary 派生/主表 VoucherDate 之间切换时,含 period 会判重失效导致重复入库)。
|
||||
命中既有行即跳过;仅当命中行 period 与新行不一致时计入 period_conflict 留痕(不覆盖)。
|
||||
7. entity_id:默认 1(陕西酣客,source=zxt-sxzxb20210228),Bot 通道可传 X-Entity-Id 或 body entity_id。
|
||||
8. 入库后不触发现金流/ledger 联动(凭证完整性未证实前不作为权威来源,见财务口径警示);
|
||||
也不影响 Excel multipart 导入(原端点不动)。
|
||||
@@ -308,6 +311,8 @@ def api_import_vouchers_json(
|
||||
"pending_undated": pending_undated,
|
||||
"pending_dict_miss": pending_dict,
|
||||
"neg_rows": neg_rows,
|
||||
"dup_skipped": 0,
|
||||
"period_conflict": 0,
|
||||
"hint": "科目字典:请协调智享通推送方补 subject_code/subject_name,或在 body 带 subject_map,或建 zxt_account_dict 表",
|
||||
}
|
||||
|
||||
@@ -362,40 +367,71 @@ def api_import_vouchers_json(
|
||||
"total": total,
|
||||
"success_rows": 0,
|
||||
"failed_rows": 0,
|
||||
"dup_skipped": 0,
|
||||
"period_conflict": 0,
|
||||
"neg_rows": neg_rows,
|
||||
"pending_undated": pending_undated,
|
||||
"pending_dict_miss": pending_dict,
|
||||
"batch": batch,
|
||||
"entity_id": entity_id,
|
||||
}
|
||||
|
||||
# ── 入库(行级业务键防重复)──
|
||||
# 幂等键 = (entity_id, voucher_no, subject_code, debit, credit, summary):
|
||||
# period 是由 voucher_date 派生的属性,不进唯一性键(否则日期口径变化时会判重失效)。
|
||||
# 同键多行按"载荷内出现次序"编号:第 n 次出现只与库内第 n 行比对——跨批次同键多重数
|
||||
# 时不会因 query().first() 恒命中同一既存行而把源端合法多行静默吞掉。
|
||||
inserted = 0
|
||||
dup_skipped = 0
|
||||
period_conflict = 0
|
||||
period_conflicts: list = []
|
||||
|
||||
row_groups: dict = {}
|
||||
for r in valid_rows:
|
||||
dup = db.query(VoucherDetail).filter(
|
||||
k = (r["voucher_no"], r["subject_code"], r["debit_amount"],
|
||||
r["credit_amount"], r["summary"])
|
||||
row_groups.setdefault(k, []).append(r)
|
||||
|
||||
for k, group_rows in row_groups.items():
|
||||
existing_rows = db.query(VoucherDetail).filter(
|
||||
VoucherDetail.entity_id == entity_id,
|
||||
VoucherDetail.voucher_no == r["voucher_no"],
|
||||
VoucherDetail.subject_code == r["subject_code"],
|
||||
VoucherDetail.debit_amount == r["debit_amount"],
|
||||
VoucherDetail.credit_amount == r["credit_amount"],
|
||||
VoucherDetail.summary == r["summary"],
|
||||
VoucherDetail.period == r["period"],
|
||||
).first()
|
||||
if dup:
|
||||
dup_skipped += 1
|
||||
continue
|
||||
db.add(VoucherDetail(
|
||||
entity_id=entity_id,
|
||||
voucher_no=r["voucher_no"],
|
||||
voucher_date=r["date"],
|
||||
subject_code=r["subject_code"],
|
||||
subject_name=r["subject_name"],
|
||||
debit_amount=r["debit_amount"],
|
||||
credit_amount=r["credit_amount"],
|
||||
summary=r["summary"],
|
||||
carry_forward=r["carry_forward"],
|
||||
period=r["period"],
|
||||
batch=batch,
|
||||
))
|
||||
inserted += 1
|
||||
VoucherDetail.voucher_no == k[0],
|
||||
VoucherDetail.subject_code == k[1],
|
||||
VoucherDetail.debit_amount == k[2],
|
||||
VoucherDetail.credit_amount == k[3],
|
||||
VoucherDetail.summary == k[4],
|
||||
).order_by(VoucherDetail.id).all()
|
||||
for pos, r in enumerate(group_rows):
|
||||
if pos < len(existing_rows):
|
||||
dup_skipped += 1
|
||||
old = existing_rows[pos]
|
||||
if (old.period or None) != (r["period"] or None):
|
||||
# 判重命中但日期口径不同:只留痕不覆盖(留痕优先,人工判定)
|
||||
period_conflict += 1
|
||||
if len(period_conflicts) < 50:
|
||||
period_conflicts.append({
|
||||
"voucher_no": r["voucher_no"],
|
||||
"existing_id": old.id,
|
||||
"existing_period": old.period,
|
||||
"incoming_period": r["period"],
|
||||
"existing_batch": old.batch,
|
||||
"incoming_batch": batch,
|
||||
})
|
||||
continue
|
||||
db.add(VoucherDetail(
|
||||
entity_id=entity_id,
|
||||
voucher_no=r["voucher_no"],
|
||||
voucher_date=r["date"],
|
||||
subject_code=r["subject_code"],
|
||||
subject_name=r["subject_name"],
|
||||
debit_amount=r["debit_amount"],
|
||||
credit_amount=r["credit_amount"],
|
||||
summary=r["summary"],
|
||||
carry_forward=r["carry_forward"],
|
||||
period=r["period"],
|
||||
batch=batch,
|
||||
))
|
||||
inserted += 1
|
||||
|
||||
period_totals = {}
|
||||
for r in valid_rows:
|
||||
@@ -418,9 +454,14 @@ def api_import_vouchers_json(
|
||||
))
|
||||
db.commit()
|
||||
logger.info(
|
||||
f"智享通凭证JSON导入[{batch}] entity={entity_id}: 总{total}/入库{inserted}/行级失败{failed}/重复跳过{dup_skipped}, "
|
||||
f"凭证级异常{len(abnormal_vouchers)}"
|
||||
f"智享通凭证JSON导入[{batch}] entity={entity_id}: 总{total}/入库{inserted}/行级失败{failed}/重复跳过{dup_skipped}/"
|
||||
f"期间冲突{period_conflict}, 凭证级异常{len(abnormal_vouchers)}"
|
||||
)
|
||||
if period_conflict:
|
||||
logger.warning(
|
||||
f"智享通凭证JSON导入[{batch}] entity={entity_id}: 行级幂等命中但 period 不一致 {period_conflict} 行"
|
||||
f"(未覆盖仅留痕,样例: {period_conflicts[:5]})"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -428,6 +469,8 @@ def api_import_vouchers_json(
|
||||
"success_rows": inserted,
|
||||
"failed_rows": failed,
|
||||
"dup_skipped": dup_skipped,
|
||||
"period_conflict": period_conflict,
|
||||
"period_conflicts": period_conflicts or None,
|
||||
"errors": errors[:100] or None,
|
||||
"balance_check": {
|
||||
"mode": "voucher_level",
|
||||
|
||||
Reference in New Issue
Block a user