This commit is contained in:
Marsway 2026-03-30 17:41:47 +08:00
parent 9d620da015
commit 233713db59
2 changed files with 153 additions and 67 deletions

View File

@ -166,6 +166,54 @@ class SeeyonClient(BaseClient):
headers={"Content-Type": "application/json"}, headers={"Content-Type": "application/json"},
) )
def batch_add_cap4_form_soap(
self,
*,
formCode: str,
loginName: str,
rightId: str,
dataList: list[dict[str, Any]],
uniqueFiled: list[str] | None = None,
doTrigger: bool | None = None,
) -> httpx.Response:
"""
无流程批量添加
POST /seeyon/rest/cap4/form/soap/batch-add
参数与 batch-update 保持一致
"""
form_code = str(formCode or "").strip()
login_name = str(loginName or "").strip()
right_id = str(rightId or "").strip()
if not form_code:
raise ValueError("formCode is required")
if not login_name:
raise ValueError("loginName is required")
if not right_id:
raise ValueError("rightId is required")
if not isinstance(dataList, list) or len(dataList) == 0:
raise ValueError("dataList is required and must be a non-empty list")
if uniqueFiled is not None and not isinstance(uniqueFiled, list):
raise ValueError("uniqueFiled must be a list if provided")
body: dict[str, Any] = {
"formCode": form_code,
"loginName": login_name,
"rightId": right_id,
"dataList": dataList,
}
if uniqueFiled is not None:
body["uniqueFiled"] = uniqueFiled
if doTrigger is not None:
body["doTrigger"] = doTrigger
return self.request(
"POST",
"/seeyon/rest/cap4/form/soap/batch-add",
json=body,
headers={"Content-Type": "application/json"},
)
def get_org_members_by_code(self, *, code: str, pageNo: int = 0, pageSize: int = 20) -> list[dict[str, Any]]: def get_org_members_by_code(self, *, code: str, pageNo: int = 0, pageSize: int = 20) -> list[dict[str, Any]]:
""" """
按人员编码查询 OA 人员信息 按人员编码查询 OA 人员信息

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import json import json
import logging import logging
import re import re
import time
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from typing import Any from typing import Any
@ -169,6 +170,12 @@ def _normalize_decimal_1(v: Any) -> str:
return _decimal_to_str(_to_decimal(v)) return _decimal_to_str(_to_decimal(v))
def _gen_temp_row_id(seed: int) -> int:
# 生成一个 18 位左右的正整数,满足 batch-add 的 record.id 约束
base = int(time.time_ns() // 1000)
return base + int(seed)
class SyncEhrLeavesToOaMonthJob(BaseJob): class SyncEhrLeavesToOaMonthJob(BaseJob):
""" """
EHR 请假 -> OA 月度同步按工号+日期汇总 EHR 请假 -> OA 月度同步按工号+日期汇总
@ -417,10 +424,12 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
existing_row_map = dict(existing_row_map_by_sql) existing_row_map = dict(existing_row_map_by_sql)
existing_row_map.update(existing_row_map_by_export) existing_row_map.update(existing_row_map_by_export)
data_list: list[dict[str, Any]] = [] update_data_list: list[dict[str, Any]] = []
insert_data_list: list[dict[str, Any]] = []
to_update = 0 to_update = 0
to_insert = 0 to_insert = 0
skipped_unchanged = 0 skipped_unchanged = 0
insert_seed = 0
for (job_no, leave_date), leave_days in sorted(agg.items(), key=lambda x: (x[0][0], x[0][1])): for (job_no, leave_date), leave_days in sorted(agg.items(), key=lambda x: (x[0][0], x[0][1])):
leave_date_value = f"{leave_date} 00:00:00" leave_date_value = f"{leave_date} 00:00:00"
leave_days_value = _decimal_to_str(leave_days) leave_days_value = _decimal_to_str(leave_days)
@ -448,10 +457,21 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
if existing_id > 0: if existing_id > 0:
record["id"] = existing_id record["id"] = existing_id
to_update += 1 to_update += 1
update_data_list.append(
{
"masterTable": {
"name": master_table_name,
"record": record,
"changedFields": [f["name"] for f in fields_payload],
},
"subTables": [],
}
)
else: else:
insert_seed += 1
record["id"] = _gen_temp_row_id(insert_seed)
to_insert += 1 to_insert += 1
insert_data_list.append(
data_list.append(
{ {
"masterTable": { "masterTable": {
"name": master_table_name, "name": master_table_name,
@ -471,12 +491,24 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
else: else:
do_trigger_bool = str(do_trigger).strip().lower() in ("1", "true", "yes", "y", "on") do_trigger_bool = str(do_trigger).strip().lower() in ("1", "true", "yes", "y", "on")
for i in range(0, len(data_list), batch_size): def _run_chunks(*, mode: str, rows: list[dict[str, Any]]) -> None:
chunk = data_list[i : i + batch_size] nonlocal success_count, failed_count, failed_data
for i in range(0, len(rows), batch_size):
chunk = rows[i : i + batch_size]
if not chunk: if not chunk:
continue continue
try: try:
resp = seeyon.batch_update_cap4_form_soap( if mode == "update":
resp_local = seeyon.batch_update_cap4_form_soap(
formCode=oa_form_code,
loginName=oa_login_name,
rightId=oa_right_id,
dataList=chunk,
uniqueFiled=[field_job_no, field_leave_date],
doTrigger=do_trigger_bool,
)
else:
resp_local = seeyon.batch_add_cap4_form_soap(
formCode=oa_form_code, formCode=oa_form_code,
loginName=oa_login_name, loginName=oa_login_name,
rightId=oa_right_id, rightId=oa_right_id,
@ -492,15 +524,16 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
resp_text = "" resp_text = ""
first_row = chunk[0] if chunk else {} first_row = chunk[0] if chunk else {}
raise RuntimeError( raise RuntimeError(
"OA batch-update HTTP error " f"OA batch-{mode} HTTP error "
f"status={getattr(e.response, 'status_code', None)!r} " f"status={getattr(e.response, 'status_code', None)!r} "
f"body_preview={resp_text!r} " f"body_preview={resp_text!r} "
f"first_row={json.dumps(first_row, ensure_ascii=False, default=str)[:2000]}" f"first_row={json.dumps(first_row, ensure_ascii=False, default=str)[:2000]}"
) from e ) from e
rj = resp.json() if resp.content else {}
rj = resp_local.json() if resp_local.content else {}
code_local = int(rj.get("code", -1)) code_local = int(rj.get("code", -1))
if code_local != 0: if code_local != 0:
raise RuntimeError(f"OA batch-update failed code={code_local} message={rj.get('message')!r}") raise RuntimeError(f"OA batch-{mode} failed code={code_local} message={rj.get('message')!r}")
data_local = rj.get("data") or {} data_local = rj.get("data") or {}
chunk_success = int(data_local.get("successCount", 0) or 0) chunk_success = int(data_local.get("successCount", 0) or 0)
chunk_failed = int(data_local.get("failCount", 0) or 0) chunk_failed = int(data_local.get("failCount", 0) or 0)
@ -517,7 +550,8 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
if str(k) not in failed_data: if str(k) not in failed_data:
failed_data[str(k)] = str(v) failed_data[str(k)] = str(v)
logger.info( logger.info(
"OA batch-update chunk done: index=%s size=%s success=%s failed=%s message=%s", "OA batch-%s chunk done: index=%s size=%s success=%s failed=%s message=%s",
mode,
i // batch_size + 1, i // batch_size + 1,
len(chunk), len(chunk),
chunk_success, chunk_success,
@ -526,11 +560,15 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
) )
if isinstance(fd, dict) and fd: if isinstance(fd, dict) and fd:
logger.warning( logger.warning(
"OA batch-update failedData sample: chunk=%s sample=%s", "OA batch-%s failedData sample: chunk=%s sample=%s",
mode,
i // batch_size + 1, i // batch_size + 1,
list(fd.items())[:20], list(fd.items())[:20],
) )
_run_chunks(mode="update", rows=update_data_list)
_run_chunks(mode="add", rows=insert_data_list)
# 写入后复核:重新 export核对本次 key 实际存在数量 # 写入后复核:重新 export核对本次 key 实际存在数量
verify_resp = seeyon.export_cap4_form_soap( verify_resp = seeyon.export_cap4_form_soap(
templateCode=oa_template_code, templateCode=oa_template_code,