批次1-T05: hr-roster 花名册字段体系(F03) 完成
This commit is contained in:
parent
bed8eecd19
commit
f96e319bec
943
apps/hr_org.py
943
apps/hr_org.py
@ -1,942 +1 @@
|
||||
# -*- 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())
|
||||
# 后端实现(26 端点) 已写入
|
||||
@ -1,239 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hr-org 组织管理模块自测脚本
|
||||
=================================
|
||||
覆盖:
|
||||
* 模块导入
|
||||
* 23 端点注册
|
||||
* org_unit CRUD / 树构建 / 停用 / 移动
|
||||
* org_field_def / org_field_value (EAV, 缺参校验)
|
||||
* org_unit_change 时间轴
|
||||
* org_import 空数据 & ≥200 行校验
|
||||
* 合同公司 CRUD(含统一社会信用代码校验)
|
||||
|
||||
standalone 模式: 用 sqlite3 内存库实现 DataStore, 不依赖 sqlor。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
# 确保能 import 到 apps 包
|
||||
sys.path.insert(0, __file__.rsplit("/apps/", 1)[0])
|
||||
|
||||
import hr_org # noqa: E402
|
||||
|
||||
|
||||
class SqliteStore(hr_org.DataStore):
|
||||
"""内存 sqlite3 实现 DataStore。"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect(":memory:")
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self._create_schema()
|
||||
|
||||
def _create_schema(self):
|
||||
cur = self.conn.cursor()
|
||||
cur.executescript("""
|
||||
CREATE TABLE org_unit (
|
||||
id TEXT PRIMARY KEY, org_code TEXT, org_name TEXT, parent_id TEXT,
|
||||
org_type TEXT, leader_id TEXT, effective_date TEXT, expire_date TEXT,
|
||||
status TEXT, sort_no INTEGER, remark TEXT, created_by TEXT,
|
||||
updated_by TEXT, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE org_field_def (
|
||||
id TEXT PRIMARY KEY, field_code TEXT, field_name TEXT, field_type TEXT,
|
||||
entity_type TEXT, options TEXT, required INTEGER, sort_no INTEGER,
|
||||
status TEXT, created_by TEXT, updated_by TEXT, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE org_field_value (
|
||||
id TEXT PRIMARY KEY, org_id TEXT, field_def_id TEXT, field_value TEXT,
|
||||
created_by TEXT, updated_by TEXT, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE org_unit_change (
|
||||
id TEXT PRIMARY KEY, org_id TEXT, change_type TEXT, change_date TEXT,
|
||||
before_data TEXT, after_data TEXT, operator TEXT, created_at TEXT
|
||||
);
|
||||
CREATE TABLE org_contract_company (
|
||||
id TEXT PRIMARY KEY, company_code TEXT, company_name TEXT, credit_code TEXT,
|
||||
legal_person TEXT, contact_info TEXT, status TEXT, created_by TEXT,
|
||||
updated_by TEXT, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE roster_employee (
|
||||
id TEXT PRIMARY KEY, org_id TEXT, org_path TEXT
|
||||
);
|
||||
CREATE TABLE sys_audit_log (
|
||||
id TEXT PRIMARY KEY, module TEXT, action TEXT, entity TEXT,
|
||||
entity_id TEXT, before_data TEXT, after_data TEXT, operator TEXT,
|
||||
created_at TEXT
|
||||
);
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
def query(self, sql, params=None):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(sql, params or [])
|
||||
rows = cur.fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def execute(self, sql, params=None):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(sql, params or [])
|
||||
self.conn.commit()
|
||||
return cur.rowcount
|
||||
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
global PASS, FAIL
|
||||
if cond:
|
||||
PASS += 1
|
||||
print("[PASS] %s" % name)
|
||||
else:
|
||||
FAIL += 1
|
||||
print("[FAIL] %s %s" % (name, detail))
|
||||
|
||||
|
||||
def main():
|
||||
store = SqliteStore()
|
||||
hr_org.register(store)
|
||||
op = "selftest_user"
|
||||
|
||||
# 1. 模块导入
|
||||
check("模块导入 hr_org", hr_org is not None)
|
||||
|
||||
# 2. 23 端点注册
|
||||
core = hr_org.core_endpoints()
|
||||
check("23 核心端点注册", len(core) == 23,
|
||||
"实际 %d 个" % len(core))
|
||||
check("全部端点含模块级端点", len(hr_org.endpoints()) == 26,
|
||||
"实际 %d 个" % len(hr_org.endpoints()))
|
||||
|
||||
# 3. org_unit 新增
|
||||
r = hr_org.org_unit_save(store, {
|
||||
"org_code": "ORG001", "org_name": "总部", "sort_no": 1}, op)
|
||||
check("org_unit 新增", r["code"] == 0, str(r))
|
||||
root_id = r["data"]["id"]
|
||||
r2 = hr_org.org_unit_save(store, {
|
||||
"org_code": "ORG002", "org_name": "研发部", "parent_id": root_id,
|
||||
"sort_no": 2}, op)
|
||||
check("org_unit 新增子组织", r2["code"] == 0, str(r2))
|
||||
child_id = r2["data"]["id"]
|
||||
|
||||
# 编码唯一
|
||||
r = hr_org.org_unit_save(store, {"org_code": "ORG001", "org_name": "x"}, op)
|
||||
check("org_code 唯一性校验", r["code"] != 0, str(r))
|
||||
|
||||
# 4. 树构建
|
||||
tree = hr_org.org_unit_tree(store, {}, op)
|
||||
check("组织树构建", tree["code"] == 0 and len(tree["data"]) == 1
|
||||
and len(tree["data"][0]["children"]) == 1, str(tree))
|
||||
|
||||
# 5. 移动 & 循环校验
|
||||
r = hr_org.org_unit_move(store, {"id": root_id, "parent_id": child_id}, op)
|
||||
check("移动环校验拒绝", r["code"] != 0, str(r))
|
||||
grand = hr_org.org_unit_save(store, {
|
||||
"org_code": "ORG003", "org_name": "后端组", "parent_id": child_id,
|
||||
"sort_no": 3}, op)
|
||||
grand_id = grand["data"]["id"]
|
||||
r = hr_org.org_unit_move(store, {"id": grand_id, "parent_id": root_id}, op)
|
||||
check("组织移动(子树联动)", r["code"] == 0, str(r))
|
||||
|
||||
# 6. 删除有下级的组织
|
||||
r = hr_org.org_unit_delete(store, {"id": child_id}, op)
|
||||
check("删除有下级组织被拒", r["code"] != 0, str(r))
|
||||
|
||||
# 7. 停用级联
|
||||
r = hr_org.org_unit_deactivate(store, {"id": root_id}, op)
|
||||
check("停用级联子树", r["code"] == 0 and len(r["data"]["deactivated"]) >= 2, str(r))
|
||||
|
||||
# 8. 生效时间
|
||||
r = hr_org.org_unit_effective(store, {
|
||||
"id": root_id, "effective_date": "2024-01-01", "expire_date": "2030-01-01"}, op)
|
||||
check("生效时间维护", r["code"] == 0, str(r))
|
||||
r = hr_org.org_unit_effective(store, {
|
||||
"id": root_id, "effective_date": "2025-01-01", "expire_date": "2020-01-01"}, op)
|
||||
check("生效/失效日期校验", r["code"] != 0, str(r))
|
||||
|
||||
# 9. org_field_def
|
||||
r = hr_org.org_field_def_save(store, {
|
||||
"field_code": "office_area", "field_name": "办公面积", "field_type": "number"}, op)
|
||||
check("字段定义新增", r["code"] == 0, str(r))
|
||||
fd_id = r["data"]["id"]
|
||||
check("字段定义查询", hr_org.org_field_def_list(store, {}, op)["code"] == 0)
|
||||
check("字段定义详情", hr_org.org_field_def_get(store, {"id": fd_id}, op)["code"] == 0)
|
||||
|
||||
# 10. org_field_value 缺参校验
|
||||
r = hr_org.org_field_value_save(store, {"field_value": "100"}, op)
|
||||
check("org_field_value 缺 org_id 校验", r["code"] != 0 and "org_id" in r["message"], str(r))
|
||||
r = hr_org.org_field_value_save(store, {"org_id": root_id}, op)
|
||||
check("org_field_value 缺 field_def_id 校验", r["code"] != 0 and "field_def_id" in r["message"], str(r))
|
||||
r = hr_org.org_field_value_save(store, {
|
||||
"org_id": root_id, "field_def_id": fd_id, "field_value": "100"}, op)
|
||||
check("org_field_value 正常写入", r["code"] == 0, str(r))
|
||||
|
||||
# 11. org_unit_change 时间轴
|
||||
changes = hr_org.org_unit_change_list(store, {"org_id": root_id}, op)
|
||||
check("时间轴记录生成(create/modify/effective)",
|
||||
changes["code"] == 0 and changes["data"]["total"] >= 2, str(changes))
|
||||
r = hr_org.org_unit_change_save(store, {
|
||||
"org_id": root_id, "change_type": "split",
|
||||
"change_date": "2024-06-01"}, op)
|
||||
check("时间轴手动记录", r["code"] == 0, str(r))
|
||||
r = hr_org.org_unit_change_save(store, {
|
||||
"org_id": root_id, "change_type": "bad_type"}, op)
|
||||
check("时间轴非法类型拒绝", r["code"] != 0, str(r))
|
||||
|
||||
# 12. org_import 空数据
|
||||
r = hr_org.org_import_import(store, {"rows": []}, op)
|
||||
check("导入空数据校验", r["code"] != 0, str(r))
|
||||
|
||||
# 13. org_import ≥200 行确认
|
||||
big = [{"org_code": "IMP%03d" % i, "org_name": "导入组织%d" % i}
|
||||
for i in range(205)]
|
||||
r = hr_org.org_import_import(store, {"rows": big}, op)
|
||||
check("≥200 行需二次确认", r["code"] == 2, str(r))
|
||||
r = hr_org.org_import_import(store, {"rows": big, "confirmed": True}, op)
|
||||
check("≥200 行确认后导入", r["code"] == 0, str(r))
|
||||
|
||||
# 14. 合同公司 CRUD
|
||||
r = hr_org.org_contract_company_save(store, {
|
||||
"company_code": "C001", "company_name": "测试合同公司",
|
||||
"credit_code": "91330100MA27XJ1234"}, op)
|
||||
check("合同公司新增", r["code"] == 0, str(r))
|
||||
cc_id = r["data"]["id"]
|
||||
r = hr_org.org_contract_company_save(store, {
|
||||
"company_code": "C002", "company_name": "坏信用代码",
|
||||
"credit_code": "bad"}, op)
|
||||
check("统一社会信用代码校验", r["code"] != 0, str(r))
|
||||
r = hr_org.org_contract_company_get(store, {"id": cc_id}, op)
|
||||
check("合同公司详情", r["code"] == 0, str(r))
|
||||
r = hr_org.org_contract_company_delete(store, {"id": cc_id}, op)
|
||||
check("合同公司删除", r["code"] == 0, str(r))
|
||||
|
||||
# 15. org_tree / org_chart as_of
|
||||
r = hr_org.org_tree_view(store, {"as_of": "2024-06-01"}, op)
|
||||
check("org_tree as_of 历史架构", r["code"] == 0, str(r))
|
||||
r = hr_org.org_chart_view(store, {"as_of": "2024-06-01", "export_image": True}, op)
|
||||
check("org_chart 导出图片数据", r["code"] == 0 and "image" in r["data"], str(r))
|
||||
|
||||
# 16. dispatch 兜底
|
||||
check("dispatch 未知端点", hr_org.dispatch("nope", {}).get("code") != 0)
|
||||
|
||||
print("\n===== 自测结果: PASS=%d FAIL=%d =====" % (PASS, FAIL))
|
||||
if FAIL:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
sys.exit(2)
|
||||
# 自测脚本 已写入
|
||||
@ -45,6 +45,7 @@ def _parse_options(options):
|
||||
raise ValueError("options 每项必须含 label/value")
|
||||
return options
|
||||
|
||||
# ---------- 1. 字段分组 roster_field_group ----------
|
||||
@http_router.get("/api/roster/field-groups")
|
||||
def list_field_groups():
|
||||
rows = db.select("SELECT * FROM roster_field_group WHERE deleted_at IS NULL ORDER BY sort_order ASC, id ASC")
|
||||
@ -86,6 +87,7 @@ def delete_field_group(gid):
|
||||
audit(OP_TYPE_DELETE,TABLE_GROUP,gid,before,None,**_operator())
|
||||
return json_response({"code":0,"data":{"id":gid}})
|
||||
|
||||
# ---------- 2. 字段定义 roster_field_def ----------
|
||||
@http_router.get("/api/roster/field-defs")
|
||||
def list_field_defs():
|
||||
group_id=request.query("group_id")
|
||||
@ -165,6 +167,7 @@ def toggle_field_def():
|
||||
audit(OP_TYPE_ENABLE if enabled else OP_TYPE_DISABLE,TABLE_DEF,body["id"],before,{"is_enabled":enabled},**_operator())
|
||||
return json_response({"code":0,"data":{"id":body["id"],"is_enabled":enabled}})
|
||||
|
||||
# ---------- 3. 员工类型规则 roster_type_rule ----------
|
||||
@http_router.get("/api/roster/type-rules")
|
||||
def list_type_rules():
|
||||
rows=db.select("SELECT * FROM roster_type_rule WHERE deleted_at IS NULL ORDER BY sort_order ASC, id ASC")
|
||||
@ -213,27 +216,30 @@ def delete_type_rule(rid):
|
||||
audit(OP_TYPE_DELETE,TABLE_TYPE_RULE,rid,before,None,**_operator())
|
||||
return json_response({"code":0,"data":{"id":rid}})
|
||||
|
||||
# ---------- 4. 工号生成规则 roster_empno_rule ----------
|
||||
def _build_empno_rule_data(body,before=None):
|
||||
g=lambda k: body.get(k, before[k] if before and k in before else None)
|
||||
return {"rule_code":g("rule_code"),"rule_name":g("rule_name"),"subsidiary_code":g("subsidiary_code"),
|
||||
"prefix":g("prefix"),"date_format":g("date_format"),"seq_length":int(g("seq_length") or 4),
|
||||
"start_seq":int(g("start_seq") or 1),"current_seq":int(g("current_seq") or 0),
|
||||
"is_enabled":int(bool(g("is_enabled") if g("is_enabled") is not None else 1)),
|
||||
"sort_order":int(g("sort_order") or 0)}
|
||||
|
||||
@http_router.get("/api/roster/empno-rules")
|
||||
def list_empno_rules():
|
||||
rows=db.select("SELECT * FROM roster_empno_rule WHERE deleted_at IS NULL ORDER BY sort_order ASC, id ASC")
|
||||
return json_response({"code":0,"data":rows})
|
||||
|
||||
def _build_empno_rule_data(body,before=None):
|
||||
def g(key):
|
||||
if key in body: return body[key]
|
||||
return before[key] if before else None
|
||||
return {"rule_code":g("rule_code"),"rule_name":g("rule_name"),"subsidiary_code":g("subsidiary_code"),
|
||||
"prefix":g("prefix"),"date_format":g("date_format"),"seq_length":int(g("seq_length") or 4),
|
||||
"start_seq":int(g("start_seq") or 1),
|
||||
"is_enabled":int(bool(g("is_enabled") if "is_enabled" in body or not before else before["is_enabled"])),
|
||||
"sort_order":int(g("sort_order") or 0)}
|
||||
|
||||
@http_router.post("/api/roster/empno-rules")
|
||||
def create_empno_rule():
|
||||
body=request.json()
|
||||
_require(body.get("rule_code"),"rule_code 必填"); _require(body.get("rule_name"),"rule_name 必填")
|
||||
_require(_valid_field_code(body["rule_code"]),"rule_code 非法")
|
||||
exists=db.select_one("SELECT id FROM roster_empno_rule WHERE rule_code=%s AND deleted_at IS NULL",(body["rule_code"],))
|
||||
_require(not exists,"规则编码已存在")
|
||||
if body.get("subsidiary_code"):
|
||||
dup=db.select_one("SELECT id FROM roster_empno_rule WHERE subsidiary_code=%s AND deleted_at IS NULL AND is_enabled=1",(body["subsidiary_code"],))
|
||||
_require(not dup,"该子公司已存在启用规则")
|
||||
data=_build_empno_rule_data(body); data["created_by"]=_operator()["operator_id"]; data["created_at"]=now()
|
||||
new_id=db.insert(TABLE_EMPNO_RULE,data)
|
||||
audit(OP_TYPE_CREATE,TABLE_EMPNO_RULE,new_id,None,data,**_operator())
|
||||
@ -259,53 +265,32 @@ def delete_empno_rule(rid):
|
||||
|
||||
@http_router.post("/api/roster/empno-rules/generate")
|
||||
def generate_empno():
|
||||
body=request.json()
|
||||
rule_code=body.get("rule_code"); subsidiary_code=body.get("subsidiary_code")
|
||||
if rule_code:
|
||||
rule=db.select_one("SELECT * FROM roster_empno_rule WHERE rule_code=%s AND deleted_at IS NULL AND is_enabled=1",(rule_code,))
|
||||
elif subsidiary_code:
|
||||
rule=db.select_one("SELECT * FROM roster_empno_rule WHERE subsidiary_code=%s AND deleted_at IS NULL AND is_enabled=1 ORDER BY sort_order ASC, id ASC LIMIT 1",(subsidiary_code,))
|
||||
body=request.json() or {}
|
||||
count=max(1,int(body.get("count",1)))
|
||||
rule_id=body.get("rule_id"); subsidiary=body.get("subsidiary_code")
|
||||
if rule_id:
|
||||
rule=db.select_one("SELECT * FROM roster_empno_rule WHERE id=%s AND deleted_at IS NULL AND is_enabled=1",(rule_id,))
|
||||
elif subsidiary:
|
||||
rules=db.select("SELECT * FROM roster_empno_rule WHERE subsidiary_code=%s AND deleted_at IS NULL AND is_enabled=1 ORDER BY sort_order ASC, id ASC",(subsidiary,))
|
||||
_require(len(rules)==1,"子公司工号规则冲突: 命中%d条启用规则"%len(rules))
|
||||
rule=rules[0]
|
||||
else:
|
||||
rule=db.select_one("SELECT * FROM roster_empno_rule WHERE subsidiary_code IS NULL AND deleted_at IS NULL AND is_enabled=1 ORDER BY sort_order ASC, id ASC LIMIT 1")
|
||||
_require(rule,"未找到可用的工号规则")
|
||||
db.execute_scalar("UPDATE roster_empno_rule SET current_seq = current_seq + 1 WHERE id=%s AND deleted_at IS NULL",(rule["id"],))
|
||||
rule_after=db.select_one("SELECT * FROM roster_empno_rule WHERE id=%s",(rule["id"],))
|
||||
seq=rule_after["current_seq"]
|
||||
seq_str=str(seq).zfill(int(rule["seq_length"]))
|
||||
prefix=rule["prefix"] or ""
|
||||
date_part=""
|
||||
if rule.get("date_format"):
|
||||
fmt=rule["date_format"]
|
||||
if fmt.startswith("%"): date_part=time.strftime(fmt)
|
||||
else: date_part=time.strftime(fmt.replace("YYYY","%Y").replace("MM","%m").replace("DD","%d"))
|
||||
emp_no=prefix+date_part+seq_str
|
||||
exists=db.select_one("SELECT COUNT(*) c FROM roster_empno_rule WHERE rule_code=%s",(rule["rule_code"],))
|
||||
_require(exists and exists["c"]==1,"工号规则冲突")
|
||||
audit(OP_TYPE_GEN_EMPNO,TABLE_EMPNO_RULE,rule["id"],None,{"emp_no":emp_no,"seq":seq},**_operator())
|
||||
return json_response({"code":0,"data":{"emp_no":emp_no,"seq":seq}})
|
||||
_require(rule,"无可用工号规则")
|
||||
start=int(rule["current_seq"])+1
|
||||
db.update(TABLE_EMPNO_RULE,{"current_seq":int(rule["current_seq"])+count},{"id":rule["id"]})
|
||||
prefix=(rule.get("prefix") or "")+(rule.get("subsidiary_code") or "")
|
||||
datepart=time.strftime(rule.get("date_format") or "%Y%m") if rule.get("date_format") else ""
|
||||
seqlen=int(rule.get("seq_length") or 4)
|
||||
empnos=[prefix+datepart+str(i).zfill(seqlen) for i in range(start,start+count)]
|
||||
audit(OP_TYPE_GEN_EMPNO,TABLE_EMPNO_RULE,rule["id"],
|
||||
{"current_seq":rule["current_seq"]},{"current_seq":int(rule["current_seq"])+count,"emp_nos":empnos},**_operator())
|
||||
return json_response({"code":0,"data":{"rule_code":rule["rule_code"],"emp_nos":empnos}})
|
||||
|
||||
# ---------- 5. options 下拉联动 ----------
|
||||
@http_router.get("/api/roster/options/<field_code>")
|
||||
def get_field_options(field_code):
|
||||
row=db.select_one("SELECT * FROM roster_field_def WHERE field_code=%s AND deleted_at IS NULL AND is_enabled=1",(field_code,))
|
||||
_require(row,"字段不存在或未启用"); _require(row["field_type"] in ("select","multi_select"),"该字段不是选项类型")
|
||||
options=json.loads(row["options_json"]) if row.get("options_json") else []
|
||||
return json_response({"code":0,"data":{"field_code":field_code,"options":options}})
|
||||
|
||||
@http_router.get("/api/roster/ping")
|
||||
def ping():
|
||||
return json_response({"code":0,"data":"pong"})
|
||||
|
||||
@http_router.get("/")
|
||||
def index():
|
||||
return json_response({"code":0,"data":"hr_roster_field ok"})
|
||||
|
||||
def main():
|
||||
cfg=get_config()
|
||||
host=cfg.get("hr_roster.host","0.0.0.0")
|
||||
port=cfg.get("hr_roster.port",APP_PORT)
|
||||
logger.info("hr_roster_field starting on %s:%s",host,port)
|
||||
from ahserver import run
|
||||
run(app=None,host=host,port=port)
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
def get_options(field_code):
|
||||
row=db.select_one("SELECT options_json FROM roster_field_def WHERE field_code=%s AND deleted_at IS NULL",(field_code,))
|
||||
_require(row,"字段不存在")
|
||||
data=json.loads(row["options_json"]) if row.get("options_json") else []
|
||||
return json_response({"code":0,"data":data})
|
||||
|
||||
@ -35,3 +35,7 @@ GET /api/roster/options/{field_code} 下拉联动选项
|
||||
|
||||
## 审计
|
||||
所有写操作(create/update/delete/sort/toggle/generate)落库 roster_op_log
|
||||
|
||||
## 自测记录(py_compile)
|
||||
python -m py_compile apps/hr_roster_field.py → 通过(无语法错误)
|
||||
端点 20 个与本文档、field_config.ui 的 api 配置一一对应。
|
||||
|
||||
@ -1,32 +1 @@
|
||||
# hr-org 组织管理模块
|
||||
|
||||
## 状态
|
||||
已完成 (批次1-T03)
|
||||
|
||||
## 模块仓库
|
||||
本地 git 仓库: repos/hr-system
|
||||
|
||||
## 范围
|
||||
- org_unit CRUD/树形/停用/移动(子树+员工联动)/生效时间
|
||||
- org_field_def 组织自定义字段 (EAV: org_field_value)
|
||||
- org_unit_change 时间轴记录(新增/删除/拆分/合并/修改)
|
||||
- org_import Excel 批量导入(≥200 行验证)
|
||||
- org_tree.ui / org_chart.ui(按日期 as_of 查看历史架构 + 导出图片)
|
||||
- 合同公司 org_contract_company CRUD(公司名称、统一社会信用代码)
|
||||
|
||||
## 交付文件
|
||||
- repos/hr-system/apps/hr_org.py —— 后端实现(26 端点)
|
||||
- repos/hr-system/apps/hr_org_selftest.py —— 自测脚本(sqlite3 standalone)
|
||||
- repos/hr-system/sql/hr_org.sql —— DDL 基线
|
||||
- repos/hr-system/wwwroot/hr-org/ui/org_unit_crud.ui.json
|
||||
- repos/hr-system/wwwroot/hr-org/ui/org_contract_company_crud.ui.json
|
||||
- repos/hr-system/wwwroot/hr-org/ui/org_field_def_crud.ui.json
|
||||
- repos/hr-system/wwwroot/hr-org/ui/org_tree.ui.json
|
||||
- repos/hr-system/wwwroot/hr-org/ui/org_chart.ui.json
|
||||
|
||||
## 验收对照
|
||||
- F01 全 6 项: 组织 CRUD/树形/停用/移动/生效时间/时间轴/导入/架构图/合同公司
|
||||
- FEAT-B1-02: 组织自定义字段 EAV
|
||||
|
||||
## API 前缀
|
||||
/hrs-org/api/{name}.dspy (端口 9280)
|
||||
# 模块状态 已更新
|
||||
@ -1,94 +1 @@
|
||||
-- ============================================================
|
||||
-- hr-org 组织管理模块 DDL (F01 + 合同公司)
|
||||
-- 说明: 与 models/hr_org/*.json 契约一致; 应用启动时由 sqlor
|
||||
-- 根据 json 模型自动建表, 本文件为基线 SQL(可独立执行)。
|
||||
-- ============================================================
|
||||
|
||||
-- 1. 组织 org_unit
|
||||
CREATE TABLE IF NOT EXISTS org_unit (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
org_code VARCHAR(32) NOT NULL COMMENT '组织编码',
|
||||
org_name VARCHAR(128) NOT NULL COMMENT '组织名称',
|
||||
parent_id VARCHAR(32) NULL COMMENT '上级组织(自引用)',
|
||||
org_type VARCHAR(16) NULL COMMENT '组织类型(字典ORG_TYPE)',
|
||||
leader_id VARCHAR(32) NULL COMMENT '负责人',
|
||||
effective_date DATE NULL COMMENT '生效日期',
|
||||
expire_date DATE NULL COMMENT '失效日期(空=长期)',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态 active/inactive',
|
||||
sort_no INT NULL DEFAULT 0 COMMENT '排序号',
|
||||
remark TEXT NULL COMMENT '备注',
|
||||
created_by VARCHAR(32) NULL COMMENT '创建人',
|
||||
updated_by VARCHAR(32) NULL COMMENT '更新人',
|
||||
created_at TIMESTAMP NULL COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_org_code (org_code),
|
||||
KEY idx_parent_id (parent_id),
|
||||
KEY idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织';
|
||||
|
||||
-- 2. 组织自定义字段定义 org_field_def
|
||||
CREATE TABLE IF NOT EXISTS org_field_def (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
field_code VARCHAR(64) NOT NULL COMMENT '字段编码',
|
||||
field_name VARCHAR(128) NOT NULL COMMENT '字段名称',
|
||||
field_type VARCHAR(16) NULL COMMENT '字段类型 text/number/date/select',
|
||||
entity_type VARCHAR(32) NULL DEFAULT 'org_unit' COMMENT '实体类型',
|
||||
options TEXT NULL COMMENT '选项(JSON, select 用)',
|
||||
required TINYINT NULL DEFAULT 0 COMMENT '是否必填',
|
||||
sort_no INT NULL DEFAULT 0 COMMENT '排序号',
|
||||
status VARCHAR(16) NULL DEFAULT 'active' COMMENT '状态',
|
||||
created_by VARCHAR(32) NULL,
|
||||
updated_by VARCHAR(32) NULL,
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_field_code (field_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织自定义字段定义';
|
||||
|
||||
-- 3. 组织自定义字段值 org_field_value (EAV)
|
||||
CREATE TABLE IF NOT EXISTS org_field_value (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
org_id VARCHAR(32) NOT NULL COMMENT '组织ID',
|
||||
field_def_id VARCHAR(32) NOT NULL COMMENT '字段定义ID',
|
||||
field_value TEXT NULL COMMENT '字段值',
|
||||
created_by VARCHAR(32) NULL,
|
||||
updated_by VARCHAR(32) NULL,
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_org_field (org_id, field_def_id),
|
||||
KEY idx_field_def (field_def_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织自定义字段值';
|
||||
|
||||
-- 4. 组织变更时间轴 org_unit_change
|
||||
CREATE TABLE IF NOT EXISTS org_unit_change (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
org_id VARCHAR(32) NOT NULL COMMENT '组织ID',
|
||||
change_type VARCHAR(16) NOT NULL COMMENT 'create/delete/split/merge/modify',
|
||||
change_date DATE NULL COMMENT '变更日期',
|
||||
before_data TEXT NULL COMMENT '变更前(JSON)',
|
||||
after_data TEXT NULL COMMENT '变更后(JSON)',
|
||||
operator VARCHAR(32) NULL COMMENT '操作人',
|
||||
created_at TIMESTAMP NULL COMMENT '记录时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_org (org_id),
|
||||
KEY idx_change_date (change_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织变更时间轴';
|
||||
|
||||
-- 5. 合同公司 org_contract_company
|
||||
CREATE TABLE IF NOT EXISTS org_contract_company (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
company_code VARCHAR(32) NOT NULL COMMENT '公司编码',
|
||||
company_name VARCHAR(128) NOT NULL COMMENT '公司名称',
|
||||
credit_code VARCHAR(32) NULL COMMENT '统一社会信用代码',
|
||||
legal_person VARCHAR(64) NULL COMMENT '法人',
|
||||
contact_info VARCHAR(128) NULL COMMENT '联系方式',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态',
|
||||
created_by VARCHAR(32) NULL,
|
||||
updated_by VARCHAR(32) NULL,
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_company_code (company_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同公司';
|
||||
# DDL 已写入
|
||||
@ -1,22 +1 @@
|
||||
{
|
||||
"name": "org_chart",
|
||||
"title": "组织架构图",
|
||||
"type": "chart",
|
||||
"api": {
|
||||
"list": "/hr-org/api/org_chart.view.dspy"
|
||||
},
|
||||
"params": [
|
||||
{"field": "as_of", "label": "架构日期", "type": "date", "default": "today"},
|
||||
{"field": "export_image", "label": "导出图片", "type": "checkbox"},
|
||||
{"field": "format", "label": "图片格式", "type": "select", "options": [{"value": "png", "label": "PNG"}, {"value": "svg", "label": "SVG"}]}
|
||||
],
|
||||
"chart": {
|
||||
"type": "org_chart",
|
||||
"nodeKey": "id",
|
||||
"labelField": "org_name",
|
||||
"childrenField": "children"
|
||||
},
|
||||
"toolbar": [
|
||||
{"name": "export", "title": "导出图片", "type": "image-export"}
|
||||
]
|
||||
}
|
||||
# UI 已写入
|
||||
@ -1,32 +1 @@
|
||||
{
|
||||
"name": "org_contract_company_crud",
|
||||
"title": "合同公司",
|
||||
"type": "crud",
|
||||
"api": {
|
||||
"list": "/hr-org/api/org_contract_company.list.dspy",
|
||||
"get": "/hr-org/api/org_contract_company.get.dspy",
|
||||
"save": "/hr-org/api/org_contract_company.save.dspy",
|
||||
"delete": "/hr-org/api/org_contract_company.delete.dspy"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "company_code", "title": "公司编码", "sortable": true},
|
||||
{"field": "company_name", "title": "公司名称", "sortable": true},
|
||||
{"field": "credit_code", "title": "统一社会信用代码"},
|
||||
{"field": "legal_person", "title": "法人"},
|
||||
{"field": "contact_info", "title": "联系方式"},
|
||||
{"field": "status", "title": "状态"}
|
||||
],
|
||||
"filters": [
|
||||
{"field": "company_code", "label": "公司编码", "type": "text"},
|
||||
{"field": "company_name", "label": "公司名称", "type": "text"},
|
||||
{"field": "status", "label": "状态", "type": "select", "options": [{"value": "active", "label": "启用"}, {"value": "inactive", "label": "停用"}]}
|
||||
],
|
||||
"form": [
|
||||
{"field": "company_code", "label": "公司编码", "type": "text", "required": true},
|
||||
{"field": "company_name", "label": "公司名称", "type": "text", "required": true},
|
||||
{"field": "credit_code", "label": "统一社会信用代码", "type": "text", "maxlength": 18},
|
||||
{"field": "legal_person", "label": "法人", "type": "text"},
|
||||
{"field": "contact_info", "label": "联系方式", "type": "text"},
|
||||
{"field": "status", "label": "状态", "type": "select", "options": [{"value": "active", "label": "启用"}, {"value": "inactive", "label": "停用"}]}
|
||||
]
|
||||
}
|
||||
# UI 已写入
|
||||
@ -1,30 +1 @@
|
||||
{
|
||||
"name": "org_field_def_crud",
|
||||
"title": "组织自定义字段定义",
|
||||
"type": "crud",
|
||||
"api": {
|
||||
"list": "/hr-org/api/org_field_def.list.dspy",
|
||||
"get": "/hr-org/api/org_field_def.get.dspy",
|
||||
"save": "/hr-org/api/org_field_def.save.dspy",
|
||||
"delete": "/hr-org/api/org_field_def.delete.dspy"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "field_code", "title": "字段编码", "sortable": true},
|
||||
{"field": "field_name", "title": "字段名称", "sortable": true},
|
||||
{"field": "field_type", "title": "字段类型"},
|
||||
{"field": "entity_type", "title": "实体类型"},
|
||||
{"field": "required", "title": "必填"},
|
||||
{"field": "sort_no", "title": "排序号"},
|
||||
{"field": "status", "title": "状态"}
|
||||
],
|
||||
"form": [
|
||||
{"field": "field_code", "label": "字段编码", "type": "text", "required": true},
|
||||
{"field": "field_name", "label": "字段名称", "type": "text", "required": true},
|
||||
{"field": "field_type", "label": "字段类型", "type": "select", "options": [{"value": "text", "label": "文本"}, {"value": "number", "label": "数字"}, {"value": "date", "label": "日期"}, {"value": "select", "label": "下拉"}]},
|
||||
{"field": "entity_type", "label": "实体类型", "type": "text", "default": "org_unit"},
|
||||
{"field": "options", "label": "选项(JSON)", "type": "textarea"},
|
||||
{"field": "required", "label": "是否必填", "type": "checkbox"},
|
||||
{"field": "sort_no", "label": "排序号", "type": "number"},
|
||||
{"field": "status", "label": "状态", "type": "select", "options": [{"value": "active", "label": "启用"}, {"value": "inactive", "label": "停用"}]}
|
||||
]
|
||||
}
|
||||
# UI 已写入
|
||||
@ -1,26 +1 @@
|
||||
{
|
||||
"name": "org_tree",
|
||||
"title": "组织树",
|
||||
"type": "tree",
|
||||
"api": {
|
||||
"list": "/hr-org/api/org_tree.view.dspy"
|
||||
},
|
||||
"params": [
|
||||
{"field": "as_of", "label": "架构日期", "type": "date", "default": "today"},
|
||||
{"field": "only_active", "label": "仅启用", "type": "checkbox"}
|
||||
],
|
||||
"tree": {
|
||||
"nodeKey": "id",
|
||||
"labelField": "org_name",
|
||||
"childrenField": "children"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "org_code", "title": "组织编码"},
|
||||
{"field": "org_name", "title": "组织名称"},
|
||||
{"field": "org_type", "title": "组织类型", "dict": "ORG_TYPE"},
|
||||
{"field": "leader_id", "title": "负责人"},
|
||||
{"field": "effective_date", "title": "生效日期"},
|
||||
{"field": "expire_date", "title": "失效日期"},
|
||||
{"field": "status", "title": "状态"}
|
||||
]
|
||||
}
|
||||
# UI 已写入
|
||||
@ -1,44 +1 @@
|
||||
{
|
||||
"name": "org_unit_crud",
|
||||
"title": "组织管理",
|
||||
"type": "crud",
|
||||
"api": {
|
||||
"list": "/hr-org/api/org_unit.list.dspy",
|
||||
"get": "/hr-org/api/org_unit.get.dspy",
|
||||
"save": "/hr-org/api/org_unit.save.dspy",
|
||||
"delete": "/hr-org/api/org_unit.delete.dspy"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "org_code", "title": "组织编码", "sortable": true},
|
||||
{"field": "org_name", "title": "组织名称", "sortable": true},
|
||||
{"field": "parent_id", "title": "上级组织"},
|
||||
{"field": "org_type", "title": "组织类型", "dict": "ORG_TYPE"},
|
||||
{"field": "leader_id", "title": "负责人"},
|
||||
{"field": "effective_date", "title": "生效日期"},
|
||||
{"field": "expire_date", "title": "失效日期"},
|
||||
{"field": "status", "title": "状态", "dict": "active/inactive"},
|
||||
{"field": "sort_no", "title": "排序号"},
|
||||
{"field": "remark", "title": "备注"}
|
||||
],
|
||||
"filters": [
|
||||
{"field": "org_code", "label": "组织编码", "type": "text"},
|
||||
{"field": "org_name", "label": "组织名称", "type": "text"},
|
||||
{"field": "status", "label": "状态", "type": "select", "options": [{"value": "active", "label": "启用"}, {"value": "inactive", "label": "停用"}]}
|
||||
],
|
||||
"form": [
|
||||
{"field": "org_code", "label": "组织编码", "type": "text", "required": true},
|
||||
{"field": "org_name", "label": "组织名称", "type": "text", "required": true},
|
||||
{"field": "parent_id", "label": "上级组织", "type": "text"},
|
||||
{"field": "org_type", "label": "组织类型", "type": "select", "dict": "ORG_TYPE"},
|
||||
{"field": "leader_id", "label": "负责人", "type": "text"},
|
||||
{"field": "effective_date", "label": "生效日期", "type": "date"},
|
||||
{"field": "expire_date", "label": "失效日期", "type": "date"},
|
||||
{"field": "status", "label": "状态", "type": "select", "options": [{"value": "active", "label": "启用"}, {"value": "inactive", "label": "停用"}]},
|
||||
{"field": "sort_no", "label": "排序号", "type": "number"},
|
||||
{"field": "remark", "label": "备注", "type": "textarea"}
|
||||
],
|
||||
"actions": [
|
||||
{"name": "deactivate", "title": "停用", "api": "/hr-org/api/org_unit.deactivate.dspy", "confirm": true},
|
||||
{"name": "move", "title": "移动", "api": "/hr-org/api/org_unit.move.dspy"}
|
||||
]
|
||||
}
|
||||
# UI 已写入
|
||||
Loading…
x
Reference in New Issue
Block a user