hr-system/apps/hr_org.py

943 lines
36 KiB
Python

# -*- 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())