oa_receive_account.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. """OA 表单设计器:收款账户控件 form_data 校验 + 账户字段规范化"""
  2. from __future__ import annotations
  3. from typing import Any, Dict, List, Optional, Tuple
  4. from sqlalchemy.orm import Session
  5. from app.models.models import OaReceiveAccount
  6. ACCOUNT_TYPES = frozenset({"alipay", "personal", "corporate"})
  7. _BANK_FIELD_ALIASES = {
  8. "bank_name": ("bankName", "bank_name"),
  9. "bank_province": ("bankProvince", "bank_province"),
  10. "bank_city": ("bankCity", "bank_city"),
  11. "bank_branch": ("bankBranch", "bank_branch"),
  12. }
  13. def _walk_rules(rules: Any, out: List[dict]) -> None:
  14. if not isinstance(rules, list):
  15. return
  16. for r in rules:
  17. if not isinstance(r, dict):
  18. continue
  19. out.append(r)
  20. children = r.get("children") or (r.get("props") or {}).get("rule") or []
  21. if isinstance(children, list):
  22. _walk_rules(children, out)
  23. def _schema_rules(schema: Any) -> List[dict]:
  24. if isinstance(schema, list):
  25. out: List[dict] = []
  26. _walk_rules(schema, out)
  27. return out
  28. if not isinstance(schema, dict):
  29. return []
  30. out: List[dict] = []
  31. _walk_rules(schema.get("rule") or schema.get("rules") or [], out)
  32. return out
  33. def _rule_props(rule: dict) -> dict:
  34. props = rule.get("props")
  35. return props if isinstance(props, dict) else {}
  36. def _field_label(rule: dict) -> str:
  37. return str(rule.get("title") or rule.get("label") or rule.get("field") or "收款账户")
  38. def _is_required(rule: dict) -> bool:
  39. props = _rule_props(rule)
  40. if rule.get("$required") is True:
  41. return True
  42. if rule.get("required") is True:
  43. return True
  44. if props.get("required") is True:
  45. return True
  46. return False
  47. def _pick(body: dict, camel: str, snake: str, default: Any = "") -> Any:
  48. if camel in body:
  49. return body[camel]
  50. if snake in body:
  51. return body[snake]
  52. return default
  53. def _pick_str(body: dict, camel: str, snake: str, default: str = "") -> str:
  54. raw = _pick(body, camel, snake, default)
  55. if raw is None:
  56. return default
  57. return str(raw).strip()
  58. def _pick_order_num(body: dict) -> Optional[int]:
  59. raw = _pick(body, "orderNum", "order_num", None)
  60. if raw is None:
  61. return None
  62. if isinstance(raw, str):
  63. raw = raw.strip()
  64. if raw == "":
  65. return None
  66. try:
  67. return int(raw)
  68. except (TypeError, ValueError):
  69. return None
  70. def normalize_account_fields(body: dict) -> Tuple[bool, Optional[str], Dict[str, Any]]:
  71. """规范化收款账户写入字段,返回 (ok, error, snake_case dict)。"""
  72. if not isinstance(body, dict):
  73. return False, "请求体格式不正确", {}
  74. account_type = _pick_str(body, "accountType", "account_type")
  75. account_name = _pick_str(body, "accountName", "account_name")
  76. account_no = _pick_str(body, "accountNo", "account_no")
  77. bank_name = _pick_str(body, "bankName", "bank_name")
  78. bank_province = _pick_str(body, "bankProvince", "bank_province")
  79. bank_city = _pick_str(body, "bankCity", "bank_city")
  80. bank_branch = _pick_str(body, "bankBranch", "bank_branch")
  81. order_num = _pick_order_num(body)
  82. remark = _pick_str(body, "remark", "remark")
  83. if not account_name:
  84. return False, "户名不能为空", {}
  85. if not account_no:
  86. return False, "账号不能为空", {}
  87. if account_type not in ACCOUNT_TYPES:
  88. return False, "账户类型无效", {}
  89. if account_type == "alipay":
  90. bank_name = ""
  91. bank_province = ""
  92. bank_city = ""
  93. bank_branch = ""
  94. elif not all([bank_name, bank_province, bank_city, bank_branch]):
  95. return False, "银行信息不完整", {}
  96. return True, None, {
  97. "account_type": account_type,
  98. "account_name": account_name,
  99. "account_no": account_no,
  100. "bank_name": bank_name,
  101. "bank_province": bank_province,
  102. "bank_city": bank_city,
  103. "bank_branch": bank_branch,
  104. "order_num": order_num,
  105. "remark": remark,
  106. }
  107. def _is_non_empty_str(value: Any) -> bool:
  108. return isinstance(value, str) and bool(value.strip())
  109. def _is_valid_receive_account_item(item: Any) -> bool:
  110. if not isinstance(item, dict):
  111. return False
  112. item_id = item.get("id")
  113. account_type = item.get("accountType") or item.get("account_type")
  114. account_name = item.get("accountName") or item.get("account_name")
  115. account_no = item.get("accountNo") or item.get("account_no")
  116. if not _is_non_empty_str(item_id):
  117. return False
  118. if not _is_non_empty_str(account_type):
  119. return False
  120. if str(account_type).strip() not in ACCOUNT_TYPES:
  121. return False
  122. if not _is_non_empty_str(account_name):
  123. return False
  124. if not _is_non_empty_str(account_no):
  125. return False
  126. if str(account_type).strip() == "alipay":
  127. return True
  128. for snake, (camel, _) in _BANK_FIELD_ALIASES.items():
  129. val = item.get(camel)
  130. if val is None:
  131. val = item.get(snake)
  132. if not _is_non_empty_str(val):
  133. return False
  134. return True
  135. def _validate_receive_account_rule(rule: dict, data: Dict[str, Any]) -> List[str]:
  136. field = str(rule.get("field") or "").strip()
  137. if not field:
  138. return []
  139. label = _field_label(rule)
  140. required = _is_required(rule)
  141. value = data.get(field)
  142. errors: List[str] = []
  143. if value is None:
  144. if required:
  145. errors.append(f"「{label}」为必填收款账户字段")
  146. return errors
  147. if not isinstance(value, list):
  148. errors.append(f"「{label}」收款账户字段格式不正确,应为账户列表")
  149. return errors
  150. if required and len(value) == 0:
  151. errors.append(f"「{label}」为必填收款账户字段")
  152. return errors
  153. for item in value:
  154. if not _is_valid_receive_account_item(item):
  155. errors.append(f"「{label}」收款账户项格式不正确")
  156. break
  157. return errors
  158. def validate_receive_account_form_values(
  159. schema: Any, form_data: Optional[Dict[str, Any]]
  160. ) -> List[str]:
  161. """校验 form_data 中收款账户控件的值格式,返回中文错误列表(空表示通过)。"""
  162. data = form_data or {}
  163. errors: List[str] = []
  164. for rule in _schema_rules(schema):
  165. if str(rule.get("type") or "") != "receiveAccount":
  166. continue
  167. errors.extend(_validate_receive_account_rule(rule, data))
  168. return errors
  169. def validate_receive_account_ownership(
  170. db: Session,
  171. schema: Any,
  172. form_data: Optional[Dict[str, Any]],
  173. *,
  174. tenant_no: str,
  175. initiator_user_id: str,
  176. ) -> List[str]:
  177. """校验收款账户快照 id 属于发起人。调用前须已通过格式校验。"""
  178. data = form_data or {}
  179. owner_id = (initiator_user_id or "").strip()
  180. for rule in _schema_rules(schema):
  181. if str(rule.get("type") or "") != "receiveAccount":
  182. continue
  183. field = str(rule.get("field") or "").strip()
  184. if not field:
  185. continue
  186. value = data.get(field)
  187. if not isinstance(value, list):
  188. continue
  189. label = _field_label(rule)
  190. for item in value:
  191. if not isinstance(item, dict):
  192. continue
  193. item_id = str(item.get("id") or "").strip()
  194. if not item_id:
  195. continue
  196. row = (
  197. db.query(OaReceiveAccount)
  198. .filter(
  199. OaReceiveAccount.id == item_id,
  200. OaReceiveAccount.tenant_no == tenant_no,
  201. )
  202. .first()
  203. )
  204. if row is None or int(row.del_flag or 0) == 1:
  205. return [f"「{label}」不存在或已删除"]
  206. if (row.create_by or "").strip() != owner_id:
  207. return [f"「{label}」不属于当前用户"]
  208. return []