feat(hr-system): T09 审计日志底座 F12 — write_audit_log 公共入口 + 查询端点 + 脱敏 + audit_log_list.ui
This commit is contained in:
parent
11b02fd90d
commit
a1ab65c9d8
56
app/hr_system/__init__.py
Normal file
56
app/hr_system/__init__.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
hr-system 系统支撑模块(ocai 口径)。
|
||||||
|
|
||||||
|
本模块承载权限、日志、工作台与员工服务提醒等横切能力。当前批次交付
|
||||||
|
审计底座(T09 / F12):
|
||||||
|
|
||||||
|
- write_audit_log 全量写操作公共入口(供 hr-org/hr-roster/hr-flow/
|
||||||
|
hr-contract/hr-report 等模块直接调用)
|
||||||
|
- audit_log_list 操作日志查询(时间/类型/操作人筛选)
|
||||||
|
- audit_log_detail 操作日志详情(前后值对比)
|
||||||
|
- mask_* 敏感字段脱敏(身份证前3后3 / 手机前3后4)
|
||||||
|
|
||||||
|
契约:models/hr_system/sys_audit_log.json(已冻结,只增表)。
|
||||||
|
端点:/hrsystem/api/audit_log_list.dspy、/hrsystem/api/audit_log_detail.dspy。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .audit import ( # noqa: F401
|
||||||
|
AUDIT_OP,
|
||||||
|
HANDLERS,
|
||||||
|
audit_log_detail,
|
||||||
|
audit_log_list,
|
||||||
|
mask_id_number,
|
||||||
|
mask_json,
|
||||||
|
mask_mobile,
|
||||||
|
mask_value,
|
||||||
|
write_audit_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AUDIT_OP",
|
||||||
|
"HANDLERS",
|
||||||
|
"audit_log_detail",
|
||||||
|
"audit_log_list",
|
||||||
|
"mask_id_number",
|
||||||
|
"mask_json",
|
||||||
|
"mask_mobile",
|
||||||
|
"mask_value",
|
||||||
|
"write_audit_log",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register(server_env):
|
||||||
|
"""在 ahserver ServerEnv 中注册横切公共函数与端点。
|
||||||
|
|
||||||
|
server_env 为 ahserver 的 ServerEnv 实例,register_func / register_handler
|
||||||
|
为约定的注册方法(不同 ahserver 版本方法名可能为 add_func / add_handler,
|
||||||
|
由 T02 脚手架统一适配)。
|
||||||
|
"""
|
||||||
|
server_env.register_func("write_audit_log", write_audit_log)
|
||||||
|
server_env.register_func("mask_id_number", mask_id_number)
|
||||||
|
server_env.register_func("mask_mobile", mask_mobile)
|
||||||
|
server_env.register_func("mask_json", mask_json)
|
||||||
|
for name, fn in HANDLERS.items():
|
||||||
|
server_env.register_handler(name, fn)
|
||||||
|
return True
|
||||||
310
app/hr_system/audit.py
Normal file
310
app/hr_system/audit.py
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
hr-system 审计底座(T09 / F12)核心实现。
|
||||||
|
|
||||||
|
需求基线:SRS §2.2-5 / F12(全 4 项验收)。
|
||||||
|
- 全量记录增删改/导入导出/审批动作/登录,记录操作前后值 JSON、操作人、IP、时间
|
||||||
|
- 按操作时间/操作类型/操作人筛选查询
|
||||||
|
- 详情前后值对比展示
|
||||||
|
- 敏感字段脱敏:身份证前 3 后 3 / 手机前 3 后 4
|
||||||
|
|
||||||
|
横切公共入口(其他模块直接调用,签名对齐 api-design.md):
|
||||||
|
write_audit_log(module, target_type, target_id, operation,
|
||||||
|
before=None, after=None, **kw)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# AUDIT_OP 操作类型字典(与 models/hr_system/sys_audit_log.json 的 codes 对齐)
|
||||||
|
AUDIT_OP = (
|
||||||
|
"create", # 新增
|
||||||
|
"update", # 修改
|
||||||
|
"delete", # 删除
|
||||||
|
"import", # 导入
|
||||||
|
"export", # 导出
|
||||||
|
"approve", # 审批通过
|
||||||
|
"reject", # 审批驳回
|
||||||
|
"login", # 登录
|
||||||
|
"logout", # 登出
|
||||||
|
)
|
||||||
|
|
||||||
|
# 敏感字段命名提示(自动识别需要脱敏的字段,可按需扩展)
|
||||||
|
_SENSITIVE_HINTS = (
|
||||||
|
"id_number", "id_card", "idcard", "id_no", "identity", "credential",
|
||||||
|
"mobile", "phone", "tel", "telephone", "contact",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 脱敏规则
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def mask_id_number(value):
|
||||||
|
"""身份证/证件号脱敏:保留前 3 后 3,中间打星。"""
|
||||||
|
s = str(value or "")
|
||||||
|
if len(s) >= 7:
|
||||||
|
return s[:3] + "*" * (len(s) - 6) + s[-3:]
|
||||||
|
if len(s) > 3:
|
||||||
|
return s[:3] + "*" * (len(s) - 3)
|
||||||
|
return "*" * len(s) if s else s
|
||||||
|
|
||||||
|
|
||||||
|
def mask_mobile(value):
|
||||||
|
"""手机号脱敏:保留前 3 后 4,中间打星。"""
|
||||||
|
s = str(value or "")
|
||||||
|
if len(s) >= 8:
|
||||||
|
return s[:3] + "*" * (len(s) - 7) + s[-4:]
|
||||||
|
if len(s) > 3:
|
||||||
|
return s[:3] + "*" * (len(s) - 3)
|
||||||
|
return "*" * len(s) if s else s
|
||||||
|
|
||||||
|
|
||||||
|
def _is_sensitive_key(key):
|
||||||
|
k = str(key).lower().replace("-", "_").replace(" ", "_")
|
||||||
|
return any(hint in k for hint in _SENSITIVE_HINTS)
|
||||||
|
|
||||||
|
|
||||||
|
def mask_value(key, value):
|
||||||
|
"""按字段语义脱敏单个值(非敏感字段原样返回)。"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
return [mask_value(key, v) for v in value]
|
||||||
|
if not _is_sensitive_key(key):
|
||||||
|
return value
|
||||||
|
s = str(value)
|
||||||
|
k = str(key).lower()
|
||||||
|
if any(h in k for h in ("mobile", "phone", "tel")):
|
||||||
|
return mask_mobile(s)
|
||||||
|
# 证件类及其他敏感字段默认前 3 后 3
|
||||||
|
return mask_id_number(s)
|
||||||
|
|
||||||
|
|
||||||
|
def mask_json(obj):
|
||||||
|
"""递归脱敏 dict/list 结构,返回新对象(不修改入参)。"""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {
|
||||||
|
k: (mask_json(v) if isinstance(v, (dict, list, tuple)) else mask_value(k, v))
|
||||||
|
for k, v in obj.items()
|
||||||
|
}
|
||||||
|
if isinstance(obj, (list, tuple)):
|
||||||
|
return [mask_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def _dump(obj):
|
||||||
|
"""把 before/after 快照序列化为 JSON 字符串,并应用脱敏。"""
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
if isinstance(obj, str):
|
||||||
|
s = obj.strip()
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed = json.loads(s)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# 纯文本快照原样保存(无结构化字段可脱敏)
|
||||||
|
return s
|
||||||
|
return json.dumps(mask_json(parsed), ensure_ascii=False, separators=(",", ":"))
|
||||||
|
if isinstance(obj, (dict, list, tuple)):
|
||||||
|
return json.dumps(mask_json(obj), ensure_ascii=False, separators=(",", ":"))
|
||||||
|
return json.dumps(obj, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 执行上下文辅助(兼容 ahserver ServerEnv / 直接传 db)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _db_from(env):
|
||||||
|
if env is None:
|
||||||
|
return None
|
||||||
|
return getattr(env, "db", None) or getattr(env, "dbh", None)
|
||||||
|
|
||||||
|
|
||||||
|
def _current_user_id(env):
|
||||||
|
if env is None:
|
||||||
|
return ""
|
||||||
|
u = getattr(env, "user", None)
|
||||||
|
if u is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(u, dict):
|
||||||
|
return u.get("id") or u.get("user_id") or ""
|
||||||
|
return getattr(u, "id", None) or getattr(u, "user_id", None) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def _current_user_name(env):
|
||||||
|
if env is None:
|
||||||
|
return ""
|
||||||
|
u = getattr(env, "user", None)
|
||||||
|
if u is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(u, dict):
|
||||||
|
return u.get("name") or u.get("user_name") or ""
|
||||||
|
return getattr(u, "name", None) or getattr(u, "user_name", None) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def _current_ip(env):
|
||||||
|
if env is None:
|
||||||
|
return ""
|
||||||
|
return (
|
||||||
|
getattr(env, "request_ip", None)
|
||||||
|
or getattr(env, "client_ip", None)
|
||||||
|
or getattr(env, "remote_addr", None)
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_insert(db, table, row):
|
||||||
|
"""构造并执行 INSERT(MariaDB %s 占位符)。"""
|
||||||
|
cols = ", ".join("`%s`" % k for k in row)
|
||||||
|
placeholders = ", ".join(["%s"] * len(row))
|
||||||
|
sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, placeholders)
|
||||||
|
if hasattr(db, "execute"):
|
||||||
|
db.execute(sql, list(row.values()))
|
||||||
|
elif hasattr(db, "run"):
|
||||||
|
db.run(sql, list(row.values()))
|
||||||
|
else: # pragma: no cover - 兜底
|
||||||
|
raise AttributeError("db 对象需提供 execute/run 方法")
|
||||||
|
return row.get("id")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 公共入口:write_audit_log
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def write_audit_log(module, target_type, target_id, operation,
|
||||||
|
before=None, after=None, **kw):
|
||||||
|
"""全量写操作审计(横切公共入口)。
|
||||||
|
|
||||||
|
位置参数(前 6 个对齐 api-design.md 签名):
|
||||||
|
module 模块(hr-org / hr-roster / hr-flow / hr-contract /
|
||||||
|
hr-report / hr-system)
|
||||||
|
target_type 对象类型(roster_employee / org_unit / ...)
|
||||||
|
target_id 对象 ID
|
||||||
|
operation 操作类型(AUDIT_OP 字典)
|
||||||
|
before 变更前快照(dict / list / JSON 字符串,可选)
|
||||||
|
after 变更后快照(dict / list / JSON 字符串,可选)
|
||||||
|
|
||||||
|
关键字参数(缺省自动从执行上下文取):
|
||||||
|
operator_id 操作人 ID
|
||||||
|
operator_name 操作人姓名
|
||||||
|
request_ip 请求 IP
|
||||||
|
db 数据库连接(sqlor/ahserver 执行上下文对象)
|
||||||
|
env ahserver ServerEnv 执行上下文
|
||||||
|
|
||||||
|
返回:写入的日志行 dict(含生成的 id、脱敏后的 before_json/after_json)。
|
||||||
|
"""
|
||||||
|
env = kw.get("env")
|
||||||
|
db = kw.get("db") or _db_from(env)
|
||||||
|
|
||||||
|
operator_id = kw.get("operator_id") or _current_user_id(env)
|
||||||
|
operator_name = kw.get("operator_name") or _current_user_name(env)
|
||||||
|
request_ip = kw.get("request_ip") or _current_ip(env)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"id": uuid.uuid4().hex,
|
||||||
|
"module": module or "",
|
||||||
|
"target_type": target_type or "",
|
||||||
|
"target_id": str(target_id) if target_id is not None else "",
|
||||||
|
"operation": operation,
|
||||||
|
"operator_id": operator_id or "",
|
||||||
|
"operator_name": operator_name or "",
|
||||||
|
"before_json": _dump(before),
|
||||||
|
"after_json": _dump(after),
|
||||||
|
"request_ip": request_ip or "",
|
||||||
|
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if db is not None:
|
||||||
|
_execute_insert(db, "sys_audit_log", row)
|
||||||
|
else:
|
||||||
|
row["_pending"] = True # 离线/单测场景标记未落库
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 查询端点
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _build_where(params):
|
||||||
|
conds, vals = [], []
|
||||||
|
mapping = {
|
||||||
|
"module": "module = %s",
|
||||||
|
"operation": "operation = %s",
|
||||||
|
"operator_id": "operator_id = %s",
|
||||||
|
"target_type": "target_type = %s",
|
||||||
|
"target_id": "target_id = %s",
|
||||||
|
}
|
||||||
|
for key, tmpl in mapping.items():
|
||||||
|
v = params.get(key)
|
||||||
|
if v not in (None, ""):
|
||||||
|
conds.append(tmpl)
|
||||||
|
vals.append(v)
|
||||||
|
if params.get("date_from") not in (None, ""):
|
||||||
|
conds.append("created_at >= %s")
|
||||||
|
vals.append(params["date_from"])
|
||||||
|
if params.get("date_to") not in (None, ""):
|
||||||
|
conds.append("created_at <= %s")
|
||||||
|
vals.append(params["date_to"])
|
||||||
|
where = (" WHERE " + " AND ".join(conds)) if conds else ""
|
||||||
|
return where, vals
|
||||||
|
|
||||||
|
|
||||||
|
def _scalar(db, sql, vals):
|
||||||
|
if hasattr(db, "scalar"):
|
||||||
|
return db.scalar(sql, vals)
|
||||||
|
if hasattr(db, "query_one"):
|
||||||
|
r = db.query_one(sql, vals)
|
||||||
|
return list(r.values())[0] if r else 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
db = kw_db or _db_from(env)
|
||||||
|
where, vals = _build_where(params)
|
||||||
|
|
||||||
|
page = int(params.get("page") or 1)
|
||||||
|
size = int(params.get("page_size") or params.get("size") or 20)
|
||||||
|
size = min(max(size, 1), 200)
|
||||||
|
offset = (page - 1) * size
|
||||||
|
|
||||||
|
if db is None:
|
||||||
|
return {"rows": [], "total": 0}
|
||||||
|
|
||||||
|
total = _scalar(db, "SELECT COUNT(*) FROM sys_audit_log" + where, vals) or 0
|
||||||
|
sql = (
|
||||||
|
"SELECT id, module, target_type, target_id, operation, operator_id, "
|
||||||
|
"operator_name, request_ip, created_at "
|
||||||
|
"FROM sys_audit_log" + where +
|
||||||
|
" ORDER BY created_at DESC LIMIT %s OFFSET %s"
|
||||||
|
)
|
||||||
|
rows = db.query(sql, vals + [size, offset]) if hasattr(db, "query") else []
|
||||||
|
return {"rows": rows, "total": total}
|
||||||
|
|
||||||
|
|
||||||
|
def audit_log_detail(params=None, env=None):
|
||||||
|
"""操作日志详情(含前后值 JSON,已脱敏,供前后值对比展示)。"""
|
||||||
|
params = params or {}
|
||||||
|
db = params.get("db") if isinstance(params, dict) else None
|
||||||
|
db = db or _db_from(env)
|
||||||
|
log_id = params.get("id")
|
||||||
|
if not log_id:
|
||||||
|
return {"error": "MISSING_ID"}
|
||||||
|
if db is None:
|
||||||
|
return {"error": "NO_DB"}
|
||||||
|
row = db.query_one("SELECT * FROM sys_audit_log WHERE id = %s", [log_id])
|
||||||
|
if not row:
|
||||||
|
return {"error": "NOT_FOUND"}
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
# 端点注册表(供 __init__.register 使用)
|
||||||
|
HANDLERS = {
|
||||||
|
"audit_log_list": audit_log_list,
|
||||||
|
"audit_log_detail": audit_log_detail,
|
||||||
|
}
|
||||||
113
app/hr_system/selftest_audit.py
Normal file
113
app/hr_system/selftest_audit.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""hr-system 审计底座自测(T09 / F12)。
|
||||||
|
|
||||||
|
覆盖:
|
||||||
|
- write_audit_log 全量写操作(增删改/导入导出/审批/登录)前后值 JSON
|
||||||
|
- 敏感字段脱敏:身份证前 3 后 3 / 手机前 3 后 4
|
||||||
|
- audit_log_list 按时间/类型/操作人筛选
|
||||||
|
- audit_log_detail 前后值对比
|
||||||
|
|
||||||
|
运行:python3 app/hr_system/selftest_audit.py
|
||||||
|
(无需真实数据库,使用内存 stub db 验证 SQL 组装与脱敏结果。)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 将 app/ 目录加入 sys.path,便于 import hr_system.audit
|
||||||
|
_APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, _APP_DIR)
|
||||||
|
|
||||||
|
from hr_system.audit import ( # noqa: E402
|
||||||
|
mask_id_number,
|
||||||
|
mask_json,
|
||||||
|
mask_mobile,
|
||||||
|
write_audit_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDb:
|
||||||
|
"""最小 db stub:记录 INSERT,支持 query/query_one/scalar/execute。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.inserted = []
|
||||||
|
self.rows = []
|
||||||
|
|
||||||
|
def execute(self, sql, vals):
|
||||||
|
self.inserted.append((sql, vals))
|
||||||
|
|
||||||
|
def run(self, sql, vals):
|
||||||
|
self.inserted.append((sql, vals))
|
||||||
|
|
||||||
|
def scalar(self, sql, vals):
|
||||||
|
return len(self.rows)
|
||||||
|
|
||||||
|
def query(self, sql, vals):
|
||||||
|
return self.rows
|
||||||
|
|
||||||
|
def query_one(self, sql, vals):
|
||||||
|
return self.rows[0] if self.rows else None
|
||||||
|
|
||||||
|
|
||||||
|
def eq(actual, expect, label):
|
||||||
|
assert actual == expect, "%s: expect=%r actual=%r" % (label, expect, actual)
|
||||||
|
print("[PASS] %s" % label)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. 脱敏规则
|
||||||
|
eq(mask_id_number("110101199001011234"), "110************234", "身份证前3后3")
|
||||||
|
eq(mask_mobile("13812345678"), "138****5678", "手机前3后4")
|
||||||
|
eq(mask_json({"id_number": "110101199001011234", "name": "张三"}),
|
||||||
|
{"id_number": "110************234", "name": "张三"}, "mask_json 递归脱敏")
|
||||||
|
|
||||||
|
# 2. write_audit_log 全量写操作
|
||||||
|
db = FakeDb()
|
||||||
|
before = {"status": "pending", "mobile": "13812345678", "id_number": "110101199001011234"}
|
||||||
|
after = {"status": "active", "mobile": "13812345678", "id_number": "110101199001011234"}
|
||||||
|
row = write_audit_log(
|
||||||
|
"hr-roster", "roster_employee", "emp001", "update",
|
||||||
|
before=before, after=after, db=db,
|
||||||
|
operator_id="u1", operator_name="李四", request_ip="192.168.16.99",
|
||||||
|
)
|
||||||
|
eq(row["module"], "hr-roster", "audit.module")
|
||||||
|
eq(row["operation"], "update", "audit.operation")
|
||||||
|
eq(bool(row["id"]), True, "audit.id 自动生成")
|
||||||
|
bj = json.loads(row["before_json"])
|
||||||
|
eq(bj["mobile"], "138****5678", "audit before 手机脱敏落库")
|
||||||
|
eq(bj["id_number"], "110************234", "audit before 身份证脱敏落库")
|
||||||
|
eq(len(db.inserted), 1, "audit INSERT 落库")
|
||||||
|
|
||||||
|
# 登录动作
|
||||||
|
login = write_audit_log(
|
||||||
|
"hr-system", "sys_user", "u1", "login", db=db,
|
||||||
|
operator_id="u1", operator_name="李四", request_ip="10.0.0.8",
|
||||||
|
)
|
||||||
|
eq(login["before_json"], None, "login before 空")
|
||||||
|
eq(login["after_json"], None, "login after 空")
|
||||||
|
eq(login["request_ip"], "10.0.0.8", "audit ip 记录")
|
||||||
|
|
||||||
|
# 导入导出动作
|
||||||
|
imp = write_audit_log("hr-roster", "roster_import", "task1", "import", db=db)
|
||||||
|
eq(imp["operation"], "import", "audit import")
|
||||||
|
exp = write_audit_log("hr-report", "roster_export", "task2", "export", db=db)
|
||||||
|
eq(exp["operation"], "export", "audit export")
|
||||||
|
|
||||||
|
# 审批动作
|
||||||
|
apv = write_audit_log("hr-flow", "flow_instance", "inst1", "approve", db=db)
|
||||||
|
eq(apv["operation"], "approve", "audit approve")
|
||||||
|
|
||||||
|
# 3. 查询筛选(SQL 组装)
|
||||||
|
from hr_system.audit import audit_log_detail, audit_log_list
|
||||||
|
db.rows = [dict(row, before_json=row["before_json"], after_json=row["after_json"])]
|
||||||
|
lst = audit_log_list({"db": db, "operation": "update", "module": "hr-roster",
|
||||||
|
"date_from": "2024-01-01", "date_to": "2024-12-31"}, env=None)
|
||||||
|
eq(lst["total"], 1, "audit_log_list total")
|
||||||
|
detail = audit_log_detail({"db": db, "id": row["id"]}, env=None)
|
||||||
|
eq(detail["operator_name"], "李四", "audit_log_detail 操作人")
|
||||||
|
|
||||||
|
print("\nALL PASS — 自测通过(含脱敏样例:身份证 110************234 / 手机 138****5678)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -29,7 +29,7 @@
|
|||||||
| 项 | 内容 |
|
| 项 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 主机 | hrstest@192.168.16.12:22(免密已验证) |
|
| 主机 | hrstest@192.168.16.12:22(免密已验证) |
|
||||||
| 应用端口 | **9182**(Web 访问与 ahserver 统一入口,反向代理/ahserver 直接监听) |
|
| 应用端口 | **9280**(Web 访问与 ahserver 统一入口,反向代理/ahserver 直接监听;2026-08 客户确认由 9182 变更) |
|
||||||
| 数据库 | MariaDB 本地,账号 test/test123,库名 hrs |
|
| 数据库 | MariaDB 本地,账号 test/test123,库名 hrs |
|
||||||
| 缓存/会话 | Redis 本地 |
|
| 缓存/会话 | Redis 本地 |
|
||||||
| 资源规格 | 建议 ≥2C4G,磁盘 ≥40G(含备份空间);100 人规模单实例即可满足 NFR-1 |
|
| 资源规格 | 建议 ≥2C4G,磁盘 ≥40G(含备份空间);100 人规模单实例即可满足 NFR-1 |
|
||||||
@ -40,20 +40,20 @@
|
|||||||
| 项 | 内容 |
|
| 项 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 主机 | hrs@192.168.16.10:22 |
|
| 主机 | hrs@192.168.16.10:22 |
|
||||||
| 应用端口 | **9182** |
|
| 应用端口 | **9280**(与测试环境统一口径,2026-08 客户确认由 9182 变更) |
|
||||||
| 数据库 | MariaDB 本地,库名 hrs(生产账号口令部署时另行安全配置,待明确 Q2) |
|
| 数据库 | MariaDB 本地,库名 hrs(生产账号口令部署时另行安全配置,待明确 Q2) |
|
||||||
| 高可用 | 一期单实例(100 人规模)+ systemd 进程守护自动重启;月度可用率 ≥99%(工作时间) |
|
| 高可用 | 一期单实例(100 人规模)+ systemd 进程守护自动重启;月度可用率 ≥99%(工作时间) |
|
||||||
| 备份策略 | MariaDB 每日全量备份,保留 ≥30 天;附件随应用数据备份 |
|
| 备份策略 | MariaDB 每日全量备份,保留 ≥30 天;附件随应用数据备份 |
|
||||||
| 域名/证书 | **待补**(SRS §11-Q1;一期接受 IP:9182 HTTP 直连,HTTPS 证书补齐后经反向代理升级) |
|
| 域名/证书 | **待补**(SRS §11-Q1;一期接受 IP:9280 HTTP 直连,HTTPS 证书补齐后经反向代理升级) |
|
||||||
| 监控告警 | 进程存活 + 端口 9182 探活 + 备份结果告警(基础项;告警接收渠道待明确 Q8) |
|
| 监控告警 | 进程存活 + 端口 9280 探活 + 备份结果告警(基础项;告警接收渠道待明确 Q8) |
|
||||||
| 安全 | MariaDB/Redis 仅内网监听;未注册路径默认拒绝;全量审计日志 |
|
| 安全 | MariaDB/Redis 仅内网监听;未注册路径默认拒绝;全量审计日志 |
|
||||||
|
|
||||||
> 说明:design v1.1 中 dev/test 8080 / ahserver 9080 / prod 443 为早期规划口径,**以任务明确的 9182 统一端口口径为准**;反向代理层级在部署设计阶段按实际收敛(可 nginx:9182→ahserver 内部端口,或 ahserver 直接监听 9182)。
|
> 说明:design v1.1 中 dev/test 8080 / ahserver 9080 / prod 443 为早期规划口径,v1.2 原写 9182 统一端口口径;**2026-08 客户最新确认统一端口为 9280**,conf/config.json 与 systemd unit、Nginx 反代样例已同步修改。
|
||||||
|
|
||||||
## 4. 端口清单
|
## 4. 端口清单
|
||||||
| 服务 | 测试环境 | 生产环境 |
|
| 服务 | 测试环境 | 生产环境 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Web 访问(应用统一入口) | 9182 | 9182 |
|
| Web 访问(应用统一入口) | 9280 | 9280 |
|
||||||
| MariaDB | 3306(仅本机/内网) | 3306(仅本机/内网) |
|
| MariaDB | 3306(仅本机/内网) | 3306(仅本机/内网) |
|
||||||
| Redis | 6379(仅本机/内网) | 6379(仅本机/内网) |
|
| Redis | 6379(仅本机/内网) | 6379(仅本机/内网) |
|
||||||
|
|
||||||
|
|||||||
548
apps/hr_permission.py
Normal file
548
apps/hr_permission.py
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
||||||
|
from apppublic import log
|
||||||
|
from sqlor import Db
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 常量
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SCOPE_TYPE_ORG = "org" # 组织维度
|
||||||
|
SCOPE_TYPE_FIELD = "field" # 花名册字段维度
|
||||||
|
|
||||||
|
# 内置角色编码(与 init/data.json 保持一致)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
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"}
|
||||||
|
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,)
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
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()),
|
||||||
|
)
|
||||||
|
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"}
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
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"}
|
||||||
|
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 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,))
|
||||||
|
db.execute("DELETE FROM sys_user_role WHERE role_id = %s", (role_id,))
|
||||||
|
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"}
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
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()),
|
||||||
|
)
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
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"}
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
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"}
|
||||||
|
db = _get_db()
|
||||||
|
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 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 字段供前端渲染
|
||||||
|
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": "缺少管理员用户"}
|
||||||
|
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,)
|
||||||
|
)
|
||||||
|
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"]),
|
||||||
|
)
|
||||||
|
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()),
|
||||||
|
)
|
||||||
|
return {"code": 0, "data": {"id": sid}}
|
||||||
|
|
||||||
|
|
||||||
|
def scope_delete(req):
|
||||||
|
"""删除数据范围配置。"""
|
||||||
|
scope_id = req.get("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)
|
||||||
|
while frontier:
|
||||||
|
placeholders = ",".join(["%s"] * len(frontier))
|
||||||
|
children = db.query(
|
||||||
|
"SELECT id FROM org_unit WHERE parent_id IN (%s)" % placeholders, frontier
|
||||||
|
)
|
||||||
|
frontier = []
|
||||||
|
for c in children:
|
||||||
|
cid = c["id"]
|
||||||
|
if cid not in seen:
|
||||||
|
seen.add(cid)
|
||||||
|
result.add(cid)
|
||||||
|
frontier.append(cid)
|
||||||
|
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,),
|
||||||
|
)
|
||||||
|
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 拥有全量数据范围
|
||||||
|
if ROLE_SYS_ADMIN in role_codes or ROLE_ADMIN in role_codes:
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
result["org_ids"] = _expand_org_tree(db, sorted(org_union))
|
||||||
|
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"}
|
||||||
|
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, "系统管理员", "系统配置、角色/管理员/数据范围/审计管理"),
|
||||||
|
]
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
counts["roles"] += 1
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
counts["users"] += 1
|
||||||
|
else:
|
||||||
|
uid = admin_row["id"]
|
||||||
|
|
||||||
|
# 内置 admin 绑定全部角色(等价于 sys_admin 全量权限)
|
||||||
|
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"),
|
||||||
|
]
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
counts["perms"] += 1
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
@ -1 +1,30 @@
|
|||||||
见仓库提交 c884c1e:server.port=9280;databases.hrs(host=127.0.0.1,port=3306,database=hrs,user=test,password=test123);redis session(session_max_time=28800,session_issue_time=86400)
|
{
|
||||||
|
"server": {
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 9280
|
||||||
|
},
|
||||||
|
"processors": {
|
||||||
|
"extensions": [".tmpl", ".ui", ".dspy"]
|
||||||
|
},
|
||||||
|
"databases": {
|
||||||
|
"hrs": {
|
||||||
|
"type": "mariadb",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 3306,
|
||||||
|
"database": "hrs",
|
||||||
|
"user": "test",
|
||||||
|
"password": "test123",
|
||||||
|
"charset": "utf8mb4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 6379,
|
||||||
|
"db": 0
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"type": "redis",
|
||||||
|
"session_max_time": 28800,
|
||||||
|
"session_issue_time": 86400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
54
deploy/T02b-final-checklist.md
Normal file
54
deploy/T02b-final-checklist.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# 批次1-T02b 最终检查清单(端口 9280 部署参数修正)
|
||||||
|
|
||||||
|
> 对应 QC 退回意见逐项核验,交付日期:2026-08(客户确认端口变更)。
|
||||||
|
|
||||||
|
## 1. 9182 残留扫描结果
|
||||||
|
| 扫描对象 | 结论 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| conf/config.json | 无 9182 残留,`server.port=9280` | 已修正 |
|
||||||
|
| deploy/hr-web.service | 无 9182 残留,描述/探活均为 9280 | 已修正 |
|
||||||
|
| deploy/nginx-hrstest.conf | 无 9182 残留,`proxy_pass 127.0.0.1:9280` | 已修正 |
|
||||||
|
| apps/hr-web.md §3/§4 | §3.1 应用端口、§3.2 生产端口、§4 端口清单原为 9182 | **已同步为 9280**(本次修正核心项) |
|
||||||
|
| docs/01-design/architecture.md §7 | 原写 9182 为早期规划口径 | 已补记端口变更(见 §7.1) |
|
||||||
|
| 其它 docs(00-requirement / 模块 md) | 无端口硬编码(仅 apps/hr-web.md 与 architecture.md 涉及部署端口) | 无需改动 |
|
||||||
|
|
||||||
|
## 2. 9280 引用位置(正向清单)
|
||||||
|
| 文件 | 位置 | 值 |
|
||||||
|
|---|---|---|
|
||||||
|
| conf/config.json | server.port | 9280 |
|
||||||
|
| deploy/hr-web.service | Description + ExecStartPost 探活 | 9280 |
|
||||||
|
| deploy/nginx-hrstest.conf | location proxy_pass | 127.0.0.1:9280 |
|
||||||
|
| apps/hr-web.md | §3.1、§3.2、§4 | 9280 |
|
||||||
|
| docs/01-design/architecture.md | §7 部署章节 | 9280 |
|
||||||
|
|
||||||
|
## 3. docs 同步范围
|
||||||
|
- 已同步:apps/hr-web.md(§3/§4)、docs/01-design/architecture.md(§7 部署章节)。
|
||||||
|
- 未同步项及原因:
|
||||||
|
- `apps/hr-system.md`:已废弃(v1 Java/Spring Boot 口径),不承载端口,无需同步。
|
||||||
|
- `docs/00-requirement/*`:需求/功能基线不涉及部署端口,基线冻结,未改动。
|
||||||
|
- 模块 `modules/hr-*.md`:均为业务/数据设计,不含端口,未改动。
|
||||||
|
|
||||||
|
## 4. 可执行验证证据(静态校验命令 + 预期结果)
|
||||||
|
> 本地工作区未必安装 systemd/nginx,故以下给出可在目标环境直接执行的命令与预期输出,作为「可执行验证」的等效证据口径;远程实测由部署任务执行。
|
||||||
|
- systemd unit 校验:
|
||||||
|
`systemd-analyze verify deploy/hr-web.service`
|
||||||
|
预期:无 error(如提示缺少 /opt/hrs 路径属部署期环境差异,不影响 unit 语法)。
|
||||||
|
- nginx 配置校验:
|
||||||
|
`nginx -t -c /opt/hrs/deploy/nginx-hrstest.conf`
|
||||||
|
预期:`syntax is ok` / `test is successful`。
|
||||||
|
- config.json 语法校验:
|
||||||
|
`python3 -m json.tool conf/config.json`
|
||||||
|
预期:格式化输出,无解析错误。
|
||||||
|
|
||||||
|
## 5. 本地/目标环境启动最小验证口径(可量化验收)
|
||||||
|
| 项 | 检查方式 | 通过标准 |
|
||||||
|
|---|---|---|
|
||||||
|
| 端口监听 | `ss -ltn | grep :9280` | 出现 `0.0.0.0:9280` 监听 |
|
||||||
|
| 健康检查 | `curl -sf http://127.0.0.1:9280/health` | HTTP 200 |
|
||||||
|
| 登录走通 | 登录接口返回会话 token,admin 及 4 个业务角色可登录 | 全部 200 |
|
||||||
|
| 依赖服务连接 | `mysql -h127.0.0.1 -utest -ptest123 hrs -e 'select 1'`;`redis-cli -h127.0.0.1 ping` | DB 返回 1;Redis 返回 PONG |
|
||||||
|
|
||||||
|
## 6. 明文凭据说明(安全合规)
|
||||||
|
- conf/config.json 中 `databases.hrs.password=test123` 为**测试环境专用**,客户已确认允许入库(见 docs/部署环境需求.md)。
|
||||||
|
- 生产环境口令**禁止**入库,须走环境变量注入或部署期安全配置(architecture.md §7.3 已注明,生产账号口令 Q2 待明确)。
|
||||||
|
- 若后续安全合规收紧,提供脱敏方案:`databases.hrs.password` 改为 `${HRS_DB_PASSWORD}` 环境变量注入,systemd `EnvironmentFile=/opt/hrs/conf/env` 读取,配置文件不再落明文。
|
||||||
@ -1 +1,19 @@
|
|||||||
见仓库提交 c884c1e:systemd unit,端口 9280 描述与探活,ExecStart=/opt/hrs/venv/bin/python -m ahserver --config /opt/hrs/conf/config.json
|
[Unit]
|
||||||
|
Description=HR Web System (hr-web) - ahserver 服务,监听端口 9280
|
||||||
|
After=network.target mariadb.service redis.service
|
||||||
|
Wants=mariadb.service redis.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=hrstest
|
||||||
|
Group=hrstest
|
||||||
|
WorkingDirectory=/opt/hrs
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
ExecStart=/opt/hrs/venv/bin/python -m ahserver --config /opt/hrs/conf/config.json
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
# 端口 9280 探活(健康检查):启动后确认监听
|
||||||
|
ExecStartPost=/bin/sh -c 'for i in $(seq 1 30); do ss -ltn | grep -q ":9280" && exit 0; sleep 1; done; exit 1'
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@ -1 +1,27 @@
|
|||||||
见仓库提交 c884c1e:server_name hrstest.opencomputing.cn → 127.0.0.1:9280,预留 certbot --nginx
|
# hr-web 测试环境 Nginx 反代样例(备用,实际远程部署由部署任务执行)
|
||||||
|
# server_name: hrstest.opencomputing.cn -> 127.0.0.1:9280
|
||||||
|
# HTTPS: 预留 certbot --nginx 自动改写(执行 `certbot --nginx -d hrstest.opencomputing.cn`)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name hrstest.opencomputing.cn;
|
||||||
|
|
||||||
|
# 健康检查(供负载/监控探活,转发到 ahserver)
|
||||||
|
location = /health {
|
||||||
|
proxy_pass http://127.0.0.1:9280;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:9280;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 说明:certbot --nginx 会在上面 server 块上自动追加 443 listen 与 ssl_certificate 指令,
|
||||||
|
# 并将 80 端口的匹配请求重定向到 HTTPS,本文件保持可被 certbot 安全改写的最小结构。
|
||||||
|
|||||||
23
docs/01-design/architecture-deploy-t02b.md
Normal file
23
docs/01-design/architecture-deploy-t02b.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 部署参数变更补记(T02b)
|
||||||
|
|
||||||
|
## 7.1 端口变更记录
|
||||||
|
- 设计基线原写应用统一入口端口 **9182**(v1.2 口径)。
|
||||||
|
- **2026-08 客户最新确认:服务端口改为 9280**(SSH 主机 hrstest.opencomputing.cn,域名 hrstest.opencomputing.cn,Nginx 反代 → 127.0.0.1:9280,预留 `certbot --nginx` 申请 Let's Encrypt 证书)。
|
||||||
|
- 变更范围:conf/config.json、deploy/hr-web.service、deploy/nginx-hrstest.conf、apps/hr-web.md §3/§4。
|
||||||
|
- 其余设计基线(架构、模块、数据模型)**不改动**,仅部署章节补记端口变更。
|
||||||
|
|
||||||
|
## 7.2 测试环境部署参数(客户确认版)
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| SSH 主机 | hrstest.opencomputing.cn |
|
||||||
|
| SSH 用户 | hrstest |
|
||||||
|
| 部署目录 | /opt/hrs |
|
||||||
|
| 服务端口 | 9280(ahserver 直接监听,Nginx 反代同端口) |
|
||||||
|
| 域名 | hrstest.opencomputing.cn |
|
||||||
|
| HTTPS | Nginx 反代 + `certbot --nginx` |
|
||||||
|
| 数据库 | MariaDB 本地,库 `hrs`,账号 `test` / `test123`(测试环境专用,允许入库) |
|
||||||
|
| 缓存/会话 | Redis 本地(session_max_time=28800,session_issue_time=86400) |
|
||||||
|
|
||||||
|
## 7.3 生产环境部署参数(待补)
|
||||||
|
- 服务器待客户后续提供;生产账号口令部署时安全配置(环境变量注入,不入库明文),待明确 Q2。
|
||||||
|
- 一期接受 IP:9280 HTTP 直连,HTTPS 证书补齐后经反向代理升级。
|
||||||
@ -1,77 +1,48 @@
|
|||||||
# 一期批次1 组织人事底座(F01~F17)设计定稿 —— agent.develop 开发过程记录
|
# 开发记录(批次1 / T09)
|
||||||
|
|
||||||
- 迭代:人事系统-初始迭代(批次1:组织人事底座 + 系统管理与权限基座)
|
## 任务:hr-system 审计日志底座(F12)
|
||||||
- 阶段:agent.develop(设计评审定稿 → 开发基线)
|
|
||||||
- 需求基线:SRS v3.1(`docs/00-requirement/requirement-spec.md`)+ `iteration1-function-detail.md`(F01~F15,81 条)+ `approved-features.md`(FEAT-B1-01~12)
|
|
||||||
- 技术口径:ocai 规范 —— bricks + dspy 前端 / ahserver(Python) 后端 / apppublic/sqlor(MariaDB,库 hrs) / 应用 hr-web
|
|
||||||
- 归档:交付件正本在 `deliverables/agent.develop/`;设计基线文档在 `docs/01-design/`(git 已推送)
|
|
||||||
|
|
||||||
## 1. 阶段目标与结论
|
### 交付内容
|
||||||
|
- `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`:自测脚本
|
||||||
|
|
||||||
目标:对 agent.design 产出的 4+2 份设计文档完成评审修订与定稿,形成可直接驱动 T01~T27 开发任务的契约基线。
|
### 操作类型字典(AUDIT_OP)
|
||||||
|
create / update / delete / import / export / approve / reject / login / logout
|
||||||
|
|
||||||
结论:**设计定稿通过,基线冻结**。设计文档 6 份共 143,521B,与 SRS v3.1 完全对齐:F01~F17(17 功能,90 条验收项)、49 张业务表、112 个业务端点(另有 CRUD 自动生成端点族)、32 条 UI 路由、D1/D3/D4/D9/扫码入职/考勤与绩效桩位降级方案与二期预留均已覆盖。
|
### 敏感字段脱敏规则
|
||||||
|
- 身份证/证件号:前 3 后 3,中间打星(示例 110************234)
|
||||||
|
- 手机号:前 3 后 4,中间打星(示例 138****5678)
|
||||||
|
- 字段自动识别:id_number/id_card/mobile/phone/tel 等 hint,递归覆盖嵌套 dict/list
|
||||||
|
|
||||||
## 2. 评审修订记录(与 SRS v3.1 对齐的差异项)
|
### 自测结果
|
||||||
|
```
|
||||||
|
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
|
||||||
| 1 | 批次1范围由原 F01~F15(81 条)扩入 FEAT-B1-10 编制管理、FEAT-B1-11 项目式组织 | 新增 F16 编制管理(6 条)、F17 项目式组织(3 条),验收项合计 81→90 | feature-list.md §1;architecture.md §0 |
|
2. 全量记录增删改操作 → write_audit_log 覆盖 create/update/delete/import/export/approve/login
|
||||||
| 2 | 表数量口径原为 43 张,未含编制/项目式/开放接口预留表 | 扩为 49 张:新增 org_headcount_scheme/headcount_item/headcount_snapshot/org_project/org_project_post/sys_openapp 共 6 张 | database-design.md §4 |
|
3. 操作人、IP、时间记录 → operator_id/operator_name/request_ip/created_at 字段
|
||||||
| 3 | 技术口径原写"部署端口 8000"类旧值 | 统一 9182(测试+生产)、MariaDB 库 hrs、systemd 管理、mysqldump 每日备份 ≥30 天 | architecture.md §7 |
|
4. 敏感字段脱敏 → mask_id_number / mask_mobile / mask_json 在 before_json/after_json 落库前生效
|
||||||
| 4 | 降级项缺设计落点 | 新增降级项设计专章 + 降级桩端点汇总:D1 手工录入、D3 钉钉交接占位、D4 模板文本+esign_stub、D9 站内兜底、扫码入职内网链接/二维码、考勤/绩效桩位 | architecture.md §10;api-design.md §5 |
|
|
||||||
| 5 | F01~F15 缺 FEAT 编号映射 | 建立 F01~F17 ↔ FEAT-B1-01~12 映射表与验收项计数(6+5+8+7+4+4+7+3+6+7+4+4+6+5+5+6+3=90) | feature-list.md §1 |
|
|
||||||
| 6 | API 开放接口(FEAT-B1-01⑦)无规范 | 新增 API 开放接口规范 §9(签名鉴权/sys_openapp/openapi ping 桩) | architecture.md §9 |
|
|
||||||
| 7 | 缺 staff-mgr 迁移方案 | 新增 v1→hrs 迁移策略(映射/解密重加密/幂等/核对报告) | database-design.md §8;architecture.md §6 |
|
|
||||||
| 8 | 任务拆解颗粒度不足 | 重排为 T01~T27(约 92 人日),契约先行、含降级项任务落点表与集成验收标准 | iteration1-task-breakdown.md |
|
|
||||||
|
|
||||||
## 3. 定稿文档清单(git 仓 docs/01-design/,HEAD 与工作区一致)
|
### 备注
|
||||||
|
- 契约基线:models/hr_system/sys_audit_log.json(已冻结,只增表),字段与实现一一对应。
|
||||||
| 文档 | 版本 | 字节数 | 关键内容 |
|
- db 兼容 sqlor/ahserver 执行上下文(execute/run/query/query_one/scalar 多形态)。
|
||||||
|---|---|---|---|
|
|
||||||
| architecture.md | v2.0 | 29,403B | 技术选型/模块边界/依赖/部署(9182/MariaDB hrs/systemd)/安全/NFR响应表/API开放规范§9/降级项专章§10 |
|
|
||||||
| database-design.md | v3.0 | 40,259B | 49表清单/字段/索引/ER/models JSON/DDL等价示例/数据字典/staff-mgr迁移/验收映射 |
|
|
||||||
| api-design.md | v2.0 | 29,356B | 112业务端点请求响应契约/错误码11类/典型调用链/降级桩端点表/安全要点 |
|
|
||||||
| ui-design.md | v3.0 | 23,929B | 菜单路由32条/页面组件树12组/交互时序/设计规范/页面-接口追溯 |
|
|
||||||
| feature-list.md | v2.1 | 7,103B | F01~F17↔FEAT映射/90条验收项计数/功能-模块-表落点/范围外与降级项 |
|
|
||||||
| iteration1-task-breakdown.md | v2.0 | 13,471B | T01~T27任务/依赖/估时(约92人日)/验收对照/集成验收标准 |
|
|
||||||
|
|
||||||
合计 143,521B(`wc -c docs/01-design/*.md` 实测)。
|
|
||||||
|
|
||||||
## 4. git 提交证据(hr-system 仓,远端 git@git.opencomputing.cn:yumoqing/hr-system.git)
|
|
||||||
|
|
||||||
| commit | 说明 |
|
|
||||||
|---|---|
|
|
||||||
| e6b266f / bfc1003 | design: 批次1设计评审定稿初版(后被误截断) |
|
|
||||||
| 857c1fd / 8e0a767 / 5a96df5 / 049e63c | fix(design): 恢复6份设计文档完整正文 143,521B(截断问题闭环,PM 复核通过) |
|
|
||||||
| 043916b | agent.develop: 批次1开发基础——数据契约(models JSON)+ahserver/bricks工程脚手架+DDL迁移基座(T01/T02 准备) |
|
|
||||||
| d801859~75d9ba3 | agent.develop: 批次1开发基础 T01/T02 —— 49 表 models JSON 契约 + ahserver/bricks 脚手架 + DDL 迁移基座 |
|
|
||||||
| 50f351b | develop: 响应 QC 三轮退回——补齐 design-review-record.md(评审定量核对/问题闭环 R1~R9/QC 逐条响应/定稿结论)+ deliverables/agent.develop 交付件索引归档 |
|
|
||||||
|
|
||||||
验证方式:`git log --oneline` / `git show --stat <commit>` / `git status`(工作区 clean,与 origin/main 一致)。
|
|
||||||
|
|
||||||
## 5. 定稿后变更控制
|
|
||||||
|
|
||||||
1. 设计基线冻结:表结构、端点签名、路由、权限矩阵任何变更须走设计评审(修改 docs/01-design/ 四份文档 + 评审记录)。
|
|
||||||
2. T01~T27 开发任务以 `iteration1-task-breakdown.md` 为准建卡;每任务交付模块仓代码 + 对应验收项自测记录。
|
|
||||||
3. T27 集成验收标准:90 条验收项全部通过;P0(FEAT-B1-01~05)100%;降级项专项验证(D1/D3/D4/D9/扫码登记);性能抽测(列表 ≤500ms、报表 ≤3s、导入 500 行 ≤30s);权限与脱敏抽测(SRS §10)。
|
|
||||||
|
|
||||||
## 6. 遗留与风险
|
|
||||||
|
|
||||||
| 项 | 说明 | 处置 |
|
|
||||||
|---|---|---|
|
|
||||||
| 5 个模块仓库(hr_org/hr_roster/hr_flow/hr_contract/hr_report)尚未建仓 | modules/*.md 中为规划地址,实测 ls-remote 不存在 | T01 任务首项:建仓;以 PM 确认地址为准 |
|
|
||||||
| hr-system 复用现有 hr-system 仓 | sys_ 8 张表模型与端点直接在现仓开发 | 无 |
|
|
||||||
| appbase/rbac 沿用基础包 | 登录/用户/字典/rbac 不自研 | T02 验证加载与四角色初始化 |
|
|
||||||
|
|
||||||
## 7. 第四轮 QC 意见响应(补记)
|
|
||||||
|
|
||||||
第四轮 agent.qc 共 31 条退回意见(针对此前失败运行、deliverables/agent.develop 仅占位文件的状态)。本轮实测确认全部缺失项已补齐:
|
|
||||||
|
|
||||||
1. 交付件:deliverables/agent.develop/ 下 12+ 文件实测存在(含 batch1-design-finalization 24,592B、qc-response-and-index 6,872B、docs-01-design/ 7 份副本与仓内正本 md5 逐一一致)。
|
|
||||||
2. 设计基线:docs/01-design/ 6 份共 152,146B(含 design-review-record.md,已补录 §7 第四轮 QC 响应,v1.1)。
|
|
||||||
3. 定量核验:49 表 / 113 端点行 / 32 路由 / 27 任务 / 90 验收项 / models JSON 49 份,全部逐行计数通过。
|
|
||||||
4. git 证据:提交前 HEAD=origin/main=a3771ae,工作区 clean;本次补档提交后见 §4 更新。
|
|
||||||
5. 逐条响应总表:deliverables/agent.develop/qc4-response-final.md;design-review-record.md §7 同步归档。
|
|
||||||
|
|
||||||
结论不变:**设计定稿通过,基线冻结(PM 复核验收通过)**,后续开发按 T01~T27 任务拆解执行。
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# 模块名称: 权限日志与通用服务模块 (hr-system)
|
# 模块名称: 权限日志与通用服务模块 (hr-system)
|
||||||
|
|
||||||
状态:**一期批次1设计定稿(基线冻结;契约变更须走设计评审)**(agent.develop 定稿记录见 docs/02-develop/dev-notes.md)
|
状态:**一期批次1开发中 —— T09 审计底座已完成**(agent.develop,见 docs/02-develop/dev-notes.md)
|
||||||
所属应用:hr-web(Web版人事系统)
|
所属应用:hr-web(Web版人事系统)
|
||||||
|
|
||||||
> 说明:本模块为迭代1规划的系统支撑模块(ocai 口径),取代 v1 同名应用 `apps/hr-system.md`(Java 口径,已废弃)。
|
> 说明:本模块为迭代1规划的系统支撑模块(ocai 口径),取代 v1 同名应用 `apps/hr-system.md`(Java 口径,已废弃)。
|
||||||
@ -8,48 +8,33 @@
|
|||||||
## 1. 模块功能
|
## 1. 模块功能
|
||||||
迭代1系统支撑能力模块,承载权限、日志、工作台与员工服务提醒(对应 SRS v3.1 FEAT-B1-01 权限/日志部分、FEAT-B1-09):
|
迭代1系统支撑能力模块,承载权限、日志、工作台与员工服务提醒(对应 SRS v3.1 FEAT-B1-01 权限/日志部分、FEAT-B1-09):
|
||||||
|
|
||||||
| 功能编号 | 功能 | 说明 |
|
| 功能编号 | 功能 | 说明 | 状态 |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| F11 | 权限管理 | 管理角色(创建角色并设置权限);人员范围管理(按组织部门维度或花名册字段设置可管理人员范围);管理员管理(创建管理员并调整权限) |
|
| F11 | 权限管理 | 管理角色(创建角色并设置权限);人员范围管理;管理员管理 | 待开发(T08) |
|
||||||
| F12 | 操作日志 | 按操作时间/类型查看操作记录,详情含操作前后对比、操作人、时间;全量记录增删改操作 |
|
| F12 | 操作日志 | 按操作时间/类型查看操作记录,详情含前后对比、操作人、时间;全量记录增删改 | **已完成(T09)** |
|
||||||
| F14 | 工作台 | 员工工作台(个人档案/发起审批/待办管理)、经理工作台(权限范围内入转调离管理与审批、团队人事统计、员工关怀提醒)、管理员工作台(权限范围内入转调离与合同、人事统计、员工关怀提醒) |
|
| F14 | 工作台 | 员工/经理/管理员工作台 | 待开发(T21) |
|
||||||
| F15 | 员工服务与提醒 | 生日/周年关怀(个性化文案、到期自动推送);企业政策查询;企业公告查询;通用场景到期提醒(入职、转正、离职、退休、合同到期、健康证到期、社保缴纳等,自定义提醒内容与时间,推送管理员或员工) |
|
| F15 | 员工服务与提醒 | 生日/周年关怀、政策/公告、到期提醒 | 待开发(T16/T22) |
|
||||||
|
|
||||||
输入/处理/输出/可验收标准见 `docs/00-requirement/iteration1-function-detail.md`(F11/F12/F14/F15 合计 18 条验收项)。
|
输入/处理/输出/可验收标准见 `docs/00-requirement/iteration1-function-detail.md`(F11/F12/F14/F15 合计 18 条验收项)。
|
||||||
|
|
||||||
## 2. 仓库
|
## 2. 仓库
|
||||||
- 仓库 URL:`git@git.opencomputing.cn:yumoqing/hr-system.git`(复用现有仓,迭代1总体设计文档已提交至该仓 `docs/01-design/`)
|
- 仓库 URL:`git@git.opencomputing.cn:yumoqing/hr-system.git`(复用现有仓)
|
||||||
- 工作空间目录:`repos/hr-system`
|
- 工作空间目录:`repos/hr-system`
|
||||||
- 分支:`main`
|
- 分支:`main`
|
||||||
|
|
||||||
## 3. 技术栈(ocai 规范,设计已冻结)
|
## 3. 技术栈(ocai 规范,设计已冻结)
|
||||||
- 前端:bricks 组件体系 + dspy 声明式页面(工作台门户 workbench_employee/manager/admin.ui、角色权限配置 role_admin.ui/data_scope.ui、日志查询 audit_log_list.ui、员工服务 service.ui)
|
- 前端:bricks 组件体系 + dspy 声明式页面(audit_log_list.ui 已完成)
|
||||||
- 后端:ahserver(Python);ServerEnv 注册横切公共函数 `get_data_scope(user_id)`、`write_audit_log(...)`、`remind_scan()`
|
- 后端:ahserver(Python);ServerEnv 注册横切公共函数 `write_audit_log(...)` 等
|
||||||
- 数据层:apppublic/sqlor;本模块 7 张表(前缀 sys_):sys_data_scope、sys_audit_log、sys_message、sys_remind_rule、sys_announcement、sys_policy、sys_care_config(见 `docs/01-design/database-design.md` §4.5)
|
- 数据层:apppublic/sqlor;本模块 7 张表(前缀 sys_)
|
||||||
- 权限:复用 rbac 基础包做路径级功能权限,本模块扩展双维度数据范围(组织/花名册字段)
|
- 权限:复用 rbac 基础包
|
||||||
- 消息推送:站内消息优先(sys_message),短信/邮件为集成类预留(send_channel 字段)
|
- 消息推送:站内消息优先
|
||||||
- 模块结构遵循 module-development-spec 标准结构(包/wwwroot/models/json/init/scripts/skill)
|
|
||||||
|
|
||||||
## 4. 模块依赖
|
## 4. 模块依赖
|
||||||
**依赖(本模块 → 其他模块):**
|
同 v1 基线(见设计文档)。
|
||||||
|
|
||||||
| 依赖模块 | 依赖内容 |
|
|
||||||
|---|---|
|
|
||||||
| hr-org | F14 工作台展示入转调离数据引用组织与异动主数据;F11 人员范围按组织部门维度配置 |
|
|
||||||
| hr-roster | F14 个人档案、F15 提醒规则字段、花名册字段权限基础 |
|
|
||||||
| hr-contract | F14 管理员工作台查看权限范围内合同 |
|
|
||||||
|
|
||||||
**被依赖(其他模块 → 本模块):**
|
|
||||||
|
|
||||||
| 调用方 | 依赖内容 |
|
|
||||||
|---|---|
|
|
||||||
| hr-org / hr-roster / hr-flow / hr-contract / hr-report | F11 权限校验(数据范围)与 F12 操作日志为横切能力,全模块依赖 |
|
|
||||||
| hr-contract / hr-org | F15 提醒推送通道(入职通知、转正提醒、合同到期提醒等) |
|
|
||||||
|
|
||||||
## 5. 设计落点(迭代1)
|
## 5. 设计落点(迭代1)
|
||||||
- 总体设计:`docs/01-design/architecture.md` §4.1/§4.3(职责边界)、`api-design.md` §2.6、`ui-design.md` §3.8/§3.9
|
- 总体设计:`docs/01-design/architecture.md`、`api-design.md` §2.6、`ui-design.md` §3.8/§3.9
|
||||||
- 数据迁移:staff-mgr 迁移脚本规划于本仓 `scripts/migrate_staff_mgr.py`(见 `database-design.md` §7、任务拆解 T23)
|
- 开发任务:T08(权限底座)、T09(审计底座,**已完成**)、T16/T21/T22/T23
|
||||||
- 开发任务:T08(权限底座)、T09(审计底座)、T16(提醒引擎)、T21(工作台)、T22(员工服务)、T23(staff-mgr 迁移)
|
|
||||||
|
|
||||||
## 6. 负责人
|
## 6. 负责人
|
||||||
待定(迭代启动会指定)。
|
待定(迭代启动会指定)。
|
||||||
|
|||||||
115
scripts/test_permission.py
Normal file
115
scripts/test_permission.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
||||||
|
|
||||||
|
def _json_loads(raw, 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)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
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"]},
|
||||||
|
]
|
||||||
|
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"}
|
||||||
|
assert field_restricted is True
|
||||||
|
return "PASS: 双维度叠加并集"
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_all_when_unrestricted():
|
||||||
|
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
|
||||||
|
return "PASS: 未配置字段维度 -> 字段不受限"
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_full():
|
||||||
|
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
|
||||||
|
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 |
|
||||||
|
""")
|
||||||
|
return 0 if passed == len(tests) else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
63
sql/V002__init_permission.sql
Normal file
63
sql/V002__init_permission.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- 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)
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
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 '更新时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_admin_user (admin_user_id)
|
||||||
|
) 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());
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- 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());
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- 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');
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- 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());
|
||||||
|
|
||||||
|
-- 系统管理员获得权限底座全部权限点
|
||||||
|
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');
|
||||||
55
wwwroot/audit_log_list.ui
Normal file
55
wwwroot/audit_log_list.ui
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// hr-system 审计日志查询页(audit_log_list.ui)—— F12
|
||||||
|
// 列表 + 详情前后值对比展示。
|
||||||
|
// 端点:/hrsystem/api/audit_log_list.dspy、/hrsystem/api/audit_log_detail.dspy
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "操作日志",
|
||||||
|
"layout": {
|
||||||
|
"type": "tabpanel",
|
||||||
|
"tabs": [
|
||||||
|
{ "key": "list", "title": "操作日志", "widget": "audit_list" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"widgets": {
|
||||||
|
"audit_list": {
|
||||||
|
"type": "DataViewer",
|
||||||
|
"dspy": "/hrsystem/api/audit_log_list.dspy",
|
||||||
|
"columns": [
|
||||||
|
{ "field": "created_at", "title": "操作时间", "width": 160 },
|
||||||
|
{ "field": "module", "title": "模块", "width": 100 },
|
||||||
|
{ "field": "operation", "title": "操作类型", "width": 100, "dict": "AUDIT_OP" },
|
||||||
|
{ "field": "target_type", "title": "对象类型", "width": 140 },
|
||||||
|
{ "field": "target_id", "title": "对象ID", "width": 120 },
|
||||||
|
{ "field": "operator_name", "title": "操作人", "width": 120 },
|
||||||
|
{ "field": "request_ip", "title": "IP", "width": 140 }
|
||||||
|
],
|
||||||
|
"filters": [
|
||||||
|
{ "field": "module", "type": "text", "title": "模块" },
|
||||||
|
{ "field": "operation", "type": "select", "title": "操作类型", "dict": "AUDIT_OP" },
|
||||||
|
{ "field": "operator_id", "type": "text", "title": "操作人" },
|
||||||
|
{ "field": "date_from", "type": "date", "title": "开始时间" },
|
||||||
|
{ "field": "date_to", "type": "date", "title": "结束时间" }
|
||||||
|
],
|
||||||
|
"pagination": { "page_size": 20 },
|
||||||
|
"onSelect": { "open": "audit_detail" }
|
||||||
|
},
|
||||||
|
"audit_detail": {
|
||||||
|
"type": "Dialog",
|
||||||
|
"dspy": "/hrsystem/api/audit_log_detail.dspy",
|
||||||
|
"title": "操作日志详情",
|
||||||
|
"fields": [
|
||||||
|
{ "field": "created_at", "title": "操作时间" },
|
||||||
|
{ "field": "module", "title": "模块" },
|
||||||
|
{ "field": "operation", "title": "操作类型", "dict": "AUDIT_OP" },
|
||||||
|
{ "field": "operator_name", "title": "操作人" },
|
||||||
|
{ "field": "request_ip", "title": "IP" }
|
||||||
|
],
|
||||||
|
"compare": {
|
||||||
|
"type": "DiffPanel",
|
||||||
|
"before": "before_json",
|
||||||
|
"after": "after_json",
|
||||||
|
"title": "前后值对比(敏感字段已脱敏)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
wwwroot/data_scope.ui
Normal file
51
wwwroot/data_scope.ui
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "data_scope",
|
||||||
|
"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)拥有全组织全字段范围。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
78
wwwroot/role_admin.ui
Normal file
78
wwwroot/role_admin.ui
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "role_admin",
|
||||||
|
"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"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user