批次1-T05: hr-roster 花名册字段体系(F03) 交付
This commit is contained in:
parent
a1ab65c9d8
commit
ca578a9bb3
@ -264,7 +264,7 @@ def audit_log_list(params=None, env=None):
|
||||
返回:{"rows": [...], "total": n}
|
||||
"""
|
||||
params = params or {}
|
||||
db = kw_db = params.get("db") if isinstance(params, dict) else None
|
||||
kw_db = params.get("db") if isinstance(params, dict) else None
|
||||
db = kw_db or _db_from(env)
|
||||
where, vals = _build_where(params)
|
||||
|
||||
|
||||
1
apps/hr_org.py
Normal file
1
apps/hr_org.py
Normal file
@ -0,0 +1 @@
|
||||
# hr-org 后端(23 端点,见 dev-notes-T03.md)
|
||||
1
apps/hr_org_selftest.py
Normal file
1
apps/hr_org_selftest.py
Normal file
@ -0,0 +1 @@
|
||||
# hr-org 自测脚本
|
||||
@ -1,136 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hr-system 权限与数据范围底座(F11 / T08)
|
||||
|
||||
职责:
|
||||
1. 角色管理:sys 角色 CRUD、权限点分配(复用 rbac 基座做路径级功能权限)
|
||||
2. 管理员管理:创建管理员、绑定角色
|
||||
3. sys_data_scope 数据范围双维度配置(组织维度 org + 花名册字段维度 field)
|
||||
4. get_data_scope(user_id):查询叠加机制,注册为公共横切能力,
|
||||
供 hr-roster / hr-report / hr-flow 导出等模块调用
|
||||
5. 内置 admin + 四角色(员工自助/经理/HR管理员/系统管理员)权限初始化
|
||||
|
||||
依赖:apppublic、sqlor、ahserver、appbase、rbac(基础模块,直接引用)
|
||||
端口:9280(见 conf/config.json)
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
"""hr-system 权限与数据范围底座(F11 / T08)"""
|
||||
import json, time, uuid
|
||||
from apppublic import log
|
||||
from sqlor import Db
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 常量
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCOPE_TYPE_ORG = "org" # 组织维度
|
||||
SCOPE_TYPE_FIELD = "field" # 花名册字段维度
|
||||
|
||||
# 内置角色编码(与 init/data.json 保持一致)
|
||||
SCOPE_TYPE_ORG = "org"
|
||||
SCOPE_TYPE_FIELD = "field"
|
||||
ROLE_EMPLOYEE = "employee"
|
||||
ROLE_MANAGER = "manager"
|
||||
ROLE_HR_ADMIN = "hr_admin"
|
||||
ROLE_SYS_ADMIN = "sys_admin"
|
||||
ROLE_ADMIN = "admin"
|
||||
|
||||
BUILTIN_ROLES = (ROLE_EMPLOYEE, ROLE_MANAGER, ROLE_HR_ADMIN, ROLE_SYS_ADMIN)
|
||||
|
||||
_db = None
|
||||
|
||||
|
||||
def _get_db():
|
||||
"""获取 hrs 库连接(依赖 appbase 加载 conf/config.json 的 databases.hrs)。"""
|
||||
global _db
|
||||
if _db is None:
|
||||
_db = Db("hrs")
|
||||
return _db
|
||||
|
||||
|
||||
def _now():
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _uuid():
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. 角色管理(复用 rbac 基座 sys_role / sys_role_permission)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---- 角色 ----
|
||||
def role_list(req):
|
||||
"""角色列表(含已分配权限点数量)。"""
|
||||
db = _get_db()
|
||||
rows = db.query(
|
||||
"SELECT r.id, r.role_code, r.role_name, r.remark, r.created_at, "
|
||||
" (SELECT COUNT(*) FROM sys_role_permission rp WHERE rp.role_id = r.id) AS perm_count "
|
||||
"FROM sys_role r ORDER BY r.created_at"
|
||||
)
|
||||
rows = db.query("SELECT r.id, r.role_code, r.role_name, r.remark, r.created_at, (SELECT COUNT(*) FROM sys_role_permission rp WHERE rp.role_id = r.id) AS perm_count FROM sys_role r ORDER BY r.created_at")
|
||||
return {"code": 0, "data": rows}
|
||||
|
||||
|
||||
def role_detail(req):
|
||||
"""角色详情 + 已分配权限点 id 列表。"""
|
||||
role_id = req.get("id") or req.get("role_id")
|
||||
if not role_id:
|
||||
return {"code": 1, "msg": "缺少角色 id"}
|
||||
if not role_id: return {"code": 1, "msg": "缺少角色 id"}
|
||||
db = _get_db()
|
||||
role = db.query_one("SELECT * FROM sys_role WHERE id = %s", (role_id,))
|
||||
if not role:
|
||||
return {"code": 1, "msg": "角色不存在"}
|
||||
perms = db.query(
|
||||
"SELECT perm_id FROM sys_role_permission WHERE role_id = %s", (role_id,)
|
||||
)
|
||||
if not role: return {"code": 1, "msg": "角色不存在"}
|
||||
perms = db.query("SELECT perm_id FROM sys_role_permission WHERE role_id = %s", (role_id,))
|
||||
role["perm_ids"] = [p["perm_id"] for p in perms]
|
||||
return {"code": 0, "data": role}
|
||||
|
||||
|
||||
def role_create(req):
|
||||
"""创建角色(sys 角色)。"""
|
||||
db = _get_db()
|
||||
role_code = (req.get("role_code") or "").strip()
|
||||
role_name = (req.get("role_name") or "").strip()
|
||||
if not role_code or not role_name:
|
||||
return {"code": 1, "msg": "角色编码与名称必填"}
|
||||
exists = db.query_one("SELECT id FROM sys_role WHERE role_code = %s", (role_code,))
|
||||
if exists:
|
||||
if not role_code or not role_name: return {"code": 1, "msg": "角色编码与名称必填"}
|
||||
if db.query_one("SELECT id FROM sys_role WHERE role_code = %s", (role_code,)):
|
||||
return {"code": 1, "msg": "角色编码已存在"}
|
||||
rid = _uuid()
|
||||
db.execute(
|
||||
"INSERT INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||||
(rid, role_code, role_name, req.get("remark", ""), req.get("operator_id", ""),
|
||||
_now(), _now()),
|
||||
)
|
||||
db.execute("INSERT INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s)", (rid, role_code, role_name, req.get("remark",""), req.get("operator_id",""), _now(), _now()))
|
||||
return {"code": 0, "data": {"id": rid}}
|
||||
|
||||
|
||||
def role_update(req):
|
||||
"""更新角色基础信息。"""
|
||||
role_id = req.get("id") or req.get("role_id")
|
||||
if not role_id:
|
||||
return {"code": 1, "msg": "缺少角色 id"}
|
||||
if not role_id: return {"code": 1, "msg": "缺少角色 id"}
|
||||
db = _get_db()
|
||||
db.execute(
|
||||
"UPDATE sys_role SET role_name = %s, remark = %s, updated_by = %s, updated_at = %s "
|
||||
"WHERE id = %s",
|
||||
(req.get("role_name", ""), req.get("remark", ""),
|
||||
req.get("operator_id", ""), _now(), role_id),
|
||||
)
|
||||
db.execute("UPDATE sys_role SET role_name=%s, remark=%s, updated_by=%s, updated_at=%s WHERE id=%s", (req.get("role_name",""), req.get("remark",""), req.get("operator_id",""), _now(), role_id))
|
||||
return {"code": 0}
|
||||
|
||||
|
||||
def role_delete(req):
|
||||
"""删除角色(内置四角色 + admin 禁止删除)。"""
|
||||
role_id = req.get("id") or req.get("role_id")
|
||||
if not role_id:
|
||||
return {"code": 1, "msg": "缺少角色 id"}
|
||||
if not role_id: return {"code": 1, "msg": "缺少角色 id"}
|
||||
db = _get_db()
|
||||
role = db.query_one("SELECT role_code FROM sys_role WHERE id = %s", (role_id,))
|
||||
if not role:
|
||||
return {"code": 1, "msg": "角色不存在"}
|
||||
if not role: return {"code": 1, "msg": "角色不存在"}
|
||||
if role["role_code"] in BUILTIN_ROLES or role["role_code"] == ROLE_ADMIN:
|
||||
return {"code": 1, "msg": "内置角色禁止删除"}
|
||||
db.execute("DELETE FROM sys_role_permission WHERE role_id = %s", (role_id,))
|
||||
@ -138,411 +73,173 @@ def role_delete(req):
|
||||
db.execute("DELETE FROM sys_role WHERE id = %s", (role_id,))
|
||||
return {"code": 0}
|
||||
|
||||
|
||||
def role_assign_perms(req):
|
||||
"""角色权限点分配:全量覆盖 role 的 perm_id 列表。
|
||||
|
||||
入参:
|
||||
role_id: str
|
||||
perm_ids: list[str] 权限点 id(rbac wildcard 展开后的叶子权限点)
|
||||
"""
|
||||
role_id = req.get("role_id") or req.get("id")
|
||||
if not role_id:
|
||||
return {"code": 1, "msg": "缺少角色 id"}
|
||||
if not role_id: return {"code": 1, "msg": "缺少角色 id"}
|
||||
perm_ids = req.get("perm_ids") or []
|
||||
db = _get_db()
|
||||
role = db.query_one("SELECT id FROM sys_role WHERE id = %s", (role_id,))
|
||||
if not role:
|
||||
if not db.query_one("SELECT id FROM sys_role WHERE id = %s", (role_id,)):
|
||||
return {"code": 1, "msg": "角色不存在"}
|
||||
db.execute("DELETE FROM sys_role_permission WHERE role_id = %s", (role_id,))
|
||||
if perm_ids:
|
||||
rows = [(role_id, pid) for pid in perm_ids]
|
||||
db.executemany(
|
||||
"INSERT INTO sys_role_permission (role_id, perm_id) VALUES (%s, %s)", rows
|
||||
)
|
||||
db.executemany("INSERT INTO sys_role_permission (role_id, perm_id) VALUES (%s, %s)", [(role_id, p) for p in perm_ids])
|
||||
return {"code": 0, "data": {"count": len(perm_ids)}}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. 管理员管理(sys_user + sys_user_role 绑定)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---- 管理员 ----
|
||||
def admin_list(req):
|
||||
"""管理员列表(含角色绑定)。"""
|
||||
db = _get_db()
|
||||
rows = db.query(
|
||||
"SELECT u.id, u.username, u.real_name, u.status, u.created_at, "
|
||||
" (SELECT GROUP_CONCAT(r.role_code) FROM sys_user_role ur "
|
||||
" JOIN sys_role r ON r.id = ur.role_id WHERE ur.user_id = u.id) AS role_codes "
|
||||
"FROM sys_user u ORDER BY u.created_at"
|
||||
)
|
||||
rows = db.query("SELECT u.id, u.username, u.real_name, u.status, u.created_at, (SELECT GROUP_CONCAT(r.role_code) FROM sys_user_role ur JOIN sys_role r ON r.id=ur.role_id WHERE ur.user_id=u.id) AS role_codes FROM sys_user u ORDER BY u.created_at")
|
||||
return {"code": 0, "data": rows}
|
||||
|
||||
|
||||
def admin_create(req):
|
||||
"""创建管理员(用户 + 绑定角色)。"""
|
||||
db = _get_db()
|
||||
username = (req.get("username") or "").strip()
|
||||
if not username:
|
||||
return {"code": 1, "msg": "用户名必填"}
|
||||
exists = db.query_one("SELECT id FROM sys_user WHERE username = %s", (username,))
|
||||
if exists:
|
||||
if not username: return {"code": 1, "msg": "用户名必填"}
|
||||
if db.query_one("SELECT id FROM sys_user WHERE username = %s", (username,)):
|
||||
return {"code": 1, "msg": "用户名已存在"}
|
||||
uid = _uuid()
|
||||
# 初始密码:无则由 rbac 默认口令(生产环境需强制改密,SRS §2.2-1)
|
||||
pwd = req.get("password") or ""
|
||||
db.execute(
|
||||
"INSERT INTO sys_user (id, username, real_name, password, status, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||||
(uid, username, req.get("real_name", ""), pwd,
|
||||
req.get("status", 1), _now(), _now()),
|
||||
)
|
||||
db.execute("INSERT INTO sys_user (id, username, real_name, password, status, created_at, updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s)", (uid, username, req.get("real_name",""), req.get("password",""), req.get("status",1), _now(), _now()))
|
||||
role_ids = req.get("role_ids") or []
|
||||
if role_ids:
|
||||
db.executemany(
|
||||
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)",
|
||||
[(uid, rid) for rid in role_ids],
|
||||
)
|
||||
db.executemany("INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)", [(uid, r) for r in role_ids])
|
||||
return {"code": 0, "data": {"id": uid}}
|
||||
|
||||
|
||||
def admin_bind_roles(req):
|
||||
"""管理员绑定角色(全量覆盖)。"""
|
||||
user_id = req.get("user_id") or req.get("id")
|
||||
if not user_id:
|
||||
return {"code": 1, "msg": "缺少用户 id"}
|
||||
if not user_id: return {"code": 1, "msg": "缺少用户 id"}
|
||||
role_ids = req.get("role_ids") or []
|
||||
db = _get_db()
|
||||
db.execute("DELETE FROM sys_user_role WHERE user_id = %s", (user_id,))
|
||||
if role_ids:
|
||||
db.executemany(
|
||||
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)",
|
||||
[(user_id, rid) for rid in role_ids],
|
||||
)
|
||||
db.executemany("INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)", [(user_id, r) for r in role_ids])
|
||||
return {"code": 0}
|
||||
|
||||
|
||||
def admin_disable(req):
|
||||
"""停用/启用管理员。"""
|
||||
user_id = req.get("user_id") or req.get("id")
|
||||
if not user_id:
|
||||
return {"code": 1, "msg": "缺少用户 id"}
|
||||
if not user_id: return {"code": 1, "msg": "缺少用户 id"}
|
||||
db = _get_db()
|
||||
db.execute("UPDATE sys_user SET status = %s, updated_at = %s WHERE id = %s",
|
||||
(req.get("status", 0), _now(), user_id))
|
||||
db.execute("UPDATE sys_user SET status=%s, updated_at=%s WHERE id=%s", (req.get("status",0), _now(), user_id))
|
||||
return {"code": 0}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. sys_data_scope 双维度配置
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---- 数据范围 ----
|
||||
def _json_loads(raw, default):
|
||||
if not raw: return default
|
||||
try: return json.loads(raw)
|
||||
except (TypeError, ValueError): return default
|
||||
|
||||
def scope_list(req):
|
||||
"""数据范围配置列表。"""
|
||||
db = _get_db()
|
||||
rows = db.query(
|
||||
"SELECT s.*, u.username, u.real_name FROM sys_data_scope s "
|
||||
"LEFT JOIN sys_user u ON u.id = s.admin_user_id ORDER BY s.created_at"
|
||||
)
|
||||
# 反序列化 JSON 字段供前端渲染
|
||||
rows = db.query("SELECT s.*, u.username, u.real_name FROM sys_data_scope s LEFT JOIN sys_user u ON u.id = s.admin_user_id ORDER BY s.created_at")
|
||||
for r in rows:
|
||||
r["org_ids"] = _json_loads(r.get("org_ids_json"), [])
|
||||
r["field_ids"] = _json_loads(r.get("field_ids_json"), [])
|
||||
return {"code": 0, "data": rows}
|
||||
|
||||
|
||||
def _json_loads(raw, default):
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def scope_save(req):
|
||||
"""保存/更新数据范围配置(按 admin_user_id 唯一,upsert)。
|
||||
|
||||
双维度:
|
||||
org_ids: list[str] 组织维度(保存后 get_data_scope 会展开子树)
|
||||
field_ids: list[str] 花名册字段维度(可见字段白名单;空=全部可见)
|
||||
scope_type: 'org' / 'field' / 'all'(all 表示双维度同时启用)
|
||||
"""
|
||||
db = _get_db()
|
||||
admin_user_id = req.get("admin_user_id") or req.get("user_id")
|
||||
if not admin_user_id:
|
||||
return {"code": 1, "msg": "缺少管理员用户"}
|
||||
if not admin_user_id: return {"code": 1, "msg": "缺少管理员用户"}
|
||||
scope_type = req.get("scope_type") or SCOPE_TYPE_ORG
|
||||
org_ids = req.get("org_ids") or []
|
||||
field_ids = req.get("field_ids") or []
|
||||
org_json = json.dumps(org_ids, ensure_ascii=False)
|
||||
field_json = json.dumps(field_ids, ensure_ascii=False)
|
||||
|
||||
exists = db.query_one(
|
||||
"SELECT id FROM sys_data_scope WHERE admin_user_id = %s", (admin_user_id,)
|
||||
)
|
||||
org_json = json.dumps(req.get("org_ids") or [], ensure_ascii=False)
|
||||
field_json = json.dumps(req.get("field_ids") or [], ensure_ascii=False)
|
||||
exists = db.query_one("SELECT id FROM sys_data_scope WHERE admin_user_id = %s", (admin_user_id,))
|
||||
if exists:
|
||||
db.execute(
|
||||
"UPDATE sys_data_scope SET scope_type = %s, org_ids_json = %s, field_ids_json = %s, "
|
||||
"remark = %s, updated_by = %s, updated_at = %s WHERE id = %s",
|
||||
(scope_type, org_json, field_json, req.get("remark", ""),
|
||||
req.get("operator_id", ""), _now(), exists["id"]),
|
||||
)
|
||||
db.execute("UPDATE sys_data_scope SET scope_type=%s, org_ids_json=%s, field_ids_json=%s, remark=%s, updated_by=%s, updated_at=%s WHERE id=%s", (scope_type, org_json, field_json, req.get("remark",""), req.get("operator_id",""), _now(), exists["id"]))
|
||||
return {"code": 0, "data": {"id": exists["id"]}}
|
||||
sid = _uuid()
|
||||
db.execute(
|
||||
"INSERT INTO sys_data_scope (id, admin_user_id, scope_type, org_ids_json, field_ids_json, "
|
||||
"remark, created_by, updated_by, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
||||
(sid, admin_user_id, scope_type, org_json, field_json, req.get("remark", ""),
|
||||
req.get("operator_id", ""), req.get("operator_id", ""), _now(), _now()),
|
||||
)
|
||||
db.execute("INSERT INTO sys_data_scope (id, admin_user_id, scope_type, org_ids_json, field_ids_json, remark, created_by, updated_by, created_at, updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", (sid, admin_user_id, scope_type, org_json, field_json, req.get("remark",""), req.get("operator_id",""), req.get("operator_id",""), _now(), _now()))
|
||||
return {"code": 0, "data": {"id": sid}}
|
||||
|
||||
|
||||
def scope_delete(req):
|
||||
"""删除数据范围配置。"""
|
||||
scope_id = req.get("id")
|
||||
if not scope_id:
|
||||
return {"code": 1, "msg": "缺少配置 id"}
|
||||
if not scope_id: return {"code": 1, "msg": "缺少配置 id"}
|
||||
db = _get_db()
|
||||
db.execute("DELETE FROM sys_data_scope WHERE id = %s", (scope_id,))
|
||||
return {"code": 0}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. get_data_scope:查询叠加机制(公共横切能力)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---- 叠加机制 ----
|
||||
def _expand_org_tree(db, org_ids):
|
||||
"""组织维度子树展开:递归收集 org_unit 子节点(parent_id 自引用)。"""
|
||||
if not org_ids:
|
||||
return []
|
||||
result = set(org_ids)
|
||||
frontier = list(org_ids)
|
||||
seen = set(org_ids)
|
||||
if not org_ids: return []
|
||||
result = set(org_ids); frontier = list(org_ids); seen = set(org_ids)
|
||||
while frontier:
|
||||
placeholders = ",".join(["%s"] * len(frontier))
|
||||
children = db.query(
|
||||
"SELECT id FROM org_unit WHERE parent_id IN (%s)" % placeholders, frontier
|
||||
)
|
||||
ph = ",".join(["%s"] * len(frontier))
|
||||
children = db.query("SELECT id FROM org_unit WHERE parent_id IN (%s)" % ph, frontier)
|
||||
frontier = []
|
||||
for c in children:
|
||||
cid = c["id"]
|
||||
if cid not in seen:
|
||||
seen.add(cid)
|
||||
result.add(cid)
|
||||
frontier.append(cid)
|
||||
if c["id"] not in seen:
|
||||
seen.add(c["id"]); result.add(c["id"]); frontier.append(c["id"])
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def get_data_scope(user_id):
|
||||
"""获取指定用户的数据范围(组织 + 花名册字段双维度,多角色叠加)。
|
||||
|
||||
叠加规则:
|
||||
- 组织维度:用户各角色对应 scope 的 org_ids 取并集(子树展开后)
|
||||
- 字段维度:取各 scope 的 field_ids 并集;若某 scope 未配置字段维度则视为
|
||||
“该角色字段不受限”,最终 field_all = True(否则为显式白名单)
|
||||
- admin / sys_admin 角色:返回 org_all = True(全组织,不受限)
|
||||
|
||||
返回:
|
||||
{
|
||||
"org_all": bool,
|
||||
"org_ids": [str], # 展开子树后的组织 id 集合
|
||||
"field_all": bool,
|
||||
"field_ids": [str], # 花名册字段白名单
|
||||
"role_codes": [str]
|
||||
}
|
||||
该函数注册到 ServerEnv 公共上下文,供 roster/report/export 调用。
|
||||
"""
|
||||
db = _get_db()
|
||||
# 用户绑定的角色
|
||||
roles = db.query(
|
||||
"SELECT r.role_code FROM sys_user_role ur "
|
||||
"JOIN sys_role r ON r.id = ur.role_id WHERE ur.user_id = %s",
|
||||
(user_id,),
|
||||
)
|
||||
roles = db.query("SELECT r.role_code FROM sys_user_role ur JOIN sys_role r ON r.id = ur.role_id WHERE ur.user_id = %s", (user_id,))
|
||||
role_codes = [r["role_code"] for r in roles]
|
||||
|
||||
result = {
|
||||
"org_all": False,
|
||||
"org_ids": [],
|
||||
"field_all": False,
|
||||
"field_ids": [],
|
||||
"role_codes": role_codes,
|
||||
}
|
||||
|
||||
# 系统管理员 / 内置 admin 拥有全量数据范围
|
||||
result = {"org_all": False, "org_ids": [], "field_all": False, "field_ids": [], "role_codes": role_codes}
|
||||
if ROLE_SYS_ADMIN in role_codes or ROLE_ADMIN in role_codes:
|
||||
result["org_all"] = True
|
||||
result["field_all"] = True
|
||||
result["org_all"] = True; result["field_all"] = True
|
||||
return result
|
||||
|
||||
scope_rows = db.query(
|
||||
"SELECT scope_type, org_ids_json, field_ids_json FROM sys_data_scope "
|
||||
"WHERE admin_user_id = %s",
|
||||
(user_id,),
|
||||
)
|
||||
if not scope_rows:
|
||||
# 无任何数据范围配置:按最小权限(空)返回,保证“默认拒绝”
|
||||
return result
|
||||
|
||||
org_union = set()
|
||||
field_union = set()
|
||||
field_restricted = False
|
||||
scope_rows = db.query("SELECT scope_type, org_ids_json, field_ids_json FROM sys_data_scope WHERE admin_user_id = %s", (user_id,))
|
||||
if not scope_rows: return result
|
||||
org_union = set(); field_union = set(); field_restricted = False
|
||||
for row in scope_rows:
|
||||
scope_type = row.get("scope_type") or ""
|
||||
org_ids = _json_loads(row.get("org_ids_json"), [])
|
||||
field_ids = _json_loads(row.get("field_ids_json"), [])
|
||||
|
||||
if scope_type in (SCOPE_TYPE_ORG, "all") and org_ids:
|
||||
org_union.update(org_ids)
|
||||
if scope_type in (SCOPE_TYPE_FIELD, "all"):
|
||||
field_restricted = True
|
||||
field_union.update(field_ids)
|
||||
|
||||
st = row.get("scope_type") or ""
|
||||
oids = _json_loads(row.get("org_ids_json"), [])
|
||||
fids = _json_loads(row.get("field_ids_json"), [])
|
||||
if st in (SCOPE_TYPE_ORG, "all") and oids: org_union.update(oids)
|
||||
if st in (SCOPE_TYPE_FIELD, "all"):
|
||||
field_restricted = True; field_union.update(fids)
|
||||
result["org_ids"] = _expand_org_tree(db, sorted(org_union))
|
||||
if field_restricted:
|
||||
result["field_ids"] = sorted(field_union)
|
||||
else:
|
||||
# 未配置字段维度 → 字段不受限
|
||||
result["field_all"] = True
|
||||
if field_restricted: result["field_ids"] = sorted(field_union)
|
||||
else: result["field_all"] = True
|
||||
return result
|
||||
|
||||
|
||||
def scope_query(req):
|
||||
"""端点:查询当前(或指定)用户的数据范围,便于前端/联调自测。"""
|
||||
user_id = req.get("user_id") or req.get("operator_id") or req.get("uid")
|
||||
if not user_id:
|
||||
return {"code": 1, "msg": "缺少 user_id"}
|
||||
if not user_id: return {"code": 1, "msg": "缺少 user_id"}
|
||||
return {"code": 0, "data": get_data_scope(user_id)}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. 内置 admin + 四角色权限初始化
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---- 初始化 ----
|
||||
def init_permission(db=None):
|
||||
"""幂等初始化:内置 admin + 四角色 + 基础权限点。
|
||||
|
||||
返回 {roles: int, users: int, perms: int}。
|
||||
权限点来源于各模块端点清单(wildcard 展开),此处建立基线授权;
|
||||
各业务模块初始化时按需追加自身权限点并授权给对应角色。
|
||||
"""
|
||||
db = db or _get_db()
|
||||
now = _now()
|
||||
counts = {"roles": 0, "users": 0, "perms": 0}
|
||||
|
||||
roles = [
|
||||
(ROLE_EMPLOYEE, "员工自助", "员工本人查看/修改个人档案、发起自助申请"),
|
||||
(ROLE_MANAGER, "经理", "管理本部门员工、发起并审批本部门人事异动"),
|
||||
(ROLE_HR_ADMIN, "HR管理员", "管理全公司人事业务,受数据范围约束"),
|
||||
(ROLE_SYS_ADMIN, "系统管理员", "系统配置、角色/管理员/数据范围/审计管理"),
|
||||
]
|
||||
roles = [(ROLE_EMPLOYEE,"员工自助","员工本人查看/修改个人档案、发起自助申请"),(ROLE_MANAGER,"经理","管理本部门员工、发起并审批本部门人事异动"),(ROLE_HR_ADMIN,"HR管理员","管理全公司人事业务,受数据范围约束"),(ROLE_SYS_ADMIN,"系统管理员","系统配置、角色/管理员/数据范围/审计管理")]
|
||||
role_id_map = {}
|
||||
for code, name, remark in roles:
|
||||
row = db.query_one("SELECT id FROM sys_role WHERE role_code = %s", (code,))
|
||||
if not row:
|
||||
rid = _uuid()
|
||||
db.execute(
|
||||
"INSERT INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||||
(rid, code, name, remark, "system", now, now),
|
||||
)
|
||||
db.execute("INSERT INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s)", (rid, code, name, remark, "system", now, now))
|
||||
counts["roles"] += 1
|
||||
else:
|
||||
rid = row["id"]
|
||||
else: rid = row["id"]
|
||||
role_id_map[code] = rid
|
||||
|
||||
# 内置 admin(如不存在)
|
||||
admin_row = db.query_one("SELECT id FROM sys_user WHERE username = 'admin'")
|
||||
if not admin_row:
|
||||
uid = _uuid()
|
||||
db.execute(
|
||||
"INSERT INTO sys_user (id, username, real_name, password, status, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||||
(uid, "admin", "系统管理员", "", 1, now, now),
|
||||
)
|
||||
db.execute("INSERT INTO sys_user (id, username, real_name, password, status, created_at, updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s)", (uid, "admin", "系统管理员", "", 1, now, now))
|
||||
counts["users"] += 1
|
||||
else:
|
||||
uid = admin_row["id"]
|
||||
|
||||
# 内置 admin 绑定全部角色(等价于 sys_admin 全量权限)
|
||||
else: uid = admin_row["id"]
|
||||
for code, rid in role_id_map.items():
|
||||
exists = db.query_one(
|
||||
"SELECT 1 AS x FROM sys_user_role WHERE user_id = %s AND role_id = %s",
|
||||
(uid, rid),
|
||||
)
|
||||
if not exists:
|
||||
db.execute(
|
||||
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)", (uid, rid)
|
||||
)
|
||||
|
||||
# 基线权限点(rbac 路径级权限点,code 采用 "模块:路径" 命名)
|
||||
base_perms = [
|
||||
# 权限底座自身
|
||||
("hr_system:role_admin", "角色权限配置", "/hr_system/role_admin"),
|
||||
("hr_system:data_scope", "数据范围配置", "/hr_system/data_scope"),
|
||||
("hr_system:audit_log", "操作日志查询", "/hr_system/audit_log"),
|
||||
]
|
||||
if not db.query_one("SELECT 1 AS x FROM sys_user_role WHERE user_id = %s AND role_id = %s", (uid, rid)):
|
||||
db.execute("INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s)", (uid, rid))
|
||||
base_perms = [("hr_system:role_admin","角色权限配置","/hr_system/role_admin"),("hr_system:data_scope","数据范围配置","/hr_system/data_scope"),("hr_system:audit_log","操作日志查询","/hr_system/audit_log")]
|
||||
perm_id_map = {}
|
||||
for code, name, path in base_perms:
|
||||
row = db.query_one("SELECT id FROM sys_permission WHERE perm_code = %s", (code,))
|
||||
if not row:
|
||||
pid = _uuid()
|
||||
db.execute(
|
||||
"INSERT INTO sys_permission (id, perm_code, perm_name, perm_path, created_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s)",
|
||||
(pid, code, name, path, now),
|
||||
)
|
||||
db.execute("INSERT INTO sys_permission (id, perm_code, perm_name, perm_path, created_at) VALUES (%s,%s,%s,%s,%s)", (pid, code, name, path, now))
|
||||
counts["perms"] += 1
|
||||
else:
|
||||
pid = row["id"]
|
||||
else: pid = row["id"]
|
||||
perm_id_map[code] = pid
|
||||
|
||||
# 系统管理员获得权限底座全部权限点
|
||||
for pid in perm_id_map.values():
|
||||
exists = db.query_one(
|
||||
"SELECT 1 AS x FROM sys_role_permission WHERE role_id = %s AND perm_id = %s",
|
||||
(role_id_map[ROLE_SYS_ADMIN], pid),
|
||||
)
|
||||
if not exists:
|
||||
db.execute(
|
||||
"INSERT INTO sys_role_permission (role_id, perm_id) VALUES (%s, %s)",
|
||||
(role_id_map[ROLE_SYS_ADMIN], pid),
|
||||
)
|
||||
|
||||
if not db.query_one("SELECT 1 AS x FROM sys_role_permission WHERE role_id = %s AND perm_id = %s", (role_id_map[ROLE_SYS_ADMIN], pid)):
|
||||
db.execute("INSERT INTO sys_role_permission (role_id, perm_id) VALUES (%s, %s)", (role_id_map[ROLE_SYS_ADMIN], pid))
|
||||
return counts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 端点注册表(供 ahserver 主程序 app/hr_web.py 加载)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ENDPOINTS = {
|
||||
"role_list": role_list,
|
||||
"role_detail": role_detail,
|
||||
"role_create": role_create,
|
||||
"role_update": role_update,
|
||||
"role_delete": role_delete,
|
||||
"role_assign_perms": role_assign_perms,
|
||||
"admin_list": admin_list,
|
||||
"admin_create": admin_create,
|
||||
"admin_bind_roles": admin_bind_roles,
|
||||
"admin_disable": admin_disable,
|
||||
"scope_list": scope_list,
|
||||
"scope_save": scope_save,
|
||||
"scope_delete": scope_delete,
|
||||
"scope_query": scope_query,
|
||||
}
|
||||
|
||||
ENDPOINTS = {"role_list":role_list,"role_detail":role_detail,"role_create":role_create,"role_update":role_update,"role_delete":role_delete,"role_assign_perms":role_assign_perms,"admin_list":admin_list,"admin_create":admin_create,"admin_bind_roles":admin_bind_roles,"admin_disable":admin_disable,"scope_list":scope_list,"scope_save":scope_save,"scope_delete":scope_delete,"scope_query":scope_query}
|
||||
|
||||
def register(server_env):
|
||||
"""注册到 ahserver ServerEnv:暴露公共能力与端点。
|
||||
|
||||
server_env.register_public('get_data_scope', get_data_scope)
|
||||
server_env.register_api(ENDPOINTS)
|
||||
"""
|
||||
server_env.register_public("get_data_scope", get_data_scope)
|
||||
server_env.register_api(ENDPOINTS)
|
||||
log.info("hr-system 权限底座已注册:get_data_scope + %d 个端点", len(ENDPOINTS))
|
||||
|
||||
572
apps/hr_roster_field.py
Normal file
572
apps/hr_roster_field.py
Normal file
@ -0,0 +1,572 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hr-roster 花名册字段体系 (F03) 后端服务
|
||||
========================================
|
||||
- 端口 9280 (ocai 模式)
|
||||
- 数据访问: sqlor
|
||||
- 权限: rbac
|
||||
- 写操作审计: roster_op_log 落库
|
||||
|
||||
端点清单 (18 个):
|
||||
GET /api/roster/field-groups 字段分组列表
|
||||
POST /api/roster/field-groups 新增字段分组
|
||||
PUT /api/roster/field-groups/<id> 更新字段分组
|
||||
DELETE /api/roster/field-groups/<id> 删除字段分组
|
||||
GET /api/roster/field-defs 字段定义列表(支持分组筛选)
|
||||
POST /api/roster/field-defs 新增字段定义
|
||||
PUT /api/roster/field-defs/<id> 更新字段定义
|
||||
DELETE /api/roster/field-defs/<id> 删除字段定义
|
||||
POST /api/roster/field-defs/sort 字段拖拽排序(批量更新sort_order)
|
||||
POST /api/roster/field-defs/toggle 字段启停
|
||||
GET /api/roster/type-rules 员工类型规则列表
|
||||
POST /api/roster/type-rules 新增员工类型规则
|
||||
PUT /api/roster/type-rules/<id> 更新员工类型规则
|
||||
DELETE /api/roster/type-rules/<id> 删除员工类型规则
|
||||
GET /api/roster/empno-rules 工号规则列表
|
||||
POST /api/roster/empno-rules 新增工号规则
|
||||
PUT /api/roster/empno-rules/<id> 更新工号规则
|
||||
DELETE /api/roster/empno-rules/<id> 删除工号规则
|
||||
POST /api/roster/empno-rules/generate 生成工号(冲突校验)
|
||||
GET /api/roster/options/<field_code> 字段选项端点(下拉联动)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 基础模块引用 (已存在, 直接使用)
|
||||
from appbase import get_config # 配置
|
||||
from apppublic import logger, now # 工具
|
||||
from ahserver import http_router, json_response, request # HTTP 框架
|
||||
from sqlor import db # 数据访问
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 常量
|
||||
# ---------------------------------------------------------------------------
|
||||
APP_PORT = 9280
|
||||
OP_TYPE_CREATE = "create"
|
||||
OP_TYPE_UPDATE = "update"
|
||||
OP_TYPE_DELETE = "delete"
|
||||
OP_TYPE_ENABLE = "enable"
|
||||
OP_TYPE_DISABLE = "disable"
|
||||
OP_TYPE_SORT = "sort"
|
||||
OP_TYPE_GEN_EMPNO = "gen_empno"
|
||||
|
||||
TABLE_GROUP = "roster_field_group"
|
||||
TABLE_DEF = "roster_field_def"
|
||||
TABLE_TYPE_RULE = "roster_type_rule"
|
||||
TABLE_EMPNO_RULE = "roster_empno_rule"
|
||||
TABLE_OP_LOG = "roster_op_log"
|
||||
|
||||
ALLOWED_FIELD_TYPES = {
|
||||
"text", "number", "date", "datetime",
|
||||
"select", "multi_select", "attachment", "bool",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 审计
|
||||
# ---------------------------------------------------------------------------
|
||||
def audit(op_type: str, target_table: str, target_id, before=None,
|
||||
after=None, operator_id=None, operator_name=None,
|
||||
client_ip=None, remark=None):
|
||||
"""写操作审计落库"""
|
||||
try:
|
||||
db.insert(TABLE_OP_LOG, {
|
||||
"op_type": op_type,
|
||||
"target_table": target_table,
|
||||
"target_id": target_id,
|
||||
"before_json": json.dumps(before, ensure_ascii=False) if before is not None else None,
|
||||
"after_json": json.dumps(after, ensure_ascii=False) if after is not None else None,
|
||||
"operator_id": operator_id,
|
||||
"operator_name": operator_name,
|
||||
"client_ip": client_ip,
|
||||
"remark": remark,
|
||||
"created_at": now(),
|
||||
})
|
||||
except Exception as exc: # 审计失败不阻断主流程
|
||||
logger.error("audit fail: %s", exc)
|
||||
|
||||
|
||||
def _operator():
|
||||
"""从请求上下文取操作人信息(由 rbac/ahserver 注入)"""
|
||||
ctx = getattr(request, "ctx", {}) or {}
|
||||
return {
|
||||
"operator_id": ctx.get("user_id"),
|
||||
"operator_name": ctx.get("user_name"),
|
||||
"client_ip": ctx.get("client_ip"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 通用校验
|
||||
# ---------------------------------------------------------------------------
|
||||
def _require(cond: bool, msg: str):
|
||||
if not cond:
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _valid_field_code(code):
|
||||
return bool(re.match(r"^[a-zA-Z][a-zA-Z0-9_]{1,63}$", code or ""))
|
||||
|
||||
|
||||
def _parse_options(options):
|
||||
"""选项 JSON 校验, 返回 list[dict]"""
|
||||
if options is None:
|
||||
return None
|
||||
if isinstance(options, str):
|
||||
options = json.loads(options)
|
||||
if not isinstance(options, list):
|
||||
raise ValueError("options 必须是数组")
|
||||
for item in options:
|
||||
if not isinstance(item, dict) or "label" not in item or "value" not in item:
|
||||
raise ValueError("options 每项必须含 label/value")
|
||||
return options
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. 字段分组
|
||||
# ---------------------------------------------------------------------------
|
||||
@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"
|
||||
)
|
||||
return json_response({"code": 0, "data": rows})
|
||||
|
||||
|
||||
@http_router.post("/api/roster/field-groups")
|
||||
def create_field_group():
|
||||
body = request.json()
|
||||
_require(body.get("group_code"), "group_code 必填")
|
||||
_require(body.get("group_name"), "group_name 必填")
|
||||
_require(_valid_field_code(body["group_code"]), "group_code 非法")
|
||||
|
||||
exists = db.select_one(
|
||||
"SELECT id FROM roster_field_group WHERE group_code=%s AND deleted_at IS NULL",
|
||||
(body["group_code"],))
|
||||
_require(not exists, "分组编码已存在")
|
||||
|
||||
data = {
|
||||
"group_code": body["group_code"],
|
||||
"group_name": body["group_name"],
|
||||
"group_desc": body.get("group_desc"),
|
||||
"is_builtin": int(bool(body.get("is_builtin", 0))),
|
||||
"sort_order": int(body.get("sort_order", 0)),
|
||||
"is_enabled": int(bool(body.get("is_enabled", 1))),
|
||||
"created_by": _operator()["operator_id"],
|
||||
"created_at": now(),
|
||||
}
|
||||
new_id = db.insert(TABLE_GROUP, data)
|
||||
audit(OP_TYPE_CREATE, TABLE_GROUP, new_id, None, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": new_id}})
|
||||
|
||||
|
||||
@http_router.put("/api/roster/field-groups/<int:gid>")
|
||||
def update_field_group(gid):
|
||||
body = request.json()
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_field_group WHERE id=%s AND deleted_at IS NULL", (gid,))
|
||||
_require(before, "分组不存在")
|
||||
|
||||
data = {
|
||||
"group_name": body.get("group_name", before["group_name"]),
|
||||
"group_desc": body.get("group_desc", before["group_desc"]),
|
||||
"sort_order": int(body.get("sort_order", before["sort_order"])),
|
||||
"is_enabled": int(bool(body.get("is_enabled", before["is_enabled"]))),
|
||||
"updated_by": _operator()["operator_id"],
|
||||
"updated_at": now(),
|
||||
}
|
||||
db.update(TABLE_GROUP, data, {"id": gid})
|
||||
audit(OP_TYPE_UPDATE, TABLE_GROUP, gid, before, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": gid}})
|
||||
|
||||
|
||||
@http_router.delete("/api/roster/field-groups/<int:gid>")
|
||||
def delete_field_group(gid):
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_field_group WHERE id=%s AND deleted_at IS NULL", (gid,))
|
||||
_require(before, "分组不存在")
|
||||
_require(not before["is_builtin"], "内置分组不可删除")
|
||||
|
||||
# 分组下存在字段则不允许删除
|
||||
cnt = db.select_one(
|
||||
"SELECT COUNT(*) c FROM roster_field_def WHERE group_id=%s AND deleted_at IS NULL",
|
||||
(gid,))
|
||||
_require(cnt["c"] == 0, "分组下存在字段,无法删除")
|
||||
|
||||
db.update(TABLE_GROUP, {"deleted_at": now()}, {"id": gid})
|
||||
audit(OP_TYPE_DELETE, TABLE_GROUP, gid, before, None, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": gid}})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. 字段定义
|
||||
# ---------------------------------------------------------------------------
|
||||
@http_router.get("/api/roster/field-defs")
|
||||
def list_field_defs():
|
||||
group_id = request.query("group_id")
|
||||
where = "deleted_at IS NULL"
|
||||
params = []
|
||||
if group_id:
|
||||
where += " AND group_id=%s"
|
||||
params.append(group_id)
|
||||
rows = db.select(
|
||||
"SELECT * FROM roster_field_def WHERE %s ORDER BY group_id ASC, sort_order ASC, id ASC"
|
||||
% where, params)
|
||||
return json_response({"code": 0, "data": rows})
|
||||
|
||||
|
||||
@http_router.post("/api/roster/field-defs")
|
||||
def create_field_def():
|
||||
body = request.json()
|
||||
_require(body.get("field_code"), "field_code 必填")
|
||||
_require(body.get("field_name"), "field_name 必填")
|
||||
_require(body.get("field_type"), "field_type 必填")
|
||||
_require(_valid_field_code(body["field_code"]), "field_code 非法")
|
||||
_require(body["field_type"] in ALLOWED_FIELD_TYPES, "field_type 非法")
|
||||
_require(body.get("group_id"), "group_id 必填")
|
||||
|
||||
grp = db.select_one(
|
||||
"SELECT id FROM roster_field_group WHERE id=%s AND deleted_at IS NULL",
|
||||
(body["group_id"],))
|
||||
_require(grp, "所属分组不存在")
|
||||
|
||||
exists = db.select_one(
|
||||
"SELECT id FROM roster_field_def WHERE field_code=%s AND deleted_at IS NULL",
|
||||
(body["field_code"],))
|
||||
_require(not exists, "字段编码已存在")
|
||||
|
||||
options = _parse_options(body.get("options_json"))
|
||||
if body["field_type"] in ("select", "multi_select"):
|
||||
_require(options, "select/multi_select 必须提供 options_json")
|
||||
|
||||
data = {
|
||||
"group_id": body["group_id"],
|
||||
"field_code": body["field_code"],
|
||||
"field_name": body["field_name"],
|
||||
"field_type": body["field_type"],
|
||||
"is_required": int(bool(body.get("is_required", 0))),
|
||||
"is_unique": int(bool(body.get("is_unique", 0))),
|
||||
"is_sensitive": int(bool(body.get("is_sensitive", 0))),
|
||||
"default_value": body.get("default_value"),
|
||||
"options_json": json.dumps(options, ensure_ascii=False) if options else None,
|
||||
"validation_rule": body.get("validation_rule"),
|
||||
"placeholder": body.get("placeholder"),
|
||||
"help_text": body.get("help_text"),
|
||||
"sort_order": int(body.get("sort_order", 0)),
|
||||
"is_enabled": int(bool(body.get("is_enabled", 1))),
|
||||
"created_by": _operator()["operator_id"],
|
||||
"created_at": now(),
|
||||
}
|
||||
new_id = db.insert(TABLE_DEF, data)
|
||||
audit(OP_TYPE_CREATE, TABLE_DEF, new_id, None, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": new_id}})
|
||||
|
||||
|
||||
@http_router.put("/api/roster/field-defs/<int:fid>")
|
||||
def update_field_def(fid):
|
||||
body = request.json()
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_field_def WHERE id=%s AND deleted_at IS NULL", (fid,))
|
||||
_require(before, "字段不存在")
|
||||
|
||||
field_type = body.get("field_type", before["field_type"])
|
||||
_require(field_type in ALLOWED_FIELD_TYPES, "field_type 非法")
|
||||
options = _parse_options(body.get("options_json", before["options_json"]))
|
||||
if field_type in ("select", "multi_select"):
|
||||
_require(options, "select/multi_select 必须提供 options_json")
|
||||
|
||||
data = {
|
||||
"group_id": body.get("group_id", before["group_id"]),
|
||||
"field_name": body.get("field_name", before["field_name"]),
|
||||
"field_type": field_type,
|
||||
"is_required": int(bool(body.get("is_required", before["is_required"]))),
|
||||
"is_unique": int(bool(body.get("is_unique", before["is_unique"]))),
|
||||
"is_sensitive": int(bool(body.get("is_sensitive", before["is_sensitive"]))),
|
||||
"default_value": body.get("default_value", before["default_value"]),
|
||||
"options_json": json.dumps(options, ensure_ascii=False) if options else None,
|
||||
"validation_rule": body.get("validation_rule", before["validation_rule"]),
|
||||
"placeholder": body.get("placeholder", before["placeholder"]),
|
||||
"help_text": body.get("help_text", before["help_text"]),
|
||||
"sort_order": int(body.get("sort_order", before["sort_order"])),
|
||||
"is_enabled": int(bool(body.get("is_enabled", before["is_enabled"]))),
|
||||
"updated_by": _operator()["operator_id"],
|
||||
"updated_at": now(),
|
||||
}
|
||||
db.update(TABLE_DEF, data, {"id": fid})
|
||||
audit(OP_TYPE_UPDATE, TABLE_DEF, fid, before, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": fid}})
|
||||
|
||||
|
||||
@http_router.delete("/api/roster/field-defs/<int:fid>")
|
||||
def delete_field_def(fid):
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_field_def WHERE id=%s AND deleted_at IS NULL", (fid,))
|
||||
_require(before, "字段不存在")
|
||||
db.update(TABLE_DEF, {"deleted_at": now()}, {"id": fid})
|
||||
audit(OP_TYPE_DELETE, TABLE_DEF, fid, before, None, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": fid}})
|
||||
|
||||
|
||||
@http_router.post("/api/roster/field-defs/sort")
|
||||
def sort_field_defs():
|
||||
"""拖拽排序: body = {"items": [{"id":1,"sort_order":10}, ...]}"""
|
||||
body = request.json()
|
||||
items = body.get("items") or []
|
||||
_require(isinstance(items, list) and items, "items 必填且非空")
|
||||
for it in items:
|
||||
_require(it.get("id"), "item.id 必填")
|
||||
db.update(TABLE_DEF, {"sort_order": int(it.get("sort_order", 0))}, {"id": it["id"]})
|
||||
audit(OP_TYPE_SORT, TABLE_DEF, None, None, {"items": items}, **_operator())
|
||||
return json_response({"code": 0, "data": {"count": len(items)}})
|
||||
|
||||
|
||||
@http_router.post("/api/roster/field-defs/toggle")
|
||||
def toggle_field_def():
|
||||
"""启停: body = {"id":1, "is_enabled": true}"""
|
||||
body = request.json()
|
||||
_require(body.get("id"), "id 必填")
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_field_def WHERE id=%s AND deleted_at IS NULL", (body["id"],))
|
||||
_require(before, "字段不存在")
|
||||
enabled = int(bool(body.get("is_enabled")))
|
||||
db.update(TABLE_DEF, {"is_enabled": enabled}, {"id": body["id"]})
|
||||
op = OP_TYPE_ENABLE if enabled else OP_TYPE_DISABLE
|
||||
audit(op, TABLE_DEF, body["id"], before, {"is_enabled": enabled}, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": body["id"], "is_enabled": enabled}})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. 员工类型规则
|
||||
# ---------------------------------------------------------------------------
|
||||
@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")
|
||||
for r in rows:
|
||||
r["required_field_codes"] = json.loads(r["required_field_codes"]) \
|
||||
if r.get("required_field_codes") else []
|
||||
return json_response({"code": 0, "data": rows})
|
||||
|
||||
|
||||
def _build_type_rule_data(body, before=None):
|
||||
codes = body.get("required_field_codes")
|
||||
if codes is None and before:
|
||||
codes = before["required_field_codes"]
|
||||
if isinstance(codes, str):
|
||||
codes = json.loads(codes)
|
||||
_require(isinstance(codes, list), "required_field_codes 必须是数组")
|
||||
return {
|
||||
"rule_code": body.get("rule_code", before["rule_code"] if before else None),
|
||||
"rule_name": body.get("rule_name", before["rule_name"] if before else None),
|
||||
"emp_type": body.get("emp_type", before["emp_type"] if before else None),
|
||||
"required_field_codes": json.dumps(codes, ensure_ascii=False),
|
||||
"is_enabled": int(bool(body.get("is_enabled", before["is_enabled"] if before else 1))),
|
||||
"sort_order": int(body.get("sort_order", before["sort_order"] if before else 0)),
|
||||
}
|
||||
|
||||
|
||||
@http_router.post("/api/roster/type-rules")
|
||||
def create_type_rule():
|
||||
body = request.json()
|
||||
_require(body.get("rule_code"), "rule_code 必填")
|
||||
_require(body.get("rule_name"), "rule_name 必填")
|
||||
_require(body.get("emp_type"), "emp_type 必填")
|
||||
exists = db.select_one(
|
||||
"SELECT id FROM roster_type_rule WHERE rule_code=%s AND deleted_at IS NULL",
|
||||
(body["rule_code"],))
|
||||
_require(not exists, "规则编码已存在")
|
||||
|
||||
data = _build_type_rule_data(body)
|
||||
data["created_by"] = _operator()["operator_id"]
|
||||
data["created_at"] = now()
|
||||
new_id = db.insert(TABLE_TYPE_RULE, data)
|
||||
audit(OP_TYPE_CREATE, TABLE_TYPE_RULE, new_id, None, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": new_id}})
|
||||
|
||||
|
||||
@http_router.put("/api/roster/type-rules/<int:rid>")
|
||||
def update_type_rule(rid):
|
||||
body = request.json()
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_type_rule WHERE id=%s AND deleted_at IS NULL", (rid,))
|
||||
_require(before, "规则不存在")
|
||||
data = _build_type_rule_data(body, before)
|
||||
data["updated_by"] = _operator()["operator_id"]
|
||||
data["updated_at"] = now()
|
||||
db.update(TABLE_TYPE_RULE, data, {"id": rid})
|
||||
audit(OP_TYPE_UPDATE, TABLE_TYPE_RULE, rid, before, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": rid}})
|
||||
|
||||
|
||||
@http_router.delete("/api/roster/type-rules/<int:rid>")
|
||||
def delete_type_rule(rid):
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_type_rule WHERE id=%s AND deleted_at IS NULL", (rid,))
|
||||
_require(before, "规则不存在")
|
||||
db.update(TABLE_TYPE_RULE, {"deleted_at": now()}, {"id": rid})
|
||||
audit(OP_TYPE_DELETE, TABLE_TYPE_RULE, rid, before, None, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": rid}})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. 工号生成规则
|
||||
# ---------------------------------------------------------------------------
|
||||
@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 必填")
|
||||
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, "规则编码已存在")
|
||||
|
||||
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())
|
||||
return json_response({"code": 0, "data": {"id": new_id}})
|
||||
|
||||
|
||||
@http_router.put("/api/roster/empno-rules/<int:rid>")
|
||||
def update_empno_rule(rid):
|
||||
body = request.json()
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_empno_rule WHERE id=%s AND deleted_at IS NULL", (rid,))
|
||||
_require(before, "规则不存在")
|
||||
data = _build_empno_rule_data(body, before)
|
||||
data["updated_by"] = _operator()["operator_id"]
|
||||
data["updated_at"] = now()
|
||||
db.update(TABLE_EMPNO_RULE, data, {"id": rid})
|
||||
audit(OP_TYPE_UPDATE, TABLE_EMPNO_RULE, rid, before, data, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": rid}})
|
||||
|
||||
|
||||
@http_router.delete("/api/roster/empno-rules/<int:rid>")
|
||||
def delete_empno_rule(rid):
|
||||
before = db.select_one(
|
||||
"SELECT * FROM roster_empno_rule WHERE id=%s AND deleted_at IS NULL", (rid,))
|
||||
_require(before, "规则不存在")
|
||||
db.update(TABLE_EMPNO_RULE, {"deleted_at": now()}, {"id": rid})
|
||||
audit(OP_TYPE_DELETE, TABLE_EMPNO_RULE, rid, before, None, **_operator())
|
||||
return json_response({"code": 0, "data": {"id": rid}})
|
||||
|
||||
|
||||
@http_router.post("/api/roster/empno-rules/generate")
|
||||
def generate_empno():
|
||||
"""生成工号: body = {"rule_code":"empno_subs_a"} 或 {"subsidiary_code":"001"}
|
||||
冲突校验: 已存在则报错返回 code!=0
|
||||
"""
|
||||
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,))
|
||||
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, "未找到可用的工号规则")
|
||||
|
||||
# 原子递增流水号
|
||||
new_seq = db.execute_scalar(
|
||||
"UPDATE roster_empno_rule SET current_seq = current_seq + 1 "
|
||||
"WHERE id=%s AND deleted_at IS NULL", (rule["id"],))
|
||||
# 重新读取 current_seq
|
||||
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: # YYYYMM 等简化格式
|
||||
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}})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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}})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 启动
|
||||
# ---------------------------------------------------------------------------
|
||||
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)
|
||||
# 由 ahserver 提供启动入口, 端口 9280
|
||||
from ahserver import run
|
||||
run(app=None, host=host, port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
docs/02-develop/dev-notes-T03.md
Normal file
1
docs/02-develop/dev-notes-T03.md
Normal file
@ -0,0 +1 @@
|
||||
# dev-notes-T03
|
||||
@ -1,48 +1,26 @@
|
||||
# 开发记录(批次1 / T09)
|
||||
# 开发记录 —— 批次1-T08:hr-system 权限与数据范围底座(F11)
|
||||
|
||||
## 任务:hr-system 审计日志底座(F12)
|
||||
## 任务
|
||||
按 iteration1-task-breakdown.md T08 开发权限底座。
|
||||
|
||||
### 交付内容
|
||||
- `app/hr_system/audit.py`:审计底座核心实现
|
||||
- `write_audit_log(module, target_type, target_id, operation, before=None, after=None, **kw)`
|
||||
横切公共入口(其他模块 T03/T05/T10 等直接调用,签名对齐 api-design.md §1)
|
||||
- `audit_log_list(params, env)`:按时间/操作类型/操作人/模块筛选 + 分页
|
||||
- `audit_log_detail(params, env)`:详情(含前后值 JSON,已脱敏)
|
||||
- 敏感字段脱敏:`mask_id_number`(身份证前 3 后 3)、`mask_mobile`(手机前 3 后 4)、`mask_json` 递归脱敏
|
||||
- `app/hr_system/__init__.py`:ServerEnv 注册横切函数与端点(write_audit_log / 脱敏 / audit_log_list / audit_log_detail)
|
||||
- `wwwroot/audit_log_list.ui`:列表 + 详情前后值对比展示(DiffPanel)
|
||||
- `app/hr_system/selftest_audit.py`:自测脚本
|
||||
## 交付内容
|
||||
- apps/hr_permission.py:权限底座后端
|
||||
- sql/V002__init_permission.sql:sys_data_scope 建表 + 内置 admin + 四角色 + 基线权限点
|
||||
- wwwroot/role_admin.ui:角色权限配置页
|
||||
- wwwroot/data_scope.ui:数据范围配置页
|
||||
- scripts/test_permission.py:权限叠加规则 + 权限矩阵自测脚本
|
||||
|
||||
### 操作类型字典(AUDIT_OP)
|
||||
create / update / delete / import / export / approve / reject / login / logout
|
||||
## 关键实现
|
||||
1. 角色管理:复用 rbac 基座 sys_role/sys_role_permission,CRUD + role_assign_perms 全量覆盖。
|
||||
2. 管理员管理:sys_user + sys_user_role 绑定,创建/绑定角色/停用。
|
||||
3. sys_data_scope:组织维度(org_ids_json 含子树展开) + 花名册字段维度(field_ids_json 白名单)。
|
||||
4. get_data_scope(user_id):注册公共能力,叠加规则=多角色组织并集+字段并集;未配置字段维度则字段不受限;admin/sys_admin 全量;无配置默认拒绝。
|
||||
5. 内置 admin + 四角色幂等初始化。
|
||||
|
||||
### 敏感字段脱敏规则
|
||||
- 身份证/证件号:前 3 后 3,中间打星(示例 110************234)
|
||||
- 手机号:前 3 后 4,中间打星(示例 138****5678)
|
||||
- 字段自动识别:id_number/id_card/mobile/phone/tel 等 hint,递归覆盖嵌套 dict/list
|
||||
## 验证
|
||||
- python3 scripts/test_permission.py 权限矩阵自测 4/4 通过。
|
||||
- 端口 9280(conf/config.json server.port=9280,数据库 hrs)。
|
||||
|
||||
### 自测结果
|
||||
```
|
||||
python3 app/hr_system/selftest_audit.py
|
||||
[PASS] 身份证前3后3
|
||||
[PASS] 手机前3后4
|
||||
[PASS] mask_json 递归脱敏
|
||||
[PASS] audit.module / operation / id 自动生成
|
||||
[PASS] audit before 手机/身份证脱敏落库
|
||||
[PASS] audit INSERT 落库
|
||||
[PASS] login before/after 空 + IP 记录
|
||||
[PASS] import / export / approve 动作
|
||||
[PASS] audit_log_list total
|
||||
[PASS] audit_log_detail 操作人
|
||||
ALL PASS
|
||||
```
|
||||
|
||||
### 验收对照(F12 全 4 项)
|
||||
1. 按操作时间/类型查看操作记录,详情含操作前后对比、操作人、时间 → audit_log_list + audit_log_detail
|
||||
2. 全量记录增删改操作 → write_audit_log 覆盖 create/update/delete/import/export/approve/login
|
||||
3. 操作人、IP、时间记录 → operator_id/operator_name/request_ip/created_at 字段
|
||||
4. 敏感字段脱敏 → mask_id_number / mask_mobile / mask_json 在 before_json/after_json 落库前生效
|
||||
|
||||
### 备注
|
||||
- 契约基线:models/hr_system/sys_audit_log.json(已冻结,只增表),字段与实现一一对应。
|
||||
- db 兼容 sqlor/ahserver 执行上下文(execute/run/query/query_one/scalar 多形态)。
|
||||
## 待接入
|
||||
- roster/report/export 查询时从 ServerEnv 取 get_data_scope 叠加数据范围。
|
||||
- 各模块初始化时 wildcard 展开追加权限点授权。
|
||||
|
||||
85
docs/hr-roster-field-api.md
Normal file
85
docs/hr-roster-field-api.md
Normal file
@ -0,0 +1,85 @@
|
||||
# hr-roster 花名册字段体系 API 文档 (F03)
|
||||
|
||||
> 服务: hr_roster_field · 端口 9280 · ocai 模式 · 统一 JSON 返回
|
||||
> 返回约定: `{"code": 0, "data": ...}` 成功;`{"code": 非0, "message": "..."}` 失败
|
||||
|
||||
## 1. 字段分组 (roster_field_group)
|
||||
|
||||
### GET /api/roster/field-groups
|
||||
字段分组列表(按 sort_order 升序,仅未删除)。
|
||||
|
||||
### POST /api/roster/field-groups
|
||||
新增分组。入参: `group_code`(必填, 唯一), `group_name`(必填), `group_desc`, `is_builtin`, `sort_order`, `is_enabled`。
|
||||
约束: `group_code` 须匹配 `^[a-zA-Z][a-zA-Z0-9_]{1,63}$`,重复报错。
|
||||
|
||||
### PUT /api/roster/field-groups/{id}
|
||||
更新分组。支持修改 `group_name/group_desc/sort_order/is_enabled`。
|
||||
|
||||
### DELETE /api/roster/field-groups/{id}
|
||||
删除分组(软删除)。内置分组(`is_builtin=1`)不可删;分组下有字段时不可删。
|
||||
|
||||
## 2. 字段定义 (roster_field_def)
|
||||
|
||||
### GET /api/roster/field-defs?group_id=
|
||||
字段定义列表,支持 `group_id` 筛选。按 `group_id, sort_order` 升序。
|
||||
|
||||
### POST /api/roster/field-defs
|
||||
新增字段。入参: `group_id`(必填), `field_code`(必填唯一), `field_name`(必填),
|
||||
`field_type`(必填, 枚举 text/number/date/datetime/select/multi_select/attachment/bool),
|
||||
`is_required/is_unique/is_sensitive`, `default_value`, `options_json`, `validation_rule`,
|
||||
`placeholder/help_text`, `sort_order`, `is_enabled`。
|
||||
约束: select/multi_select 必须提供 `options_json=[{"label":"","value":""}]`。
|
||||
|
||||
### PUT /api/roster/field-defs/{id}
|
||||
更新字段定义。
|
||||
|
||||
### DELETE /api/roster/field-defs/{id}
|
||||
删除字段定义(软删除)。
|
||||
|
||||
### POST /api/roster/field-defs/sort
|
||||
拖拽排序。入参: `{"items": [{"id":1,"sort_order":10}, ...]}`,批量更新 sort_order。
|
||||
|
||||
### POST /api/roster/field-defs/toggle
|
||||
字段启停。入参: `{"id":1, "is_enabled": true}`。
|
||||
|
||||
## 3. 员工类型规则 (roster_type_rule)
|
||||
|
||||
### GET /api/roster/type-rules
|
||||
类型规则列表,`required_field_codes` 返回为数组。
|
||||
|
||||
### POST /api/roster/type-rules
|
||||
新增规则。入参: `rule_code`(唯一), `rule_name`, `emp_type`, `required_field_codes`(数组), `sort_order`, `is_enabled`。
|
||||
|
||||
### PUT /api/roster/type-rules/{id}
|
||||
更新规则。
|
||||
|
||||
### DELETE /api/roster/type-rules/{id}
|
||||
删除规则(软删除)。
|
||||
|
||||
## 4. 工号生成规则 (roster_empno_rule)
|
||||
|
||||
### GET /api/roster/empno-rules
|
||||
工号规则列表(按 sort_order 升序,匹配优先级)。
|
||||
|
||||
### POST /api/roster/empno-rules
|
||||
新增规则。入参: `rule_code`(唯一), `rule_name`, `subsidiary_code`, `prefix`, `date_format`, `seq_length`, `start_seq`, `sort_order`, `is_enabled`。
|
||||
|
||||
### PUT /api/roster/empno-rules/{id}
|
||||
更新规则。
|
||||
|
||||
### DELETE /api/roster/empno-rules/{id}
|
||||
删除规则(软删除)。
|
||||
|
||||
### POST /api/roster/empno-rules/generate
|
||||
生成工号。入参: `{"rule_code": "empno_subs_a"}` 或 `{"subsidiary_code": "001"}`(无参数取默认规则)。
|
||||
逻辑: 前缀 + 日期段(按 date_format) + 流水号(seq_length 补零);流水号原子递增;冲突报错。
|
||||
|
||||
## 5. options 端点(下拉联动)
|
||||
|
||||
### GET /api/roster/options/{field_code}
|
||||
返回指定选项型字段的 `options` 数组,供前端下拉联动。
|
||||
约束: 字段须存在、启用,且类型为 select/multi_select。
|
||||
|
||||
## 审计
|
||||
所有写操作(create/update/delete/sort/toggle/generate)均写入 `roster_op_log`,
|
||||
记录操作类型、目标表、目标ID、变更前后快照、操作人、IP、时间。
|
||||
@ -1,40 +1,28 @@
|
||||
# 模块名称: 权限日志与通用服务模块 (hr-system)
|
||||
|
||||
状态:**一期批次1开发中 —— T09 审计底座已完成**(agent.develop,见 docs/02-develop/dev-notes.md)
|
||||
所属应用:hr-web(Web版人事系统)
|
||||
|
||||
> 说明:本模块为迭代1规划的系统支撑模块(ocai 口径),取代 v1 同名应用 `apps/hr-system.md`(Java 口径,已废弃)。
|
||||
状态:**一期批次1 T08 权限底座已完成**
|
||||
所属应用:hr-web
|
||||
|
||||
## 1. 模块功能
|
||||
迭代1系统支撑能力模块,承载权限、日志、工作台与员工服务提醒(对应 SRS v3.1 FEAT-B1-01 权限/日志部分、FEAT-B1-09):
|
||||
|
||||
| 功能编号 | 功能 | 说明 | 状态 |
|
||||
|---|---|---|---|
|
||||
| F11 | 权限管理 | 管理角色(创建角色并设置权限);人员范围管理;管理员管理 | 待开发(T08) |
|
||||
| F12 | 操作日志 | 按操作时间/类型查看操作记录,详情含前后对比、操作人、时间;全量记录增删改 | **已完成(T09)** |
|
||||
| F14 | 工作台 | 员工/经理/管理员工作台 | 待开发(T21) |
|
||||
| F15 | 员工服务与提醒 | 生日/周年关怀、政策/公告、到期提醒 | 待开发(T16/T22) |
|
||||
|
||||
输入/处理/输出/可验收标准见 `docs/00-requirement/iteration1-function-detail.md`(F11/F12/F14/F15 合计 18 条验收项)。
|
||||
F11 权限管理、F12 操作日志、F14 工作台、F15 员工服务与提醒。
|
||||
|
||||
## 2. 仓库
|
||||
- 仓库 URL:`git@git.opencomputing.cn:yumoqing/hr-system.git`(复用现有仓)
|
||||
- 工作空间目录:`repos/hr-system`
|
||||
- 分支:`main`
|
||||
- 仓库 URL:git@git.opencomputing.cn:yumoqing/hr-system.git
|
||||
- 工作空间目录:repos/hr-system
|
||||
- 分支:main
|
||||
|
||||
## 3. 技术栈(ocai 规范,设计已冻结)
|
||||
- 前端:bricks 组件体系 + dspy 声明式页面(audit_log_list.ui 已完成)
|
||||
- 后端:ahserver(Python);ServerEnv 注册横切公共函数 `write_audit_log(...)` 等
|
||||
- 数据层:apppublic/sqlor;本模块 7 张表(前缀 sys_)
|
||||
- 权限:复用 rbac 基础包
|
||||
- 消息推送:站内消息优先
|
||||
## 3. 技术栈
|
||||
- 前端:bricks + dspy 声明式页面(role_admin.ui/data_scope.ui/audit_log_list.ui/service.ui)
|
||||
- 后端:ahserver;ServerEnv 注册 get_data_scope/write_audit_log/remind_scan
|
||||
- 数据层:apppublic/sqlor;7 张 sys_ 表
|
||||
- 权限:rbac 路径级 + 双维度数据范围
|
||||
|
||||
## 4. 模块依赖
|
||||
同 v1 基线(见设计文档)。
|
||||
依赖 hr-org/hr-roster/hr-contract;被 hr-org/hr-roster/hr-flow/hr-contract/hr-report 依赖。
|
||||
|
||||
## 5. 设计落点(迭代1)
|
||||
- 总体设计:`docs/01-design/architecture.md`、`api-design.md` §2.6、`ui-design.md` §3.8/§3.9
|
||||
- 开发任务:T08(权限底座)、T09(审计底座,**已完成**)、T16/T21/T22/T23
|
||||
## 5. 设计落点
|
||||
architecture.md §4.1/§4.3、api-design.md §2.6、ui-design.md §3.8/§3.9;
|
||||
开发任务 T08(已完成)/T09/T16/T21/T22/T23。
|
||||
|
||||
## 6. 负责人
|
||||
待定(迭代启动会指定)。
|
||||
待定。
|
||||
|
||||
@ -1,115 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
T08 / F11 权限底座自测脚本(离线 SQL 语义自测 + 叠加规则验证)
|
||||
|
||||
运行方式:
|
||||
cd repos/hr-system
|
||||
python3 scripts/test_permission.py
|
||||
|
||||
说明:不依赖真实 MariaDB 连接也可跑通纯逻辑部分(叠加规则、子树展开);
|
||||
若配置了 hrs 库则可执行 init 幂等性验证(需 appbase/sql 环境)。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
"""T08/F11 权限底座自测(纯逻辑叠加规则 + 权限矩阵)"""
|
||||
import json, sys
|
||||
|
||||
def _json_loads(raw, default):
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
if not raw: return default
|
||||
try: return json.loads(raw)
|
||||
except (TypeError, ValueError): return default
|
||||
|
||||
def expand_org_tree(parent_map, org_ids):
|
||||
"""纯逻辑子树展开(模拟 org_unit.parent_id 自引用)。"""
|
||||
if not org_ids:
|
||||
return []
|
||||
result = set(org_ids)
|
||||
frontier = list(org_ids)
|
||||
seen = set(org_ids)
|
||||
if not org_ids: return []
|
||||
result = set(org_ids); frontier = list(org_ids); seen = set(org_ids)
|
||||
while frontier:
|
||||
children = []
|
||||
for oid in frontier:
|
||||
for child in parent_map.get(oid, []):
|
||||
if child not in seen:
|
||||
seen.add(child)
|
||||
result.add(child)
|
||||
children.append(child)
|
||||
seen.add(child); result.add(child); children.append(child)
|
||||
frontier = children
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def test_org_expand():
|
||||
# 组织树:A -> B -> C
|
||||
parent_map = {"A": ["B"], "B": ["C"]}
|
||||
out = expand_org_tree(parent_map, ["A"])
|
||||
assert out == ["A", "B", "C"], out
|
||||
out = expand_org_tree({"A":["B"],"B":["C"]}, ["A"])
|
||||
assert out == ["A","B","C"], out
|
||||
return "PASS: 组织维度子树展开"
|
||||
|
||||
|
||||
def test_scope_overlay():
|
||||
"""模拟 get_data_scope 叠加规则(双维度 + 多角色 + admin 全量)。"""
|
||||
# 两个 scope:org 并集;field 并集;admin 全量
|
||||
scopes = [
|
||||
{"scope_type": "org", "org_ids": ["A", "B"], "field_ids": []},
|
||||
{"scope_type": "field", "org_ids": [], "field_ids": ["f1", "f2"]},
|
||||
]
|
||||
scopes = [{"scope_type":"org","org_ids":["A","B"],"field_ids":[]},{"scope_type":"field","org_ids":[],"field_ids":["f1","f2"]}]
|
||||
org_union, field_union, field_restricted = set(), set(), False
|
||||
for s in scopes:
|
||||
if s["scope_type"] in ("org", "all") and s["org_ids"]:
|
||||
org_union.update(s["org_ids"])
|
||||
if s["scope_type"] in ("field", "all"):
|
||||
field_restricted = True
|
||||
field_union.update(s["field_ids"])
|
||||
assert org_union == {"A", "B"}
|
||||
assert field_union == {"f1", "f2"}
|
||||
if s["scope_type"] in ("org","all") and s["org_ids"]: org_union.update(s["org_ids"])
|
||||
if s["scope_type"] in ("field","all"): field_restricted = True; field_union.update(s["field_ids"])
|
||||
assert org_union == {"A","B"}
|
||||
assert field_union == {"f1","f2"}
|
||||
assert field_restricted is True
|
||||
return "PASS: 双维度叠加并集"
|
||||
|
||||
|
||||
def test_field_all_when_unrestricted():
|
||||
scopes = [{"scope_type": "org", "org_ids": ["A"], "field_ids": []}]
|
||||
scopes = [{"scope_type":"org","org_ids":["A"],"field_ids":[]}]
|
||||
field_restricted = False
|
||||
field_union = set()
|
||||
for s in scopes:
|
||||
if s["scope_type"] in ("field", "all"):
|
||||
field_restricted = True
|
||||
field_union.update(s["field_ids"])
|
||||
assert field_restricted is False # -> field_all = True
|
||||
if s["scope_type"] in ("field","all"): field_restricted = True; field_union.update(s["field_ids"])
|
||||
assert field_restricted is False
|
||||
return "PASS: 未配置字段维度 -> 字段不受限"
|
||||
|
||||
|
||||
def test_admin_full():
|
||||
role_codes = ["manager", "sys_admin"]
|
||||
role_codes = ["manager","sys_admin"]
|
||||
assert ("sys_admin" in role_codes or "admin" in role_codes) is True
|
||||
return "PASS: sys_admin/admin 全量数据范围"
|
||||
|
||||
|
||||
def main():
|
||||
tests = [test_org_expand, test_scope_overlay, test_field_all_when_unrestricted, test_admin_full]
|
||||
passed = 0
|
||||
for t in tests:
|
||||
try:
|
||||
print(t())
|
||||
passed += 1
|
||||
print(t()); passed += 1
|
||||
except AssertionError as e:
|
||||
print("FAIL:", t.__name__, e)
|
||||
print("\n权限矩阵自测结果:{}/{} 通过".format(passed, len(tests)))
|
||||
print("""
|
||||
权限矩阵(自测基线):
|
||||
| 角色 | 路径级功能权限(rbac) | 组织数据范围 | 花名册字段范围 |
|
||||
|--------------|---------------------------|--------------------------|------------------------|
|
||||
| 员工自助 | 个人档案/自助申请 | 仅本人(org_ids=空) | 本人可见字段白名单 |
|
||||
| 经理 | 本部门入转调离管理+审批 | 本部门+子树(并集展开) | 经理视角字段白名单 |
|
||||
| HR管理员 | 全公司人事业务 | 按 sys_data_scope 并集 | 按 sys_data_scope 并集 |
|
||||
| 系统管理员 | 系统配置+权限+审计 | org_all=True | field_all=True |
|
||||
| admin | 全量(等价 sys_admin) | org_all=True | field_all=True |
|
||||
| 角色 | 路径级功能权限(rbac) | 组织数据范围 | 花名册字段范围 |
|
||||
| 员工自助 | 个人档案/自助申请 | 仅本人 | 本人可见字段白名单 |
|
||||
| 经理 | 本部门入转调离管理+审批 | 本部门+子树 | 经理视角字段白名单 |
|
||||
| HR管理员 | 全公司人事业务 | 按 sys_data_scope 并集 | 按 sys_data_scope 并集 |
|
||||
| 系统管理员 | 系统配置+权限+审计 | org_all=True | field_all=True |
|
||||
| admin | 全量(等价 sys_admin) | org_all=True | field_all=True |
|
||||
""")
|
||||
return 0 if passed == len(tests) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
@ -1,63 +1,35 @@
|
||||
-- =====================================================================
|
||||
-- hr-system 权限底座(T08 / F11)初始化 DDL + 种子数据
|
||||
-- 依赖:rbac 基座已创建 sys_role / sys_user / sys_user_role /
|
||||
-- sys_permission / sys_role_permission(路径级功能权限)
|
||||
-- 本脚本仅补齐本模块专属表 sys_data_scope(如 T01 已建则跳过 DDL 段)
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 1. sys_data_scope 双维度数据范围表(幂等:IF NOT EXISTS)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- hr-system 权限底座(T08/F11) DDL + 种子数据
|
||||
CREATE TABLE IF NOT EXISTS sys_data_scope (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
admin_user_id VARCHAR(32) NULL COMMENT '管理员用户(sys_user.id)',
|
||||
scope_type VARCHAR(16) NULL COMMENT '范围类型(org/field/all)',
|
||||
org_ids_json TEXT NULL COMMENT '组织维度(org_unit.id 列表, 含子树由服务端展开)',
|
||||
field_ids_json TEXT NULL COMMENT '花名册字段维度(roster_field_def.id 白名单)',
|
||||
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 '更新时间',
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
admin_user_id VARCHAR(32) NULL COMMENT '管理员用户(sys_user.id)',
|
||||
scope_type VARCHAR(16) NULL COMMENT '范围类型(org/field/all)',
|
||||
org_ids_json TEXT NULL COMMENT '组织维度(org_unit.id 列表,含子树服务端展开)',
|
||||
field_ids_json TEXT NULL COMMENT '花名册字段维度(roster_field_def.id 白名单)',
|
||||
remark TEXT NULL,
|
||||
created_by VARCHAR(32) NULL,
|
||||
updated_by VARCHAR(32) NULL,
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_admin_user (admin_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='数据范围配置(F11,双维度)';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据范围配置(F11,双维度)';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 2. 内置四角色(幂等:INSERT IGNORE,编码唯一)
|
||||
-- ---------------------------------------------------------------------
|
||||
INSERT IGNORE INTO sys_role (id, role_code, role_name, remark, created_by, created_at, updated_at) VALUES
|
||||
('ROLE_EMPLOYEE', 'employee', '员工自助', '员工本人查看/修改个人档案、发起自助申请', 'system', NOW(), NOW()),
|
||||
('ROLE_MANAGER', 'manager', '经理', '管理本部门员工、发起并审批本部门人事异动', 'system', NOW(), NOW()),
|
||||
('ROLE_HR_ADMIN', 'hr_admin', 'HR管理员', '管理全公司人事业务,受数据范围约束', 'system', NOW(), NOW()),
|
||||
('ROLE_SYS_ADMIN','sys_admin', '系统管理员', '系统配置、角色/管理员/数据范围/审计管理', 'system', NOW(), NOW());
|
||||
('ROLE_EMPLOYEE','employee','员工自助','员工本人查看/修改个人档案、发起自助申请','system',NOW(),NOW()),
|
||||
('ROLE_MANAGER','manager','经理','管理本部门员工、发起并审批本部门人事异动','system',NOW(),NOW()),
|
||||
('ROLE_HR_ADMIN','hr_admin','HR管理员','管理全公司人事业务,受数据范围约束','system',NOW(),NOW()),
|
||||
('ROLE_SYS_ADMIN','sys_admin','系统管理员','系统配置、角色/管理员/数据范围/审计管理','system',NOW(),NOW());
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 3. 内置 admin 用户(密码由 rbac 初始化流程处理,此处仅建账号)
|
||||
-- ---------------------------------------------------------------------
|
||||
INSERT IGNORE INTO sys_user (id, username, real_name, password, status, created_at, updated_at) VALUES
|
||||
('USER_ADMIN', 'admin', '系统管理员', '', 1, NOW(), NOW());
|
||||
('USER_ADMIN','admin','系统管理员','',1,NOW(),NOW());
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 4. admin 绑定全部四角色(等价 sys_admin 全量权限)
|
||||
-- ---------------------------------------------------------------------
|
||||
INSERT IGNORE INTO sys_user_role (user_id, role_id) VALUES
|
||||
('USER_ADMIN', 'ROLE_EMPLOYEE'),
|
||||
('USER_ADMIN', 'ROLE_MANAGER'),
|
||||
('USER_ADMIN', 'ROLE_HR_ADMIN'),
|
||||
('USER_ADMIN', 'ROLE_SYS_ADMIN');
|
||||
('USER_ADMIN','ROLE_EMPLOYEE'),('USER_ADMIN','ROLE_MANAGER'),('USER_ADMIN','ROLE_HR_ADMIN'),('USER_ADMIN','ROLE_SYS_ADMIN');
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 5. 权限底座基线权限点(rbac 路径级)
|
||||
-- ---------------------------------------------------------------------
|
||||
INSERT IGNORE INTO sys_permission (id, perm_code, perm_name, perm_path, created_at) VALUES
|
||||
('PERM_ROLE_ADMIN', 'hr_system:role_admin', '角色权限配置', '/hr_system/role_admin', NOW()),
|
||||
('PERM_DATA_SCOPE', 'hr_system:data_scope', '数据范围配置', '/hr_system/data_scope', NOW()),
|
||||
('PERM_AUDIT_LOG', 'hr_system:audit_log', '操作日志查询', '/hr_system/audit_log', NOW());
|
||||
('PERM_ROLE_ADMIN','hr_system:role_admin','角色权限配置','/hr_system/role_admin',NOW()),
|
||||
('PERM_DATA_SCOPE','hr_system:data_scope','数据范围配置','/hr_system/data_scope',NOW()),
|
||||
('PERM_AUDIT_LOG','hr_system:audit_log','操作日志查询','/hr_system/audit_log',NOW());
|
||||
|
||||
-- 系统管理员获得权限底座全部权限点
|
||||
INSERT IGNORE INTO sys_role_permission (role_id, perm_id) VALUES
|
||||
('ROLE_SYS_ADMIN', 'PERM_ROLE_ADMIN'),
|
||||
('ROLE_SYS_ADMIN', 'PERM_DATA_SCOPE'),
|
||||
('ROLE_SYS_ADMIN', 'PERM_AUDIT_LOG');
|
||||
('ROLE_SYS_ADMIN','PERM_ROLE_ADMIN'),('ROLE_SYS_ADMIN','PERM_DATA_SCOPE'),('ROLE_SYS_ADMIN','PERM_AUDIT_LOG');
|
||||
|
||||
1
sql/hr_org.sql
Normal file
1
sql/hr_org.sql
Normal file
@ -0,0 +1 @@
|
||||
-- hr-org DDL
|
||||
168
sql/roster_field_schema.sql
Normal file
168
sql/roster_field_schema.sql
Normal file
@ -0,0 +1,168 @@
|
||||
-- =============================================================
|
||||
-- hr-roster 花名册字段体系 (F03) DDL
|
||||
-- 数据库: MySQL 8.0+ / 兼容 MariaDB 10.4+
|
||||
-- 编码: utf8mb4
|
||||
-- 说明: 覆盖 F03 验收项 1/3/6 所需表结构
|
||||
-- roster_field_group 字段分组
|
||||
-- roster_field_def 字段定义
|
||||
-- roster_type_rule 员工类型规则
|
||||
-- roster_empno_rule 工号生成规则
|
||||
-- roster_op_log 操作审计日志
|
||||
-- =============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- -------------------------------------------------------------
|
||||
-- 1. 字段分组表
|
||||
-- -------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `roster_field_group`;
|
||||
CREATE TABLE `roster_field_group` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`group_code` VARCHAR(64) NOT NULL COMMENT '分组编码(唯一, 如 work_info/personal_info)',
|
||||
`group_name` VARCHAR(128) NOT NULL COMMENT '分组名称',
|
||||
`group_desc` VARCHAR(512) NULL DEFAULT NULL COMMENT '分组描述',
|
||||
`is_builtin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否内置(1内置不可删,0自定义)',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号(升序)',
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用状态(1启用0停用)',
|
||||
`created_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '创建人用户ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '更新人用户ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL DEFAULT NULL COMMENT '软删除时间(NULL未删)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_group_code` (`group_code`),
|
||||
KEY `idx_sort` (`sort_order`),
|
||||
KEY `idx_enabled` (`is_enabled`),
|
||||
KEY `idx_deleted` (`deleted_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花名册字段分组';
|
||||
|
||||
-- 内置分组初始化数据
|
||||
INSERT INTO `roster_field_group` (`group_code`, `group_name`, `group_desc`, `is_builtin`, `sort_order`, `is_enabled`) VALUES
|
||||
('work_info', '工作信息', '组织/岗位/职级/工号等', 1, 10, 1),
|
||||
('personal_info', '个人信息', '姓名/证件/联系方式等', 1, 20, 1),
|
||||
('contract_info', '合同信息', '合同/入离职/试用期等', 1, 30, 1),
|
||||
('payroll_info', '薪酬信息', '薪资/社保/公积金等', 1, 40, 1);
|
||||
|
||||
-- -------------------------------------------------------------
|
||||
-- 2. 字段定义表
|
||||
-- -------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `roster_field_def`;
|
||||
CREATE TABLE `roster_field_def` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`group_id` BIGINT UNSIGNED NOT NULL COMMENT '所属分组ID(roster_field_group.id)',
|
||||
`field_code` VARCHAR(64) NOT NULL COMMENT '字段编码(唯一, 如 emp_name/emp_no)',
|
||||
`field_name` VARCHAR(128) NOT NULL COMMENT '字段显示名称',
|
||||
`field_type` VARCHAR(32) NOT NULL COMMENT '字段类型(text/number/date/datetime/select/multi_select/attachment/bool)',
|
||||
`is_required` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否必填(1是0否)',
|
||||
`is_unique` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否唯一(1是0否)',
|
||||
`is_sensitive` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '敏感标记(1是0否, 敏感字段读需脱敏)',
|
||||
`default_value` VARCHAR(512) NULL DEFAULT NULL COMMENT '默认值',
|
||||
`options_json` JSON NULL COMMENT '选项(select/multi_select用), 格式:[{"label":"","value":""}]',
|
||||
`validation_rule` VARCHAR(512) NULL DEFAULT NULL COMMENT '校验规则(正则/范围等)',
|
||||
`placeholder` VARCHAR(256) NULL DEFAULT NULL COMMENT '输入提示',
|
||||
`help_text` VARCHAR(512) NULL DEFAULT NULL COMMENT '帮助说明',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '组内排序号(拖拽排序)',
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用状态(1启用0停用)',
|
||||
`created_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '创建人用户ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '更新人用户ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_field_code` (`field_code`),
|
||||
KEY `idx_group` (`group_id`),
|
||||
KEY `idx_sort` (`group_id`, `sort_order`),
|
||||
KEY `idx_enabled` (`is_enabled`),
|
||||
KEY `idx_deleted` (`deleted_at`),
|
||||
CONSTRAINT `fk_field_group` FOREIGN KEY (`group_id`) REFERENCES `roster_field_group` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花名册字段定义';
|
||||
|
||||
-- -------------------------------------------------------------
|
||||
-- 3. 员工类型规则表 (不同员工类型不同必填规则)
|
||||
-- -------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `roster_type_rule`;
|
||||
CREATE TABLE `roster_type_rule` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`rule_code` VARCHAR(64) NOT NULL COMMENT '规则编码(唯一)',
|
||||
`rule_name` VARCHAR(128) NOT NULL COMMENT '规则名称',
|
||||
`emp_type` VARCHAR(32) NOT NULL COMMENT '员工类型(正式工/临时工/实习生/劳务派遣等)',
|
||||
`required_field_codes` JSON NULL COMMENT '该类型必填字段编码列表, 格式:["emp_no","emp_name",...]',
|
||||
`extra_config` JSON NULL COMMENT '扩展配置(校验/联动等)',
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用状态(1启用0停用)',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
`created_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '创建人用户ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '更新人用户ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_rule_code` (`rule_code`),
|
||||
KEY `idx_emp_type` (`emp_type`),
|
||||
KEY `idx_enabled` (`is_enabled`),
|
||||
KEY `idx_deleted` (`deleted_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工类型必填规则';
|
||||
|
||||
-- 默认规则初始化
|
||||
INSERT INTO `roster_type_rule` (`rule_code`, `rule_name`, `emp_type`, `required_field_codes`) VALUES
|
||||
('type_formal', '正式工必填规则', '正式工', '["emp_no","emp_name","dept_id","position_id","join_date"]'),
|
||||
('type_temp', '临时工必填规则', '临时工', '["emp_no","emp_name","dept_id","join_date"]'),
|
||||
('type_intern', '实习生必填规则', '实习生', '["emp_no","emp_name","join_date","end_date"]');
|
||||
|
||||
-- -------------------------------------------------------------
|
||||
-- 4. 工号生成规则表
|
||||
-- -------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `roster_empno_rule`;
|
||||
CREATE TABLE `roster_empno_rule` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`rule_code` VARCHAR(64) NOT NULL COMMENT '规则编码(唯一)',
|
||||
`rule_name` VARCHAR(128) NOT NULL COMMENT '规则名称',
|
||||
`subsidiary_code` VARCHAR(32) NULL DEFAULT NULL COMMENT '子公司前缀编码(如子公A=001)',
|
||||
`prefix` VARCHAR(32) NULL DEFAULT NULL COMMENT '固定前缀',
|
||||
`date_format` VARCHAR(32) NULL DEFAULT NULL COMMENT '日期格式(如 %Y%m / YYYYMM / 空=无日期段)',
|
||||
`seq_length` INT NOT NULL DEFAULT 4 COMMENT '流水号位数',
|
||||
`start_seq` INT NOT NULL DEFAULT 1 COMMENT '起始流水号',
|
||||
`current_seq` BIGINT NOT NULL DEFAULT 0 COMMENT '当前流水号(生成时原子递增)',
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用状态(1启用0停用)',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号(匹配优先级)',
|
||||
`created_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '创建人用户ID',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_by` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '更新人用户ID',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted_at` DATETIME NULL DEFAULT NULL COMMENT '软删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_rule_code` (`rule_code`),
|
||||
KEY `idx_subsidiary` (`subsidiary_code`),
|
||||
KEY `idx_enabled` (`is_enabled`),
|
||||
KEY `idx_deleted` (`deleted_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工号生成规则';
|
||||
|
||||
-- 默认规则初始化
|
||||
INSERT INTO `roster_empno_rule` (`rule_code`, `rule_name`, `subsidiary_code`, `prefix`, `date_format`, `seq_length`, `start_seq`, `current_seq`) VALUES
|
||||
('empno_default', '默认工号规则', NULL, 'E', '%Y%m', 5, 1, 0),
|
||||
('empno_subs_a', '子公司A工号规则', '001', 'A', '%Y%m', 4, 1, 0);
|
||||
|
||||
-- -------------------------------------------------------------
|
||||
-- 5. 操作审计日志表 (写操作审计落库)
|
||||
-- -------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS `roster_op_log`;
|
||||
CREATE TABLE `roster_op_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`op_type` VARCHAR(32) NOT NULL COMMENT '操作类型(create/update/delete/enable/disable/sort/gen_empno)',
|
||||
`target_table` VARCHAR(64) NOT NULL COMMENT '目标表名',
|
||||
`target_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '目标记录ID',
|
||||
`before_json` JSON NULL COMMENT '变更前数据快照',
|
||||
`after_json` JSON NULL COMMENT '变更后数据快照',
|
||||
`operator_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '操作人用户ID',
|
||||
`operator_name` VARCHAR(128) NULL DEFAULT NULL COMMENT '操作人姓名',
|
||||
`client_ip` VARCHAR(64) NULL DEFAULT NULL COMMENT '客户端IP',
|
||||
`remark` VARCHAR(512) NULL DEFAULT NULL COMMENT '备注',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_target` (`target_table`, `target_id`),
|
||||
KEY `idx_op_type` (`op_type`),
|
||||
KEY `idx_operator` (`operator_id`),
|
||||
KEY `idx_created` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花名册操作审计日志';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@ -3,49 +3,8 @@
|
||||
"title": "人员数据范围管理(F11)",
|
||||
"type": "tabpanel",
|
||||
"items": [
|
||||
{
|
||||
"type": "grid",
|
||||
"columns": [
|
||||
{"field": "username", "title": "管理员账号", "width": 120},
|
||||
{"field": "real_name", "title": "姓名", "width": 120},
|
||||
{"field": "scope_type", "title": "范围类型", "width": 100},
|
||||
{"field": "org_ids", "title": "组织维度(含子树)", "width": 260},
|
||||
{"field": "field_ids", "title": "花名册字段维度", "width": 260},
|
||||
{"field": "remark", "title": "备注", "width": 160},
|
||||
{"field": "updated_at", "title": "更新时间", "width": 160}
|
||||
],
|
||||
"api": {
|
||||
"list": {"endpoint": "scope_list"},
|
||||
"save": {"endpoint": "scope_save"},
|
||||
"delete": {"endpoint": "scope_delete"}
|
||||
},
|
||||
"toolbar": ["refresh"]
|
||||
},
|
||||
{
|
||||
"type": "form",
|
||||
"title": "数据范围配置",
|
||||
"fields": [
|
||||
{"field": "admin_user_id", "title": "管理员", "type": "select", "optionsApi": {"endpoint": "admin_list"}, "required": true},
|
||||
{"field": "scope_type", "title": "范围类型", "type": "select", "options": [
|
||||
{"value": "org", "label": "组织维度"},
|
||||
{"value": "field", "label": "花名册字段维度"},
|
||||
{"value": "all", "label": "双维度同时启用"}
|
||||
], "default": "org"},
|
||||
{"field": "org_ids", "title": "组织部门(含子树)", "type": "org-tree", "multiple": true},
|
||||
{"field": "field_ids", "title": "花名册字段白名单", "type": "multi-select", "optionsApi": {"endpoint": "roster_field_def_options"}},
|
||||
{"field": "remark", "title": "备注", "type": "text"}
|
||||
],
|
||||
"api": {"submit": {"endpoint": "scope_save"}}
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"title": "叠加机制说明",
|
||||
"items": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "数据范围按管理员账号唯一配置:组织维度(org_unit 子树自动展开)与花名册字段维度(可见字段白名单)叠加生效。多角色时组织取并集、字段取并集;未配置字段维度则该角色字段不受限。系统管理员(admin/sys_admin)拥有全组织全字段范围。"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"type":"grid","columns":[{"field":"username","title":"管理员账号","width":120},{"field":"real_name","title":"姓名","width":120},{"field":"scope_type","title":"范围类型","width":100},{"field":"org_ids","title":"组织维度(含子树)","width":260},{"field":"field_ids","title":"花名册字段维度","width":260},{"field":"remark","title":"备注","width":160},{"field":"updated_at","title":"更新时间","width":160}],"api":{"list":{"endpoint":"scope_list"},"save":{"endpoint":"scope_save"},"delete":{"endpoint":"scope_delete"}},"toolbar":["refresh"]},
|
||||
{"type":"form","title":"数据范围配置","fields":[{"field":"admin_user_id","title":"管理员","type":"select","optionsApi":{"endpoint":"admin_list"},"required":true},{"field":"scope_type","title":"范围类型","type":"select","options":[{"value":"org","label":"组织维度"},{"value":"field","label":"花名册字段维度"},{"value":"all","label":"双维度同时启用"}],"default":"org"},{"field":"org_ids","title":"组织部门(含子树)","type":"org-tree","multiple":true},{"field":"field_ids","title":"花名册字段白名单","type":"multi-select","optionsApi":{"endpoint":"roster_field_def_options"}},{"field":"remark","title":"备注","type":"text"}],"api":{"submit":{"endpoint":"scope_save"}}},
|
||||
{"type":"panel","title":"叠加机制说明","items":[{"type":"text","text":"数据范围按管理员账号唯一配置:组织维度(org_unit 子树自动展开)与花名册字段维度(可见字段白名单)叠加生效。多角色时组织取并集、字段取并集;未配置字段维度则该角色字段不受限。系统管理员(admin/sys_admin)拥有全组织全字段范围。"}]}
|
||||
]
|
||||
}
|
||||
|
||||
122
wwwroot/field_config.ui
Normal file
122
wwwroot/field_config.ui
Normal file
@ -0,0 +1,122 @@
|
||||
{
|
||||
"app": "hr-roster-field-config",
|
||||
"title": "花名册字段配置",
|
||||
"version": "1.0.0",
|
||||
"theme": "default",
|
||||
"tabs": [
|
||||
{
|
||||
"id": "tab_field_group",
|
||||
"label": "字段分组",
|
||||
"widget": "table",
|
||||
"api": {
|
||||
"list": "/api/roster/field-groups",
|
||||
"create": "/api/roster/field-groups",
|
||||
"update": "/api/roster/field-groups/{id}",
|
||||
"delete": "/api/roster/field-groups/{id}"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "group_code", "label": "分组编码", "required": true, "unique": true},
|
||||
{"field": "group_name", "label": "分组名称", "required": true},
|
||||
{"field": "group_desc", "label": "描述"},
|
||||
{"field": "is_builtin", "label": "内置", "type": "bool", "readonly": true},
|
||||
{"field": "sort_order", "label": "排序号", "type": "number"},
|
||||
{"field": "is_enabled", "label": "启用", "type": "switch"}
|
||||
],
|
||||
"actions": ["add", "edit", "delete"],
|
||||
"toolbar": {"refresh": true, "search": ["group_code", "group_name"]}
|
||||
},
|
||||
{
|
||||
"id": "tab_field_def",
|
||||
"label": "字段定义",
|
||||
"widget": "table",
|
||||
"draggable": true,
|
||||
"dragEndApi": "/api/roster/field-defs/sort",
|
||||
"toggleApi": "/api/roster/field-defs/toggle",
|
||||
"api": {
|
||||
"list": "/api/roster/field-defs",
|
||||
"create": "/api/roster/field-defs",
|
||||
"update": "/api/roster/field-defs/{id}",
|
||||
"delete": "/api/roster/field-defs/{id}"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "field_code", "label": "字段编码", "required": true, "unique": true},
|
||||
{"field": "field_name", "label": "字段名称", "required": true},
|
||||
{"field": "group_id", "label": "所属分组", "type": "select", "optionsApi": "/api/roster/field-groups", "labelField": "group_name", "valueField": "id"},
|
||||
{"field": "field_type", "label": "字段类型", "type": "select", "options": [
|
||||
{"label": "文本", "value": "text"},
|
||||
{"label": "数字", "value": "number"},
|
||||
{"label": "日期", "value": "date"},
|
||||
{"label": "日期时间", "value": "datetime"},
|
||||
{"label": "单选", "value": "select"},
|
||||
{"label": "多选", "value": "multi_select"},
|
||||
{"label": "附件", "value": "attachment"},
|
||||
{"label": "布尔", "value": "bool"}
|
||||
]},
|
||||
{"field": "is_required", "label": "必填", "type": "switch"},
|
||||
{"field": "is_unique", "label": "唯一", "type": "switch"},
|
||||
{"field": "is_sensitive", "label": "敏感", "type": "switch"},
|
||||
{"field": "sort_order", "label": "排序号", "type": "number"},
|
||||
{"field": "is_enabled", "label": "启用", "type": "switch", "toggleApi": "/api/roster/field-defs/toggle"}
|
||||
],
|
||||
"actions": ["add", "edit", "delete", "sort", "toggle"],
|
||||
"toolbar": {"refresh": true, "search": ["field_code", "field_name"], "groupFilter": "group_id"}
|
||||
},
|
||||
{
|
||||
"id": "tab_type_rule",
|
||||
"label": "员工类型规则",
|
||||
"widget": "table",
|
||||
"api": {
|
||||
"list": "/api/roster/type-rules",
|
||||
"create": "/api/roster/type-rules",
|
||||
"update": "/api/roster/type-rules/{id}",
|
||||
"delete": "/api/roster/type-rules/{id}"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "rule_code", "label": "规则编码", "required": true, "unique": true},
|
||||
{"field": "rule_name", "label": "规则名称", "required": true},
|
||||
{"field": "emp_type", "label": "员工类型", "type": "select", "options": [
|
||||
{"label": "正式工", "value": "正式工"},
|
||||
{"label": "临时工", "value": "临时工"},
|
||||
{"label": "实习生", "value": "实习生"},
|
||||
{"label": "劳务派遣", "value": "劳务派遣"}
|
||||
]},
|
||||
{"field": "required_field_codes", "label": "必填字段", "type": "multi_select", "optionsApi": "/api/roster/field-defs", "labelField": "field_name", "valueField": "field_code"},
|
||||
{"field": "sort_order", "label": "排序号", "type": "number"},
|
||||
{"field": "is_enabled", "label": "启用", "type": "switch"}
|
||||
],
|
||||
"actions": ["add", "edit", "delete"],
|
||||
"toolbar": {"refresh": true}
|
||||
},
|
||||
{
|
||||
"id": "tab_empno_rule",
|
||||
"label": "工号规则",
|
||||
"widget": "table",
|
||||
"api": {
|
||||
"list": "/api/roster/empno-rules",
|
||||
"create": "/api/roster/empno-rules",
|
||||
"update": "/api/roster/empno-rules/{id}",
|
||||
"delete": "/api/roster/empno-rules/{id}",
|
||||
"generate": "/api/roster/empno-rules/generate"
|
||||
},
|
||||
"columns": [
|
||||
{"field": "rule_code", "label": "规则编码", "required": true, "unique": true},
|
||||
{"field": "rule_name", "label": "规则名称", "required": true},
|
||||
{"field": "subsidiary_code", "label": "子公司前缀", "placeholder": "如 001"},
|
||||
{"field": "prefix", "label": "固定前缀"},
|
||||
{"field": "date_format", "label": "日期格式", "type": "select", "options": [
|
||||
{"label": "无日期段", "value": ""},
|
||||
{"label": "YYYYMM", "value": "YYYYMM"},
|
||||
{"label": "%Y%m", "value": "%Y%m"},
|
||||
{"label": "%Y", "value": "%Y"}
|
||||
]},
|
||||
{"field": "seq_length", "label": "流水号位数", "type": "number", "default": 4},
|
||||
{"field": "start_seq", "label": "起始流水号", "type": "number", "default": 1},
|
||||
{"field": "current_seq", "label": "当前流水号", "type": "number", "readonly": true},
|
||||
{"field": "sort_order", "label": "优先级", "type": "number"},
|
||||
{"field": "is_enabled", "label": "启用", "type": "switch"}
|
||||
],
|
||||
"actions": ["add", "edit", "delete", "generate"],
|
||||
"toolbar": {"refresh": true, "generate": {"label": "生成工号", "api": "/api/roster/empno-rules/generate"}}
|
||||
}
|
||||
]
|
||||
}
|
||||
1
wwwroot/hr-org/ui/org_chart.ui.json
Normal file
1
wwwroot/hr-org/ui/org_chart.ui.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
wwwroot/hr-org/ui/org_contract_company_crud.ui.json
Normal file
1
wwwroot/hr-org/ui/org_contract_company_crud.ui.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
wwwroot/hr-org/ui/org_field_def_crud.ui.json
Normal file
1
wwwroot/hr-org/ui/org_field_def_crud.ui.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
wwwroot/hr-org/ui/org_tree.ui.json
Normal file
1
wwwroot/hr-org/ui/org_tree.ui.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
wwwroot/hr-org/ui/org_unit_crud.ui.json
Normal file
1
wwwroot/hr-org/ui/org_unit_crud.ui.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -3,76 +3,8 @@
|
||||
"title": "角色权限配置(F11)",
|
||||
"type": "tabpanel",
|
||||
"items": [
|
||||
{
|
||||
"type": "grid",
|
||||
"columns": [
|
||||
{"field": "role_code", "title": "角色编码", "width": 120},
|
||||
{"field": "role_name", "title": "角色名称", "width": 140},
|
||||
{"field": "remark", "title": "备注", "width": 260},
|
||||
{"field": "perm_count", "title": "权限点数", "width": 90},
|
||||
{"field": "created_at", "title": "创建时间", "width": 160}
|
||||
],
|
||||
"api": {
|
||||
"list": {"endpoint": "role_list"},
|
||||
"detail": {"endpoint": "role_detail"},
|
||||
"create": {"endpoint": "role_create"},
|
||||
"update": {"endpoint": "role_update"},
|
||||
"delete": {"endpoint": "role_delete"}
|
||||
},
|
||||
"toolbar": ["create", "update", "delete", "refresh"]
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"title": "权限点分配",
|
||||
"items": [
|
||||
{
|
||||
"type": "tree",
|
||||
"title": "可用权限点",
|
||||
"checkable": true,
|
||||
"api": {"list": {"endpoint": "perm_tree"}}
|
||||
},
|
||||
{
|
||||
"type": "button",
|
||||
"label": "保存分配",
|
||||
"event": {
|
||||
"onClick": {
|
||||
"endpoint": "role_assign_perms",
|
||||
"payload": {"role_id": "{selected_role.id}", "perm_ids": "{checked_perm_ids}"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"title": "管理员管理",
|
||||
"items": [
|
||||
{
|
||||
"type": "grid",
|
||||
"columns": [
|
||||
{"field": "username", "title": "用户名", "width": 120},
|
||||
{"field": "real_name", "title": "姓名", "width": 120},
|
||||
{"field": "role_codes", "title": "绑定角色", "width": 240},
|
||||
{"field": "status", "title": "状态", "width": 80},
|
||||
{"field": "created_at", "title": "创建时间", "width": 160}
|
||||
],
|
||||
"api": {
|
||||
"list": {"endpoint": "admin_list"},
|
||||
"create": {"endpoint": "admin_create"},
|
||||
"delete": {"endpoint": "admin_disable"}
|
||||
},
|
||||
"toolbar": ["create", "refresh"]
|
||||
},
|
||||
{
|
||||
"type": "form",
|
||||
"title": "绑定角色",
|
||||
"fields": [
|
||||
{"field": "user_id", "title": "管理员", "type": "hidden"},
|
||||
{"field": "role_ids", "title": "角色", "type": "multi-select", "optionsApi": {"endpoint": "role_list"}}
|
||||
],
|
||||
"api": {"submit": {"endpoint": "admin_bind_roles"}}
|
||||
}
|
||||
]
|
||||
}
|
||||
{"type":"grid","columns":[{"field":"role_code","title":"角色编码","width":120},{"field":"role_name","title":"角色名称","width":140},{"field":"remark","title":"备注","width":260},{"field":"perm_count","title":"权限点数","width":90},{"field":"created_at","title":"创建时间","width":160}],"api":{"list":{"endpoint":"role_list"},"detail":{"endpoint":"role_detail"},"create":{"endpoint":"role_create"},"update":{"endpoint":"role_update"},"delete":{"endpoint":"role_delete"}},"toolbar":["create","update","delete","refresh"]},
|
||||
{"type":"panel","title":"权限点分配","items":[{"type":"tree","title":"可用权限点","checkable":true,"api":{"list":{"endpoint":"perm_tree"}}},{"type":"button","label":"保存分配","event":{"onClick":{"endpoint":"role_assign_perms","payload":{"role_id":"{selected_role.id}","perm_ids":"{checked_perm_ids}"}}}}]},
|
||||
{"type":"panel","title":"管理员管理","items":[{"type":"grid","columns":[{"field":"username","title":"用户名","width":120},{"field":"real_name","title":"姓名","width":120},{"field":"role_codes","title":"绑定角色","width":240},{"field":"status","title":"状态","width":80},{"field":"created_at","title":"创建时间","width":160}],"api":{"list":{"endpoint":"admin_list"},"create":{"endpoint":"admin_create"},"delete":{"endpoint":"admin_disable"}},"toolbar":["create","refresh"]},{"type":"form","title":"绑定角色","fields":[{"field":"user_id","title":"管理员","type":"hidden"},{"field":"role_ids","title":"角色","type":"multi-select","optionsApi":{"endpoint":"role_list"}}],"api":{"submit":{"endpoint":"admin_bind_roles"}}}]}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user