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"},
)
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]]:
"""
按人员编码查询 OA 人员信息

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import json
import logging
import re
import time
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import Any
@ -169,6 +170,12 @@ def _normalize_decimal_1(v: Any) -> str:
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):
"""
EHR 请假 -> OA 月度同步按工号+日期汇总
@ -417,10 +424,12 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
existing_row_map = dict(existing_row_map_by_sql)
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_insert = 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])):
leave_date_value = f"{leave_date} 00:00:00"
leave_days_value = _decimal_to_str(leave_days)
@ -448,10 +457,21 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
if existing_id > 0:
record["id"] = existing_id
to_update += 1
update_data_list.append(
{
"masterTable": {
"name": master_table_name,
"record": record,
"changedFields": [f["name"] for f in fields_payload],
},
"subTables": [],
}
)
else:
insert_seed += 1
record["id"] = _gen_temp_row_id(insert_seed)
to_insert += 1
data_list.append(
insert_data_list.append(
{
"masterTable": {
"name": master_table_name,
@ -471,12 +491,24 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
else:
do_trigger_bool = str(do_trigger).strip().lower() in ("1", "true", "yes", "y", "on")
for i in range(0, len(data_list), batch_size):
chunk = data_list[i : i + batch_size]
def _run_chunks(*, mode: str, rows: list[dict[str, Any]]) -> None:
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:
continue
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,
loginName=oa_login_name,
rightId=oa_right_id,
@ -492,15 +524,16 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
resp_text = ""
first_row = chunk[0] if chunk else {}
raise RuntimeError(
"OA batch-update HTTP error "
f"OA batch-{mode} HTTP error "
f"status={getattr(e.response, 'status_code', None)!r} "
f"body_preview={resp_text!r} "
f"first_row={json.dumps(first_row, ensure_ascii=False, default=str)[:2000]}"
) 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))
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 {}
chunk_success = int(data_local.get("successCount", 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:
failed_data[str(k)] = str(v)
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,
len(chunk),
chunk_success,
@ -526,11 +560,15 @@ class SyncEhrLeavesToOaMonthJob(BaseJob):
)
if isinstance(fd, dict) and fd:
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,
list(fd.items())[:20],
)
_run_chunks(mode="update", rows=update_data_list)
_run_chunks(mode="add", rows=insert_data_list)
# 写入后复核:重新 export核对本次 key 实际存在数量
verify_resp = seeyon.export_cap4_form_soap(
templateCode=oa_template_code,