AutoRpa/rpa_cart/cart_repo.py

246 lines
7.5 KiB
Python

"""表 2 加购行:领取、成功盖章、失败改状态。成功不插新行。"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from db import RepoUnavailable, connect_from_env, oracle_dsn
OPERATE_TYPE_ADD_CART = 1
STATUS_OK = 1
STATUS_FAIL = 2
PUSH_NOT_SENT = 0
STAMP_SELECTION = "系统管理员"
STAMP_CLAIMING = "加购中"
STAMP_DONE = "加购班"
FAIL_REASON_MAX = 500
_CLAIM_SQL = """
UPDATE rpa_cart_settlement_goods_data
SET update_user_name = :stamp,
update_time = SYSDATE
WHERE fid = :fid
AND operate_type = 1
AND status = 1
AND push_status = 0
AND NVL(update_user_name, :selection_stamp) = :selection_stamp
"""
_SUCCESS_SQL = """
UPDATE rpa_cart_settlement_goods_data
SET update_user_name = :stamp,
fail_reason = NULL,
update_time = SYSDATE
WHERE fid = :fid
AND update_user_name = :claim_stamp
"""
_FAIL_SQL = """
UPDATE rpa_cart_settlement_goods_data
SET status = :status,
fail_reason = :fail_reason,
update_user_name = :stamp,
update_time = SYSDATE
WHERE fid = :fid
AND update_user_name = :claim_stamp
"""
@dataclass
class CartRow:
fid: int
oms_id: int
platform_type: int
supplier_name: str
bar_code: str
purchase_qty: int
status: int
fail_reason: str | None
push_status: int
update_user_name: str
operate_type: int = OPERATE_TYPE_ADD_CART
def clip_reason(text: str) -> str:
text = (text or "").strip()
if len(text) <= FAIL_REASON_MAX:
return text
return text[: FAIL_REASON_MAX - 1] + "…"
def _is_pending(row: dict[str, Any]) -> bool:
stamp = row.get("update_user_name") or STAMP_SELECTION
return (
int(row.get("operate_type") or 0) == OPERATE_TYPE_ADD_CART
and int(row.get("status") or 0) == STATUS_OK
and int(row.get("push_status") or 0) == PUSH_NOT_SENT
and stamp == STAMP_SELECTION
)
def _row_from_dict(data: dict[str, Any]) -> CartRow:
qty = data.get("purchase_qty")
return CartRow(
fid=int(data["fid"]),
oms_id=int(data["oms_id"]),
platform_type=int(data["platform_type"]),
supplier_name=str(data.get("supplier_name") or ""),
bar_code=str(data.get("bar_code") or ""),
purchase_qty=int(qty) if qty is not None else 0,
status=int(data.get("status") or 0),
fail_reason=data.get("fail_reason"),
push_status=int(data.get("push_status") or 0),
update_user_name=str(data.get("update_user_name") or ""),
operate_type=int(data.get("operate_type") or OPERATE_TYPE_ADD_CART),
)
class MemoryCartRepo:
def __init__(self, rows: list[dict[str, Any]]):
self.rows: dict[int, dict[str, Any]] = {int(row["fid"]): dict(row) for row in rows}
def claim_pending(
self, limit: int | None = None, platform_types: list[int] | None = None
) -> list[CartRow]:
pending = sorted(
(row for row in self.rows.values() if _is_pending(row)),
key=lambda row: (int(row["oms_id"]), int(row["fid"])),
)
if platform_types is not None:
allowed = set(platform_types)
pending = [row for row in pending if int(row["platform_type"]) in allowed]
if limit is not None:
pending = pending[:limit]
claimed: list[CartRow] = []
for row in pending:
row["update_user_name"] = STAMP_CLAIMING
claimed.append(_row_from_dict(row))
return claimed
def mark_success(self, fid: int) -> None:
row = self.rows[fid]
row["update_user_name"] = STAMP_DONE
row["fail_reason"] = None
def mark_fail(self, fid: int, reason: str) -> None:
row = self.rows[fid]
row["status"] = STATUS_FAIL
row["fail_reason"] = clip_reason(reason)
row["update_user_name"] = STAMP_DONE
class OracleCartRepo:
def __init__(self, connect: Callable):
self._connect = connect
@classmethod
def from_env(cls) -> "OracleCartRepo":
oracle_dsn()
return cls(connect=connect_from_env)
def claim_pending(
self, limit: int | None = None, platform_types: list[int] | None = None
) -> list[CartRow]:
conn = self._connect()
try:
with conn.cursor() as cur:
sql, params = _pending_sql(limit=limit, platform_types=platform_types)
cur.execute(sql, params)
fetched = [_row_from_cursor(cur, rec) for rec in cur.fetchall()]
claimed: list[CartRow] = []
for row in fetched:
cur.execute(
_CLAIM_SQL,
{
"fid": row.fid,
"stamp": STAMP_CLAIMING,
"selection_stamp": STAMP_SELECTION,
},
)
if cur.rowcount == 1:
row.update_user_name = STAMP_CLAIMING
claimed.append(row)
conn.commit()
return claimed
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def mark_success(self, fid: int) -> None:
self._execute(
_SUCCESS_SQL,
{"fid": int(fid), "stamp": STAMP_DONE, "claim_stamp": STAMP_CLAIMING},
)
def mark_fail(self, fid: int, reason: str) -> None:
self._execute(
_FAIL_SQL,
{
"fid": int(fid),
"status": STATUS_FAIL,
"fail_reason": clip_reason(reason),
"stamp": STAMP_DONE,
"claim_stamp": STAMP_CLAIMING,
},
)
def _execute(self, sql: str, params: dict[str, Any]) -> None:
conn = self._connect()
try:
with conn.cursor() as cur:
cur.execute(sql, params)
conn.commit()
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def _row_from_cursor(cur, rec) -> CartRow:
cols = [item[0].lower() for item in cur.description]
return _row_from_dict(dict(zip(cols, rec)))
def _pending_sql(
*, limit: int | None, platform_types: list[int] | None
) -> tuple[str, dict[str, Any]]:
params: dict[str, Any] = {"selection_stamp": STAMP_SELECTION}
filters = [
"operate_type = 1",
"status = 1",
"push_status = 0",
"NVL(update_user_name, :selection_stamp) = :selection_stamp",
]
if platform_types is not None:
if not platform_types:
filters.append("1 = 0")
else:
keys: list[str] = []
for index, platform in enumerate(platform_types):
key = f"p{index}"
keys.append(f":{key}")
params[key] = int(platform)
filters.append(f"platform_type IN ({', '.join(keys)})")
sql = f"""
SELECT fid, oms_id, platform_type, supplier_name, bar_code,
purchase_qty, status, fail_reason, push_status, update_user_name,
operate_type
FROM rpa_cart_settlement_goods_data
WHERE {' AND '.join(filters)}
ORDER BY oms_id, fid
"""
if limit is not None:
params["limit"] = int(limit)
sql = f"SELECT * FROM (\n{sql}\n) WHERE ROWNUM <= :limit"
return sql, params