Просмотр исходного кода

feat(backend): validate OA money field values on instance start

Reject negatives, non-finite values, and missing required money controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 месяцев назад
Родитель
Сommit
55e4134050

+ 5 - 0
backend/app/services/oa_engine.py

@@ -1551,11 +1551,16 @@ def start_instance(
         raise OaEngineError(f"公式计算失败: {e}") from e
 
     from app.services.oa_dept_select import validate_dept_form_values
+    from app.services.oa_money_field import validate_money_form_values
 
     dept_errs = validate_dept_form_values(schema, data)
     if dept_errs:
         raise OaEngineError(dept_errs[0])
 
+    money_errs = validate_money_form_values(schema, data)
+    if money_errs:
+        raise OaEngineError(money_errs[0])
+
     user = (
         db.query(SysUser)
         .filter(SysUser.id == initiator_user_id, SysUser.del_flag == 0)

+ 119 - 0
backend/app/services/oa_money_field.py

@@ -0,0 +1,119 @@
+"""OA 表单设计器:金额控件 form_data 校验"""
+from __future__ import annotations
+
+import math
+from typing import Any, Dict, List, Optional, Tuple
+
+MAX_MONEY = 999999999999.99
+
+
+def _walk_rules(rules: Any, out: List[dict]) -> None:
+    if not isinstance(rules, list):
+        return
+    for r in rules:
+        if not isinstance(r, dict):
+            continue
+        out.append(r)
+        children = r.get("children") or (r.get("props") or {}).get("rule") or []
+        if isinstance(children, list):
+            _walk_rules(children, out)
+
+
+def _schema_rules(schema: Any) -> List[dict]:
+    if isinstance(schema, list):
+        out: List[dict] = []
+        _walk_rules(schema, out)
+        return out
+    if not isinstance(schema, dict):
+        return []
+    out: List[dict] = []
+    _walk_rules(schema.get("rule") or schema.get("rules") or [], out)
+    return out
+
+
+def _rule_props(rule: dict) -> dict:
+    props = rule.get("props")
+    return props if isinstance(props, dict) else {}
+
+
+def _field_label(rule: dict) -> str:
+    return str(rule.get("title") or rule.get("label") or rule.get("field") or "金额")
+
+
+def _is_required(rule: dict) -> bool:
+    props = _rule_props(rule)
+    if rule.get("$required") is True:
+        return True
+    if rule.get("required") is True:
+        return True
+    if props.get("required") is True:
+        return True
+    return False
+
+
+def _is_empty(value: Any) -> bool:
+    return value is None or value == ""
+
+
+def _parse_money(value: Any) -> Tuple[Optional[float], Optional[str]]:
+    """解析金额值。成功返回 (number, None);失败返回 (None, error_key)。"""
+    if _is_empty(value):
+        return None, None
+    if isinstance(value, bool):
+        return None, "invalid"
+    if isinstance(value, (int, float)):
+        n = float(value)
+    elif isinstance(value, str):
+        text = value.replace(",", "").strip()
+        if not text:
+            return None, None
+        try:
+            n = float(text)
+        except ValueError:
+            return None, "invalid"
+    else:
+        return None, "invalid"
+
+    if not math.isfinite(n):
+        return None, "invalid"
+    if n < 0:
+        return None, "negative"
+    if n > MAX_MONEY:
+        return None, "too_large"
+    return n, None
+
+
+def _validate_money_rule(rule: dict, data: Dict[str, Any]) -> List[str]:
+    field = str(rule.get("field") or "").strip()
+    if not field:
+        return []
+
+    label = _field_label(rule)
+    required = _is_required(rule)
+    value = data.get(field)
+    errors: List[str] = []
+
+    if _is_empty(value):
+        if required:
+            errors.append(f"「{label}」为必填金额字段")
+        return errors
+
+    _, err_key = _parse_money(value)
+    if err_key == "negative":
+        errors.append(f"「{label}」金额不能为负数")
+    elif err_key == "too_large":
+        errors.append("金额过大")
+    elif err_key == "invalid":
+        errors.append(f"「{label}」金额格式不正确")
+    return errors
+
+
+def validate_money_form_values(schema: Any, form_data: Optional[Dict[str, Any]]) -> List[str]:
+    """校验 form_data 中金额控件的值格式,返回中文错误列表(空表示通过)。"""
+    data = form_data or {}
+    errors: List[str] = []
+    for rule in _schema_rules(schema):
+        if str(rule.get("type") or "") != "money":
+            continue
+        errors.extend(_validate_money_rule(rule, data))
+    return errors

+ 47 - 0
backend/tests/test_oa_money_field.py

@@ -0,0 +1,47 @@
+import unittest
+
+from app.services.oa_money_field import validate_money_form_values
+
+
+class TestValidateMoneyFormValues(unittest.TestCase):
+    def test_ok_number(self):
+        schema = {"rule": [{"type": "money", "field": "amt", "title": "金额", "$required": True}]}
+        self.assertEqual(validate_money_form_values(schema, {"amt": 1234.56}), [])
+
+    def test_ok_int(self):
+        schema = {"rule": [{"type": "money", "field": "amt"}]}
+        self.assertEqual(validate_money_form_values(schema, {"amt": 10}), [])
+
+    def test_ok_null_optional(self):
+        schema = {"rule": [{"type": "money", "field": "amt", "$required": False}]}
+        self.assertEqual(validate_money_form_values(schema, {"amt": None}), [])
+        self.assertEqual(validate_money_form_values(schema, {}), [])
+
+    def test_required_missing(self):
+        schema = {
+            "rule": [{"type": "money", "field": "amt", "title": "报销金额", "$required": True}]
+        }
+        errs = validate_money_form_values(schema, {})
+        self.assertTrue(any("报销金额" in e for e in errs))
+
+    def test_negative_rejected(self):
+        schema = {"rule": [{"type": "money", "field": "amt"}]}
+        errs = validate_money_form_values(schema, {"amt": -1})
+        self.assertTrue(len(errs) >= 1)
+
+    def test_string_number_accepted(self):
+        schema = {"rule": [{"type": "money", "field": "amt"}]}
+        self.assertEqual(validate_money_form_values(schema, {"amt": "12.5"}), [])
+
+    def test_bool_rejected(self):
+        schema = {"rule": [{"type": "money", "field": "amt"}]}
+        errs = validate_money_form_values(schema, {"amt": True})
+        self.assertTrue(len(errs) >= 1)
+
+    def test_ignore_other_types(self):
+        schema = {"rule": [{"type": "input", "field": "x", "$required": True}]}
+        self.assertEqual(validate_money_form_values(schema, {}), [])
+
+
+if __name__ == "__main__":
+    unittest.main()