# -*- coding: utf-8 -*- """ hr-org 组织管理模块后端实现 ================================= 模块: hr-org (组织管理 F01 + 合同公司) 端口: 9280 API 前缀: /hr-org/api/{name}.dspy 实现 23 个端点: 1 org_unit.list 组织分页/条件查询(叠加数据范围) 2 org_unit.get 组织详情 3 org_unit.save 组织新增/修改(含生效时间) 4 org_unit.delete 组织删除(有下级/员工则拒绝) 5 org_unit.deactivate 组织停用(级联停用子树) 6 org_unit.move 组织移动(子树+员工联动) 7 org_unit.tree 组织树(扁平 -> 树) 8 org_unit.effective 组织生效时间维护 9 org_field_def.list 组织自定义字段定义查询 10 org_field_def.get 组织自定义字段定义详情 11 org_field_def.save 组织自定义字段定义新增/修改 12 org_field_def.delete 组织自定义字段定义删除 13 org_field_value.list 组织自定义字段值(EAV)查询 14 org_field_value.get 组织自定义字段值详情 15 org_field_value.save 组织自定义字段值新增/修改 16 org_field_value.delete 组织自定义字段值删除 17 org_unit_change.list 组织时间轴变更记录查询 18 org_unit_change.get 组织时间轴变更记录详情 19 org_unit_change.save 组织时间轴变更记录写入 20 org_contract_company.list 合同公司查询 21 org_contract_company.get 合同公司详情 22 org_contract_company.save 合同公司新增/修改 23 org_contract_company.delete 合同公司删除 另有模块级端点(不计入 23): - org_import.import Excel 批量导入(≥200 行验证) - org_tree.view 按日期 as_of 查看历史组织架构 - org_chart.view 按日期 as_of 查看组织架构图 + 导出图片 依赖基础模块: sqlor(数据访问)、rbac(权限/数据范围)、ahserver(HTTP 框架)、 appbase(配置)、apppublic(工具)。本模块不重复造轮子, 通过 DataStore 接口 对接 sqlor; 未接入真实 sqlor 时可用 standalone 模式(selftest 用 sqlite3)运行。 """ from __future__ import annotations import json import uuid from datetime import date, datetime # --------------------------------------------------------------------------- # 基础模块导入(存在则用, 缺失则降级为 standalone 供自测/单测) # --------------------------------------------------------------------------- try: # pragma: no cover - 真实部署环境 from appPublic.sqlor import Sqlor # noqa: F401 _HAS_SQLOR = True except Exception: # pragma: no cover _HAS_SQLOR = False try: # pragma: no cover from appPublic import jsonutil # noqa: F401 except Exception: # pragma: no cover jsonutil = None # --------------------------------------------------------------------------- # 常量 # --------------------------------------------------------------------------- MODULE = "hr-org" API_PREFIX = "/hr-org/api" TABLE_ORG_UNIT = "org_unit" TABLE_ORG_FIELD_DEF = "org_field_def" TABLE_ORG_FIELD_VALUE = "org_field_value" TABLE_ORG_UNIT_CHANGE = "org_unit_change" TABLE_ORG_CONTRACT_COMPANY = "org_contract_company" # org_unit_change 变更类型字典 CHANGE_TYPES = ("create", "delete", "split", "merge", "modify") # --------------------------------------------------------------------------- # 工具函数 # --------------------------------------------------------------------------- def new_id(prefix: str = "id") -> str: """生成 32 位以内主键。""" return "%s_%s" % (prefix, uuid.uuid4().hex[:24]) def now() -> str: """当前时间字符串(YYYY-MM-DD HH:MM:SS)。""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def today() -> str: return date.today().isoformat() def _dt(v): """宽松地把值转成 date, 无法解析返回 None。""" if v in (None, ""): return None if isinstance(v, datetime): return v.date() if isinstance(v, date): return v for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y%m%d"): try: return datetime.strptime(str(v)[:10], fmt).date() except Exception: continue return None def _ok(data=None, message="ok"): return {"code": 0, "message": message, "data": data} def _err(message, code=1): return {"code": code, "message": message, "data": None} # --------------------------------------------------------------------------- # DataStore 抽象: 对接 sqlor; standalone 模式用 sqlite3 实现 # --------------------------------------------------------------------------- class DataStore: """数据访问接口。真实部署时用 sqlor 实现; 自测用 sqlite3 实现。""" def query(self, sql: str, params=None): raise NotImplementedError def query_one(self, sql: str, params=None): rows = self.query(sql, params) return rows[0] if rows else None def execute(self, sql: str, params=None): raise NotImplementedError def insert(self, table: str, data: dict): cols = list(data.keys()) ph = ", ".join(["?"] * len(cols)) sql = "INSERT INTO %s (%s) VALUES (%s)" % ( table, ", ".join(cols), ph) self.execute(sql, list(data.values())) def update(self, table: str, data: dict, where: str, where_params=None): sets = ", ".join(["%s = ?" % c for c in data.keys()]) sql = "UPDATE %s SET %s WHERE %s" % (table, sets, where) self.execute(sql, list(data.values()) + list(where_params or [])) # 全局 store 引用, 由 register() 注入 _STORE: "DataStore | None" = None def register(store_or_app) -> None: """注册数据访问层。 支持两种形态: * 传入 DataStore 实例(selftest / standalone) * 传入 Sage app 对象, 内部从其上下文取出 sqlor """ global _STORE if isinstance(store_or_app, DataStore): _STORE = store_or_app return # 尝试从 app 上下文取 sqlor ctx = getattr(store_or_app, "ctx", None) or getattr(store_or_app, "context", None) if ctx is None: _STORE = store_or_app return sqlor = getattr(ctx, "sqlor", None) if sqlor is not None and not isinstance(sqlor, DataStore): _STORE = _SqlorStore(sqlor) else: _STORE = sqlor class _SqlorStore(DataStore): """把真实 sqlor 适配到 DataStore 接口。""" def __init__(self, sqlor): self._sqlor = sqlor def query(self, sql, params=None): return self._sqlor.query(sql, params) def execute(self, sql, params=None): return self._sqlor.execute(sql, params) def _store() -> DataStore: if _STORE is None: raise RuntimeError("hr-org DataStore 未注册, 请先调用 register()") return _STORE # --------------------------------------------------------------------------- # 审计 & 数据范围(写操作走审计, 查询叠加数据范围) # --------------------------------------------------------------------------- def _audit(action: str, entity: str, entity_id, before=None, after=None, operator=None) -> None: """写操作审计日志(写入 sys_audit_log, 失败不阻断业务)。""" try: _store().insert("sys_audit_log", { "id": new_id("audit"), "module": MODULE, "action": action, "entity": entity, "entity_id": entity_id, "before_data": json.dumps(before, ensure_ascii=False, default=str) if before is not None else None, "after_data": json.dumps(after, ensure_ascii=False, default=str) if after is not None else None, "operator": operator, "created_at": now(), }) except Exception: # 审计表在纯自测环境可能不存在, 忽略即可 pass def _apply_data_scope(sql: str, where: str, params, operator) -> tuple: """查询叠加数据范围。委托 rbac 计算数据范围条件后拼入 where。 此处提供一个可扩展钩子: 若接入 rbac, 由 rbac.data_scope() 返回附加条件; 未接入时原样返回(不缩小范围)。 """ try: import rbac # noqa: F401 (基础模块) scoped = rbac.data_scope(MODULE, operator) if scoped: where = "(%s) AND (%s)" % (where, scoped) if where else scoped except Exception: pass if where: sql = sql + (" WHERE " + where if " WHERE " not in sql.upper() else " AND " + where) return sql, params # --------------------------------------------------------------------------- # 纯业务逻辑(可独立单测) # --------------------------------------------------------------------------- def build_tree(rows, id_field="id", parent_field="parent_id"): """把扁平组织列表构建为树。""" nodes = {r[id_field]: dict(r, children=[]) for r in rows} roots = [] for r in rows: node = nodes[r[id_field]] pid = r.get(parent_field) if pid and pid != r[id_field] and pid in nodes: nodes[pid]["children"].append(node) else: roots.append(node) def _sort(n): n["children"].sort(key=lambda c: (c.get("sort_no") or 0, c.get("org_code") or "")) for c in n["children"]: _sort(c) for rt in roots: _sort(rt) return roots def filter_orgs_as_of(rows, as_of): """按日期 as_of 过滤历史组织(生效日期 <= as_of 且 失效日期为空或 > as_of)。""" cut = _dt(as_of) or date.today() out = [] for r in rows: eff = _dt(r.get("effective_date")) exp = _dt(r.get("expire_date")) if eff is not None and eff > cut: continue if exp is not None and exp <= cut: continue out.append(r) return out def collect_descendants(rows, org_id, id_field="id", parent_field="parent_id"): """收集某组织及其全部子孙组织 id。""" children = {r.get(parent_field): [] for r in rows} for r in rows: children.setdefault(r.get(parent_field), []).append(r[id_field]) seen = set() stack = [org_id] while stack: cur = stack.pop() if cur in seen: continue seen.add(cur) for c in children.get(cur, []): stack.append(c) return seen def find_cycle(rows, org_id, new_parent_id, id_field="id", parent_field="parent_id"): """若把 org_id 移动到 new_parent_id 会形成环则返回 True。""" parent_map = {r[id_field]: r.get(parent_field) for r in rows} if new_parent_id == org_id: return True cur = new_parent_id seen = set() while cur: if cur == org_id: return True if cur in seen: break seen.add(cur) cur = parent_map.get(cur) return False def validate_import_rows(rows): """Excel 导入数据校验。返回 (errors, valid_rows)。 rows: [{org_code, org_name, parent_code, org_type, effective_date, ...}] """ errors = [] if not rows: errors.append("导入数据为空, 请至少提供一行组织数据") return errors, [] valid = [] seen_codes = set() for idx, r in enumerate(rows, start=2): # 第 1 行为表头 line = "第 %d 行" % idx code = (r.get("org_code") or "").strip() if isinstance(r, dict) else "" name = (r.get("org_name") or "").strip() if isinstance(r, dict) else "" if not code: errors.append("%s: org_code 组织编码必填" % line) if not name: errors.append("%s: org_name 组织名称必填" % line) if code and code in seen_codes: errors.append("%s: org_code=%s 在导入数据中重复" % (line, code)) if code: seen_codes.add(code) eff = _dt(r.get("effective_date")) if isinstance(r, dict) else None exp = _dt(r.get("expire_date")) if isinstance(r, dict) else None if eff and exp and exp <= eff: errors.append("%s: 失效日期不能早于生效日期" % line) if not errors or errors[-1] != "%s: org_code 组织编码必填" % line: pass valid.append(r) return errors, valid # --------------------------------------------------------------------------- # 通用 CRUD 工具 # --------------------------------------------------------------------------- def _page(rows, page, page_size): page = max(1, int(page or 1)) page_size = max(1, int(page_size or 20)) total = len(rows) start = (page - 1) * page_size return { "list": rows[start:start + page_size], "total": total, "page": page, "page_size": page_size, } def _clean(row, table_fields): """只保留表字段, 去掉多余键。table_fields 为字段名集合。""" return {k: v for k, v in row.items() if k in table_fields} # --------------------------------------------------------------------------- # 1-8 org_unit 端点 # --------------------------------------------------------------------------- _ORG_UNIT_FIELDS = { "id", "org_code", "org_name", "parent_id", "org_type", "leader_id", "effective_date", "expire_date", "status", "sort_no", "remark", "created_by", "updated_by", "created_at", "updated_at", } def org_unit_list(store: DataStore, params, operator=None): where, p = [], [] if params.get("org_code"): where.append("org_code LIKE ?") p.append("%" + params["org_code"] + "%") if params.get("org_name"): where.append("org_name LIKE ?") p.append("%" + params["org_name"] + "%") if params.get("parent_id"): where.append("parent_id = ?") p.append(params["parent_id"]) if params.get("status"): where.append("status = ?") p.append(params["status"]) sql = "SELECT * FROM org_unit" sql, p = _apply_data_scope(sql, " AND ".join(where), p, operator) sql += " ORDER BY sort_no ASC, org_code ASC" rows = store.query(sql, p) return _ok(_page(rows, params.get("page"), params.get("page_size"))) def org_unit_get(store: DataStore, params, operator=None): row = store.query_one("SELECT * FROM org_unit WHERE id = ?", [params.get("id")]) if not row: return _err("组织不存在") return _ok(row) def org_unit_save(store: DataStore, params, operator=None): data = _clean(params, _ORG_UNIT_FIELDS) if not data.get("org_code") or not data.get("org_name"): return _err("org_code 与 org_name 必填") if not data.get("id"): # 新增 data["id"] = new_id("org") data["status"] = data.get("status") or "active" data["created_by"] = operator data["created_at"] = now() data["updated_at"] = now() # 编码唯一性校验 dup = store.query_one("SELECT id FROM org_unit WHERE org_code = ?", [data["org_code"]]) if dup: return _err("组织编码 %s 已存在" % data["org_code"]) store.insert(TABLE_ORG_UNIT, data) _record_change(store, "create", data["id"], None, data, operator) _audit("create", "org_unit", data["id"], None, data, operator) return _ok(data) # 修改 old = store.query_one("SELECT * FROM org_unit WHERE id = ?", [data["id"]]) if not old: return _err("组织不存在") data["updated_by"] = operator data["updated_at"] = now() store.update(TABLE_ORG_UNIT, data, "id = ?", [data["id"]]) _record_change(store, "modify", data["id"], old, data, operator) _audit("modify", "org_unit", data["id"], old, data, operator) return _ok(data) def org_unit_delete(store: DataStore, params, operator=None): oid = params.get("id") old = store.query_one("SELECT * FROM org_unit WHERE id = ?", [oid]) if not old: return _err("组织不存在") kids = store.query("SELECT id FROM org_unit WHERE parent_id = ?", [oid]) if kids: return _err("存在下级组织, 不能删除(请先删除或移动下级)") try: emps = store.query( "SELECT id FROM roster_employee WHERE org_id = ?", [oid]) if emps: return _err("组织下存在员工, 不能删除") except Exception: pass store.execute("DELETE FROM org_unit WHERE id = ?", [oid]) _record_change(store, "delete", oid, old, None, operator) _audit("delete", "org_unit", oid, old, None, operator) return _ok({"id": oid}) def org_unit_deactivate(store: DataStore, params, operator=None): oid = params.get("id") rows = store.query("SELECT * FROM org_unit") if not any(r["id"] == oid for r in rows): return _err("组织不存在") ids = collect_descendants(rows, oid) for each in sorted(ids): store.update(TABLE_ORG_UNIT, {"status": "inactive", "updated_at": now(), "updated_by": operator}, "id = ?", [each]) _audit("deactivate", "org_unit", oid, None, {"ids": sorted(ids)}, operator) return _ok({"deactivated": sorted(ids)}) def org_unit_move(store: DataStore, params, operator=None): oid = params.get("id") new_parent = params.get("parent_id") or None rows = store.query("SELECT * FROM org_unit") if not any(r["id"] == oid for r in rows): return _err("组织不存在") if new_parent and not any(r["id"] == new_parent for r in rows): return _err("目标上级组织不存在") if find_cycle(rows, oid, new_parent): return _err("不能移动到自身或其子孙组织下") old = next(r for r in rows if r["id"] == oid) if old.get("parent_id") == new_parent: return _ok({"moved": [oid]}) subtree = collect_descendants(rows, oid) store.update(TABLE_ORG_UNIT, {"parent_id": new_parent, "updated_at": now(), "updated_by": operator}, "id = ?", [oid]) # 员工联动: 子树内员工的组织路径同步更新(存在相关字段则更新, 否则忽略) for sid in subtree: try: store.execute( "UPDATE roster_employee SET org_path = ? WHERE org_id = ?", [new_parent or "", sid]) except Exception: pass _record_change(store, "modify", oid, {"parent_id": old.get("parent_id")}, {"parent_id": new_parent, "subtree": sorted(subtree)}, operator) _audit("move", "org_unit", oid, {"parent_id": old.get("parent_id")}, {"parent_id": new_parent}, operator) return _ok({"moved": sorted(subtree), "new_parent": new_parent}) def org_unit_tree(store: DataStore, params, operator=None): rows = store.query("SELECT * FROM org_unit ORDER BY sort_no ASC, org_code ASC") if params.get("as_of"): rows = filter_orgs_as_of(rows, params["as_of"]) if params.get("only_active"): rows = [r for r in rows if r.get("status") != "inactive"] return _ok(build_tree(rows)) def org_unit_effective(store: DataStore, params, operator=None): oid = params.get("id") old = store.query_one("SELECT * FROM org_unit WHERE id = ?", [oid]) if not old: return _err("组织不存在") eff = _dt(params.get("effective_date")) exp = _dt(params.get("expire_date")) if eff and exp and exp <= eff: return _err("失效日期不能早于生效日期") data = {"effective_date": eff.isoformat() if eff else old.get("effective_date"), "expire_date": exp.isoformat() if exp else (params.get("expire_date") is None and old.get("expire_date")) or None, "updated_at": now(), "updated_by": operator} if params.get("expire_date") is None: data["expire_date"] = None store.update(TABLE_ORG_UNIT, data, "id = ?", [oid]) _record_change(store, "modify", oid, old, data, operator) _audit("effective", "org_unit", oid, old, data, operator) return _ok(data) # --------------------------------------------------------------------------- # 9-12 org_field_def (组织自定义字段定义) # --------------------------------------------------------------------------- _ORG_FIELD_DEF_FIELDS = { "id", "field_code", "field_name", "field_type", "entity_type", "options", "required", "sort_no", "status", "created_by", "updated_by", "created_at", "updated_at", } def org_field_def_list(store: DataStore, params, operator=None): rows = store.query("SELECT * FROM org_field_def ORDER BY sort_no ASC") return _ok(_page(rows, params.get("page"), params.get("page_size"))) def org_field_def_get(store: DataStore, params, operator=None): row = store.query_one("SELECT * FROM org_field_def WHERE id = ?", [params.get("id")]) return _ok(row) if row else _err("字段定义不存在") def org_field_def_save(store: DataStore, params, operator=None): data = _clean(params, _ORG_FIELD_DEF_FIELDS) if not data.get("field_code") or not data.get("field_name"): return _err("field_code 与 field_name 必填") if not data.get("id"): data["id"] = new_id("ofd") data["status"] = data.get("status") or "active" data["created_at"] = now() data["updated_at"] = now() store.insert(TABLE_ORG_FIELD_DEF, data) _audit("create", "org_field_def", data["id"], None, data, operator) else: old = store.query_one("SELECT * FROM org_field_def WHERE id = ?", [data["id"]]) if not old: return _err("字段定义不存在") data["updated_at"] = now() store.update(TABLE_ORG_FIELD_DEF, data, "id = ?", [data["id"]]) _audit("modify", "org_field_def", data["id"], old, data, operator) return _ok(data) def org_field_def_delete(store: DataStore, params, operator=None): fid = params.get("id") old = store.query_one("SELECT * FROM org_field_def WHERE id = ?", [fid]) if not old: return _err("字段定义不存在") store.execute("DELETE FROM org_field_def WHERE id = ?", [fid]) _audit("delete", "org_field_def", fid, old, None, operator) return _ok({"id": fid}) # --------------------------------------------------------------------------- # 13-16 org_field_value (组织自定义字段值, EAV) # --------------------------------------------------------------------------- _ORG_FIELD_VALUE_FIELDS = { "id", "org_id", "field_def_id", "field_value", "created_by", "updated_by", "created_at", "updated_at", } def org_field_value_list(store: DataStore, params, operator=None): where, p = [], [] if params.get("org_id"): where.append("org_id = ?") p.append(params["org_id"]) if params.get("field_def_id"): where.append("field_def_id = ?") p.append(params["field_def_id"]) sql = "SELECT * FROM org_field_value" if where: sql += " WHERE " + " AND ".join(where) rows = store.query(sql, p) return _ok(_page(rows, params.get("page"), params.get("page_size"))) def org_field_value_get(store: DataStore, params, operator=None): row = store.query_one("SELECT * FROM org_field_value WHERE id = ?", [params.get("id")]) return _ok(row) if row else _err("字段值不存在") def org_field_value_save(store: DataStore, params, operator=None): # 缺参校验(验收点) if not params.get("org_id"): return _err("org_id 缺参: 必须指定所属组织") if not params.get("field_def_id"): return _err("field_def_id 缺参: 必须指定字段定义") data = _clean(params, _ORG_FIELD_VALUE_FIELDS) # 外键存在性校验 org = store.query_one("SELECT id FROM org_unit WHERE id = ?", [data["org_id"]]) if not org: return _err("组织 %s 不存在" % data["org_id"]) fd = store.query_one("SELECT id FROM org_field_def WHERE id = ?", [data["field_def_id"]]) if not fd: return _err("字段定义 %s 不存在" % data["field_def_id"]) if not data.get("id"): data["id"] = new_id("ofv") data["created_at"] = now() data["updated_at"] = now() store.insert(TABLE_ORG_FIELD_VALUE, data) _audit("create", "org_field_value", data["id"], None, data, operator) else: old = store.query_one("SELECT * FROM org_field_value WHERE id = ?", [data["id"]]) if not old: return _err("字段值不存在") data["updated_at"] = now() store.update(TABLE_ORG_FIELD_VALUE, data, "id = ?", [data["id"]]) _audit("modify", "org_field_value", data["id"], old, data, operator) return _ok(data) def org_field_value_delete(store: DataStore, params, operator=None): vid = params.get("id") old = store.query_one("SELECT * FROM org_field_value WHERE id = ?", [vid]) if not old: return _err("字段值不存在") store.execute("DELETE FROM org_field_value WHERE id = ?", [vid]) _audit("delete", "org_field_value", vid, old, None, operator) return _ok({"id": vid}) # --------------------------------------------------------------------------- # 17-19 org_unit_change (时间轴变更记录) # --------------------------------------------------------------------------- def _record_change(store: DataStore, change_type, org_id, before, after, operator): """统一写时间轴变更记录(供 org_unit 各写操作调用)。""" try: store.insert(TABLE_ORG_UNIT_CHANGE, { "id": new_id("ouc"), "org_id": org_id, "change_type": change_type, "change_date": today(), "before_data": json.dumps(before, ensure_ascii=False, default=str) if before is not None else None, "after_data": json.dumps(after, ensure_ascii=False, default=str) if after is not None else None, "operator": operator, "created_at": now(), }) except Exception: pass def org_unit_change_list(store: DataStore, params, operator=None): where, p = [], [] if params.get("org_id"): where.append("org_id = ?") p.append(params["org_id"]) if params.get("change_type"): where.append("change_type = ?") p.append(params["change_type"]) if params.get("from_date"): where.append("change_date >= ?") p.append(params["from_date"]) if params.get("to_date"): where.append("change_date <= ?") p.append(params["to_date"]) sql = "SELECT * FROM org_unit_change" if where: sql += " WHERE " + " AND ".join(where) sql += " ORDER BY change_date DESC, created_at DESC" rows = store.query(sql, p) return _ok(_page(rows, params.get("page"), params.get("page_size"))) def org_unit_change_get(store: DataStore, params, operator=None): row = store.query_one("SELECT * FROM org_unit_change WHERE id = ?", [params.get("id")]) return _ok(row) if row else _err("变更记录不存在") def org_unit_change_save(store: DataStore, params, operator=None): ct = params.get("change_type") if ct not in CHANGE_TYPES: return _err("change_type 非法, 允许: %s" % ", ".join(CHANGE_TYPES)) if not params.get("org_id"): return _err("org_id 缺参") rec = { "id": new_id("ouc"), "org_id": params["org_id"], "change_type": ct, "change_date": params.get("change_date") or today(), "before_data": params.get("before_data"), "after_data": params.get("after_data"), "operator": operator, "created_at": now(), } store.insert(TABLE_ORG_UNIT_CHANGE, rec) _audit("create", "org_unit_change", rec["id"], None, rec, operator) return _ok(rec) # --------------------------------------------------------------------------- # 20-23 org_contract_company (合同公司) # --------------------------------------------------------------------------- _ORG_CONTRACT_COMPANY_FIELDS = { "id", "company_code", "company_name", "credit_code", "legal_person", "contact_info", "status", "created_by", "updated_by", "created_at", "updated_at", } def org_contract_company_list(store: DataStore, params, operator=None): where, p = [], [] if params.get("company_code"): where.append("company_code LIKE ?") p.append("%" + params["company_code"] + "%") if params.get("company_name"): where.append("company_name LIKE ?") p.append("%" + params["company_name"] + "%") if params.get("status"): where.append("status = ?") p.append(params["status"]) sql = "SELECT * FROM org_contract_company" if where: sql += " WHERE " + " AND ".join(where) sql += " ORDER BY company_code ASC" rows = store.query(sql, p) return _ok(_page(rows, params.get("page"), params.get("page_size"))) def org_contract_company_get(store: DataStore, params, operator=None): row = store.query_one("SELECT * FROM org_contract_company WHERE id = ?", [params.get("id")]) return _ok(row) if row else _err("合同公司不存在") def _valid_credit_code(code): """统一社会信用代码基本校验: 18 位, 数字或大写字母。""" if not code: return True if len(code) != 18: return False return all(c.isdigit() or ("A" <= c <= "Z") for c in code) def org_contract_company_save(store: DataStore, params, operator=None): data = _clean(params, _ORG_CONTRACT_COMPANY_FIELDS) if not data.get("company_code") or not data.get("company_name"): return _err("company_code 与 company_name 必填") if data.get("credit_code") and not _valid_credit_code(data["credit_code"]): return _err("统一社会信用代码须为 18 位数字或大写字母") if not data.get("id"): data["id"] = new_id("occ") data["status"] = data.get("status") or "active" data["created_at"] = now() data["updated_at"] = now() dup = store.query_one( "SELECT id FROM org_contract_company WHERE company_code = ?", [data["company_code"]]) if dup: return _err("公司编码 %s 已存在" % data["company_code"]) store.insert(TABLE_ORG_CONTRACT_COMPANY, data) _audit("create", "org_contract_company", data["id"], None, data, operator) else: old = store.query_one( "SELECT * FROM org_contract_company WHERE id = ?", [data["id"]]) if not old: return _err("合同公司不存在") data["updated_at"] = now() store.update(TABLE_ORG_CONTRACT_COMPANY, data, "id = ?", [data["id"]]) _audit("modify", "org_contract_company", data["id"], old, data, operator) return _ok(data) def org_contract_company_delete(store: DataStore, params, operator=None): cid = params.get("id") old = store.query_one("SELECT * FROM org_contract_company WHERE id = ?", [cid]) if not old: return _err("合同公司不存在") store.execute("DELETE FROM org_contract_company WHERE id = ?", [cid]) _audit("delete", "org_contract_company", cid, old, None, operator) return _ok({"id": cid}) # --------------------------------------------------------------------------- # 模块级端点: org_import / org_tree / org_chart # --------------------------------------------------------------------------- def org_import_import(store: DataStore, params, operator=None): """Excel 批量导入(≥200 行验证)。""" rows = params.get("rows") if not isinstance(rows, list): return _err("rows 参数必须为数组") errors, valid = validate_import_rows(rows) if errors: return _err("; ".join(errors)) if len(valid) > 200 and not params.get("confirmed"): # ≥200 行需二次确认(防误操作) return {"code": 2, "message": "导入数据超过 200 行, 请确认(confirmed=true)后重试", "data": {"rows": len(valid)}} ok_ids = [] for r in valid: r = dict(r) r["id"] = new_id("org") r["status"] = r.get("status") or "active" r["created_at"] = now() r["updated_at"] = now() r["created_by"] = operator r.pop("parent_code", None) dup = store.query_one("SELECT id FROM org_unit WHERE org_code = ?", [r["org_code"]]) if dup: continue store.insert(TABLE_ORG_UNIT, r) _record_change(store, "create", r["id"], None, r, operator) ok_ids.append(r["id"]) _audit("import", "org_unit", None, None, {"imported": ok_ids}, operator) return _ok({"imported": len(ok_ids), "ids": ok_ids}) def org_tree_view(store: DataStore, params, operator=None): rows = store.query("SELECT * FROM org_unit ORDER BY sort_no ASC, org_code ASC") rows = filter_orgs_as_of(rows, params.get("as_of")) if params.get("only_active"): rows = [r for r in rows if r.get("status") != "inactive"] tree = build_tree(rows) return _ok({"as_of": params.get("as_of") or today(), "tree": tree}) def org_chart_view(store: DataStore, params, operator=None): rows = store.query("SELECT * FROM org_unit ORDER BY sort_no ASC, org_code ASC") rows = filter_orgs_as_of(rows, params.get("as_of")) tree = build_tree(rows) export = params.get("export_image") in (True, "true", "1", 1) payload = {"as_of": params.get("as_of") or today(), "chart": tree} if export: # 导出图片: 返回图数据(前端/渲染服务据此生成 PNG) payload["image"] = { "type": "org_chart", "data": tree, "format": params.get("format") or "png", } return _ok(payload) # --------------------------------------------------------------------------- # 端点注册表 # --------------------------------------------------------------------------- _HANDLERS = { "org_unit.list": org_unit_list, "org_unit.get": org_unit_get, "org_unit.save": org_unit_save, "org_unit.delete": org_unit_delete, "org_unit.deactivate": org_unit_deactivate, "org_unit.move": org_unit_move, "org_unit.tree": org_unit_tree, "org_unit.effective": org_unit_effective, "org_field_def.list": org_field_def_list, "org_field_def.get": org_field_def_get, "org_field_def.save": org_field_def_save, "org_field_def.delete": org_field_def_delete, "org_field_value.list": org_field_value_list, "org_field_value.get": org_field_value_get, "org_field_value.save": org_field_value_save, "org_field_value.delete": org_field_value_delete, "org_unit_change.list": org_unit_change_list, "org_unit_change.get": org_unit_change_get, "org_unit_change.save": org_unit_change_save, "org_contract_company.list": org_contract_company_list, "org_contract_company.get": org_contract_company_get, "org_contract_company.save": org_contract_company_save, "org_contract_company.delete": org_contract_company_delete, "org_import.import": org_import_import, "org_tree.view": org_tree_view, "org_chart.view": org_chart_view, } # 23 个核心端点(验收清单) CORE_ENDPOINTS = [k for k in _HANDLERS if k in { "org_unit.list", "org_unit.get", "org_unit.save", "org_unit.delete", "org_unit.deactivate", "org_unit.move", "org_unit.tree", "org_unit.effective", "org_field_def.list", "org_field_def.get", "org_field_def.save", "org_field_def.delete", "org_field_value.list", "org_field_value.get", "org_field_value.save", "org_field_value.delete", "org_unit_change.list", "org_unit_change.get", "org_unit_change.save", "org_contract_company.list", "org_contract_company.get", "org_contract_company.save", "org_contract_company.delete", }] def endpoints(): """返回全部端点名(含 3 个模块级端点)。""" return sorted(_HANDLERS.keys()) def core_endpoints(): return sorted(CORE_ENDPOINTS) def dispatch(name: str, params=None, operator=None): """按端点名分发。params 为请求参数 dict。""" handler = _HANDLERS.get(name) if handler is None: return _err("未知端点: %s" % name) try: return handler(_store(), params or {}, operator) except RuntimeError as e: return _err(str(e)) # --------------------------------------------------------------------------- # 真实部署接入: ahserver 注册路由 # for name, fn in _HANDLERS.items(): # ahserver.route("%s/%s.dspy" % (API_PREFIX, name), fn) # --------------------------------------------------------------------------- def bind_ahserver(ahserver): """把全部端点注册到 ahserver。ahserver 提供 route(path, handler)。""" for name in _HANDLERS: path = "%s/%s.dspy" % (API_PREFIX, name) ahserver.route(path, lambda params, n=name, o=None: dispatch(n, params, o)) return list(_HANDLERS.keys())