From ca578a9bb39fe1fa046071a3a04581a60b465bfc Mon Sep 17 00:00:00 2001 From: Pipeline Agent Date: Fri, 21 Aug 2026 00:37:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E6=AC=A11-T05:=20hr-roster=20?= =?UTF-8?q?=E8=8A=B1=E5=90=8D=E5=86=8C=E5=AD=97=E6=AE=B5=E4=BD=93=E7=B3=BB?= =?UTF-8?q?(F03)=20=E4=BA=A4=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hr_system/audit.py | 2 +- apps/hr_org.py | 1 + apps/hr_org_selftest.py | 1 + apps/hr_permission.py | 459 +++----------- apps/hr_roster_field.py | 572 ++++++++++++++++++ docs/02-develop/dev-notes-T03.md | 1 + docs/02-develop/dev-notes.md | 64 +- docs/hr-roster-field-api.md | 85 +++ modules/hr-system.md | 44 +- scripts/test_permission.py | 94 +-- sql/V002__init_permission.sql | 72 +-- sql/hr_org.sql | 1 + sql/roster_field_schema.sql | 168 +++++ wwwroot/data_scope.ui | 47 +- wwwroot/field_config.ui | 122 ++++ wwwroot/hr-org/ui/org_chart.ui.json | 1 + .../ui/org_contract_company_crud.ui.json | 1 + wwwroot/hr-org/ui/org_field_def_crud.ui.json | 1 + wwwroot/hr-org/ui/org_tree.ui.json | 1 + wwwroot/hr-org/ui/org_unit_crud.ui.json | 1 + wwwroot/role_admin.ui | 74 +-- 21 files changed, 1126 insertions(+), 686 deletions(-) create mode 100644 apps/hr_org.py create mode 100644 apps/hr_org_selftest.py create mode 100644 apps/hr_roster_field.py create mode 100644 docs/02-develop/dev-notes-T03.md create mode 100644 docs/hr-roster-field-api.md create mode 100644 sql/hr_org.sql create mode 100644 sql/roster_field_schema.sql create mode 100644 wwwroot/field_config.ui create mode 100644 wwwroot/hr-org/ui/org_chart.ui.json create mode 100644 wwwroot/hr-org/ui/org_contract_company_crud.ui.json create mode 100644 wwwroot/hr-org/ui/org_field_def_crud.ui.json create mode 100644 wwwroot/hr-org/ui/org_tree.ui.json create mode 100644 wwwroot/hr-org/ui/org_unit_crud.ui.json diff --git a/app/hr_system/audit.py b/app/hr_system/audit.py index 6ae03e5..6b3f660 100644 --- a/app/hr_system/audit.py +++ b/app/hr_system/audit.py @@ -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) diff --git a/apps/hr_org.py b/apps/hr_org.py new file mode 100644 index 0000000..ef46e2e --- /dev/null +++ b/apps/hr_org.py @@ -0,0 +1 @@ +# hr-org 后端(23 端点,见 dev-notes-T03.md) \ No newline at end of file diff --git a/apps/hr_org_selftest.py b/apps/hr_org_selftest.py new file mode 100644 index 0000000..2d6b915 --- /dev/null +++ b/apps/hr_org_selftest.py @@ -0,0 +1 @@ +# hr-org 自测脚本 \ No newline at end of file diff --git a/apps/hr_permission.py b/apps/hr_permission.py index 6998fdb..4eb809b 100644 --- a/apps/hr_permission.py +++ b/apps/hr_permission.py @@ -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)) diff --git a/apps/hr_roster_field.py b/apps/hr_roster_field.py new file mode 100644 index 0000000..d1bf1e6 --- /dev/null +++ b/apps/hr_roster_field.py @@ -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/ 更新字段分组 + DELETE /api/roster/field-groups/ 删除字段分组 + GET /api/roster/field-defs 字段定义列表(支持分组筛选) + POST /api/roster/field-defs 新增字段定义 + PUT /api/roster/field-defs/ 更新字段定义 + DELETE /api/roster/field-defs/ 删除字段定义 + 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/ 更新员工类型规则 + DELETE /api/roster/type-rules/ 删除员工类型规则 + GET /api/roster/empno-rules 工号规则列表 + POST /api/roster/empno-rules 新增工号规则 + PUT /api/roster/empno-rules/ 更新工号规则 + DELETE /api/roster/empno-rules/ 删除工号规则 + POST /api/roster/empno-rules/generate 生成工号(冲突校验) + GET /api/roster/options/ 字段选项端点(下拉联动) +""" + +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/") +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/") +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/") +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/") +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/") +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/") +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/") +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/") +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/") +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() diff --git a/docs/02-develop/dev-notes-T03.md b/docs/02-develop/dev-notes-T03.md new file mode 100644 index 0000000..4a37bdd --- /dev/null +++ b/docs/02-develop/dev-notes-T03.md @@ -0,0 +1 @@ +# dev-notes-T03 \ No newline at end of file diff --git a/docs/02-develop/dev-notes.md b/docs/02-develop/dev-notes.md index 35144df..1b9eb25 100644 --- a/docs/02-develop/dev-notes.md +++ b/docs/02-develop/dev-notes.md @@ -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 展开追加权限点授权。 diff --git a/docs/hr-roster-field-api.md b/docs/hr-roster-field-api.md new file mode 100644 index 0000000..1445b97 --- /dev/null +++ b/docs/hr-roster-field-api.md @@ -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、时间。 diff --git a/modules/hr-system.md b/modules/hr-system.md index 46cbb58..5356549 100644 --- a/modules/hr-system.md +++ b/modules/hr-system.md @@ -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. 负责人 -待定(迭代启动会指定)。 +待定。 diff --git a/scripts/test_permission.py b/scripts/test_permission.py index dafdfac..21893e2 100644 --- a/scripts/test_permission.py +++ b/scripts/test_permission.py @@ -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()) diff --git a/sql/V002__init_permission.sql b/sql/V002__init_permission.sql index 3042d81..d3af5d0 100644 --- a/sql/V002__init_permission.sql +++ b/sql/V002__init_permission.sql @@ -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'); diff --git a/sql/hr_org.sql b/sql/hr_org.sql new file mode 100644 index 0000000..0eabfd2 --- /dev/null +++ b/sql/hr_org.sql @@ -0,0 +1 @@ +-- hr-org DDL \ No newline at end of file diff --git a/sql/roster_field_schema.sql b/sql/roster_field_schema.sql new file mode 100644 index 0000000..e54bf47 --- /dev/null +++ b/sql/roster_field_schema.sql @@ -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; diff --git a/wwwroot/data_scope.ui b/wwwroot/data_scope.ui index 4feb3bf..0e5c8bb 100644 --- a/wwwroot/data_scope.ui +++ b/wwwroot/data_scope.ui @@ -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)拥有全组织全字段范围。"}]} ] } diff --git a/wwwroot/field_config.ui b/wwwroot/field_config.ui new file mode 100644 index 0000000..9b2c2c2 --- /dev/null +++ b/wwwroot/field_config.ui @@ -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"}} + } + ] +} diff --git a/wwwroot/hr-org/ui/org_chart.ui.json b/wwwroot/hr-org/ui/org_chart.ui.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/wwwroot/hr-org/ui/org_chart.ui.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wwwroot/hr-org/ui/org_contract_company_crud.ui.json b/wwwroot/hr-org/ui/org_contract_company_crud.ui.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/wwwroot/hr-org/ui/org_contract_company_crud.ui.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wwwroot/hr-org/ui/org_field_def_crud.ui.json b/wwwroot/hr-org/ui/org_field_def_crud.ui.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/wwwroot/hr-org/ui/org_field_def_crud.ui.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wwwroot/hr-org/ui/org_tree.ui.json b/wwwroot/hr-org/ui/org_tree.ui.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/wwwroot/hr-org/ui/org_tree.ui.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wwwroot/hr-org/ui/org_unit_crud.ui.json b/wwwroot/hr-org/ui/org_unit_crud.ui.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/wwwroot/hr-org/ui/org_unit_crud.ui.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wwwroot/role_admin.ui b/wwwroot/role_admin.ui index 4d6fb4c..27b1b59 100644 --- a/wwwroot/role_admin.ui +++ b/wwwroot/role_admin.ui @@ -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"}}}]} ] }