Files
ai-worker-platform/enterprise.py
T
hz4th_coder 7c4cbf8994 V3.4 负责人验收制+邮件修复+事件面板+真实网页交付
- 项目级验收:全部任务完成后进入「待验收」,邮件通知负责人,验收通过才算完成;公开验收链接 /review/<token> 免登录一键通过/打回(打回必填原因)
- 打回自动返工:自动生成含负责人意见的返工任务,AI主管立即重做并再次提交验收;平台内也可验收
- 真实网页交付:任务HTML产出自动落盘工作目录(剥离围栏/前置叙述),Demo展示真实页面;返工产出覆盖入口页;老项目已回填
- 事件记录面板:合并AI主管动态+任务日志+交付记录,可展开/收起(记忆状态),任务事件可点击定位
- 邮件通道修复:补 Date/Message-ID 头(amavisd 拒收 invalid header section 根因),打包附件/通知恢复送达;notify.py 改标准 MIME
- 规划重试:WBS 拆解失败自动重试3次,仍失败邮件通知负责人
- 项目状态新增 review(待验收),全端展示
2026-08-14 13:33:59 +08:00

664 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
V2 企业版能力
- 用户体系 + RBACadmin / member / auditor
- SSOOIDC 授权码模式 + LDAP 绑定(可选依赖),本地口令兜底
- 审计日志:全 API 关键操作留痕(actor/action/target/ip
- 合规:数据导出(全量 JSON / 审计 CSV)、保留期清理、PII 掩码
"""
import csv
import io
import json
import time
import uuid
import db
# ---------------------------------------------------------------------------
# 审计
# ---------------------------------------------------------------------------
PII_PATTERNS = [
(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', '<email>'),
(r'\b1[3-9]\d{9}\b', '<phone>'),
(r'\b\d{17}[\dXx]\b', '<idcard>'),
]
def mask_pii(text):
"""合规:敏感信息掩码"""
if not text:
return text
import re
for pat, rep in PII_PATTERNS:
text = re.sub(pat, rep, text)
return text
def audit(actor, action, target='', detail='', ip='', user_agent=''):
"""写审计日志(失败不影响主流程)"""
try:
if db.get_setting('compliance_mask_pii', '0') == '1':
detail = mask_pii(detail)
db.w(
'INSERT INTO audit_logs (actor, action, target, detail, ip, user_agent, created_at) '
'VALUES (?,?,?,?,?,?,?)',
(str(actor)[:100], str(action)[:100], str(target)[:200], str(detail)[:2000],
str(ip)[:64], str(user_agent)[:200], db.now()))
except Exception:
pass
def current_actor():
"""从请求上下文推断操作者(由 app 注入 request-local 变量)"""
import flask
try:
req = flask.request
actor = getattr(flask.g, 'auth_actor', None)
if actor:
return actor
hdr = req.headers.get('Authorization', '')
if hdr.startswith('Bearer '):
r = db.q('SELECT name FROM api_tokens WHERE token=?', (hdr[7:].strip(),), one=True)
return f'token:{r["name"]}' if r else 'token:?'
username = flask.session.get('username')
if username:
return username
return 'anonymous'
except Exception:
return 'anonymous'
def audit_auto(action, target='', detail='', save_body_keys=None):
"""装饰器版自动审计:包装 flask 路由"""
import flask
import functools
def deco(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
resp = fn(*args, **kwargs)
try:
detail_text = ''
if save_body_keys and flask.request.method in ('POST', 'PUT'):
try:
body = flask.request.get_json(silent=True) or {}
detail_text = ' '.join(f'{k}={body.get(k)}' for k in save_body_keys if k in body)
except Exception:
pass
audit(current_actor(), action, target or (flask.request.path or ''),
detail_text, flask.request.remote_addr or '',
flask.request.headers.get('User-Agent', ''))
except Exception:
pass
return resp
return wrapper
return deco
# ---------------------------------------------------------------------------
# 用户 / RBAC
# ---------------------------------------------------------------------------
def get_user(username):
return db.q('SELECT * FROM users WHERE username=?', (username,), one=True)
def role_of(username):
u = get_user(username)
return u['role'] if u else 'anonymous'
def is_admin(username):
return role_of(username) == 'admin'
def create_local_user(username, password, display_name='', role='member', email=''):
if get_user(username):
return None, '用户已存在'
if not email:
return None, '必填:用户邮箱(用于项目送达/通知)'
uid = db.w(
'INSERT INTO users (username, password_hash, display_name, email, role, source, status, created_at) '
'VALUES (?,?,?,?,?,?,?,?)',
(username, db.hash_password(password), display_name or username, email, role, 'local', 'active', db.now()))
return uid, None
def verify_local(username, password):
u = get_user(username)
if not u or u['source'] != 'local' or u['status'] != 'active':
return None
if db.verify_password(password, u['password_hash']):
db.w('UPDATE users SET last_login_at=? WHERE id=?', (db.now(), u['id']))
return u
return None
# ---------------------------------------------------------------------------
# SSOOIDC(授权码)+ LDAP(可选)
# ---------------------------------------------------------------------------
def sso_config():
cfg = {}
for k in ('oidc_enabled', 'oidc_name', 'oidc_discovery_url', 'oidc_client_id',
'oidc_client_secret', 'oidc_redirect_uri', 'oidc_scope', 'oidc_admin_group',
'ldap_enabled', 'ldap_url', 'ldap_base_dn', 'ldap_bind_dn', 'ldap_bind_password',
'ldap_user_filter', 'sso_auto_provision'):
cfg[k] = db.get_setting(k, '')
return cfg
def save_sso_config(data):
keys = list(data.keys())
for k in keys:
if k in ('oidc_client_secret', 'ldap_bind_password') and not data[k]:
continue # 留空不覆盖已保存的密钥
db.set_setting(k, str(data[k]))
def oidc_discovery():
"""读取 OIDC discovery 文档,返回端点字典"""
import requests
url = sso_config().get('oidc_discovery_url', '').strip().rstrip('/')
if not url:
raise ValueError('未配置 OIDC discovery URL')
r = requests.get(url, timeout=15)
if r.status_code != 200:
raise ValueError(f'Discovery 请求失败({r.status_code})')
return r.json()
def oidc_authorize_url(state):
"""生成授权跳转 URL"""
import urllib.parse
cfg = sso_config()
disc = oidc_discovery()
params = {
'response_type': 'code',
'client_id': cfg['oidc_client_id'],
'redirect_uri': cfg['oidc_redirect_uri'],
'scope': cfg.get('oidc_scope') or 'openid profile email',
'state': state,
}
return disc.get('authorization_endpoint') + '?' + urllib.parse.urlencode(params)
def oidc_exchange(code):
"""用授权码换 token + 用户信息"""
import requests
cfg = sso_config()
disc = oidc_discovery()
tok = requests.post(disc.get('token_endpoint'), data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': cfg['oidc_redirect_uri'],
'client_id': cfg['oidc_client_id'],
'client_secret': cfg['oidc_client_secret'],
}, timeout=15)
if tok.status_code != 200:
raise ValueError(f'Token 交换失败({tok.status_code}): {tok.text[:200]}')
token_data = tok.json()
id_token = token_data.get('id_token', '')
userinfo = {}
# 优先 userinfo 端点
if token_data.get('access_token'):
ui = requests.get(disc.get('userinfo_endpoint'), headers={
'Authorization': f"Bearer {token_data['access_token']}"}, timeout=15)
if ui.status_code == 200:
userinfo = ui.json()
# 解析 id_token payload 兜底
if not userinfo and id_token:
import base64
try:
payload = id_token.split('.')[1]
payload += '=' * (-len(payload) % 4)
userinfo = json.loads(base64.urlsafe_b64decode(payload))
except Exception:
pass
return userinfo
def sso_login(userinfo):
"""SSO 登录回调:查找或自动开通用户"""
cfg = sso_config()
username = userinfo.get('preferred_username') or userinfo.get('email') or userinfo.get('sub') or ''
email = userinfo.get('email', '')
display = userinfo.get('name') or userinfo.get('display_name') or username
groups = userinfo.get('groups') or userinfo.get('roles') or []
if not username:
return None, '无法从 SSO 响应中解析用户名'
u = get_user(username)
if not u:
if cfg.get('sso_auto_provision') != '1':
return None, '用户未开通(自动开通未启用),请联系管理员'
role = 'admin' if cfg.get('oidc_admin_group') and cfg['oidc_admin_group'] in groups else 'member'
db.w(
'INSERT INTO users (username, password_hash, display_name, email, role, source, status, created_at) '
'VALUES (?,?,?,?,?,?,?,?)',
(username, '', display, email or f'{username}@sso.local', role, 'oidc', 'active', db.now()))
u = get_user(username)
elif u['status'] != 'active':
return None, '账号已停用'
elif u['source'] != 'oidc':
return None, f'用户名 {username} 已被本地账号占用'
# SSO 邮箱变更时同步(送达通知用最新邮箱)
if email and u.get('email') != email:
db.w('UPDATE users SET email=? WHERE id=?', (email, u['id']))
db.w('UPDATE users SET last_login_at=? WHERE id=?', (db.now(), u['id']))
return u, None
def ldap_authenticate(username, password):
"""LDAP 绑定认证(依赖 ldap3,未安装时返回 None"""
try:
from ldap3 import Server, Connection, ALL
except ImportError:
return None, '未安装 ldap3,无法使用 LDAP SSO'
cfg = sso_config()
try:
server = Server(cfg['ldap_url'], get_info=ALL)
conn = Connection(server, user=cfg['ldap_bind_dn'], password=cfg['ldap_bind_password'],
auto_bind=True)
user_filter = cfg.get('ldap_user_filter') or '(uid={username})'
conn.search(cfg['ldap_base_dn'], user_filter.format(username=username), attributes=['cn', 'mail', 'displayName'])
if not conn.entries:
return None, 'LDAP 中未找到该用户'
entry = conn.entries[0]
user_dn = entry.entry_dn
conn.unbind()
conn2 = Connection(server, user=user_dn, password=password, auto_bind=True)
conn2.unbind()
return {'username': username,
'display': str(entry.displayName.value) if hasattr(entry, 'displayName') else username,
'email': str(entry.mail.value) if hasattr(entry, 'mail') else ''}, None
except Exception as e:
return None, f'LDAP 认证失败: {e}'
def ldap_login(username, password):
info, err = ldap_authenticate(username, password)
if err:
return None, err
u = get_user(username)
if not u:
if sso_config().get('sso_auto_provision') != '1':
return None, '用户未开通,请联系管理员'
db.w(
'INSERT INTO users (username, password_hash, display_name, email, role, source, status, created_at) '
'VALUES (?,?,?,?,?,?,?,?)',
(username, '', info.get('display') or username, info.get('email') or f'{username}@ldap.local',
'member', 'ldap', 'active', db.now()))
u = get_user(username)
elif u['status'] != 'active':
return None, '账号已停用'
if info.get('email') and u.get('email') != info['email']:
db.w('UPDATE users SET email=? WHERE id=?', (info['email'], u['id']))
db.w('UPDATE users SET last_login_at=? WHERE id=?', (db.now(), u['id']))
return u, None
# ---------------------------------------------------------------------------
# 合规:导出 / 保留期
# ---------------------------------------------------------------------------
def export_all():
"""全量数据导出(JSON"""
tables = ['projects', 'workers', 'tasks', 'task_logs', 'cost_records', 'documents',
'agent_runs', 'agent_steps', 'eval_datasets', 'eval_cases', 'eval_runs',
'eval_results', 'templates', 'users', 'audit_logs',
'project_deliverables', 'user_projects', 'user_workers']
out = {'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'),
'platform': 'ai-worker-platform', 'version': 'v3.4.0'}
for t in tables:
try:
out[t] = db.q(f'SELECT * FROM {t}')
except Exception:
out[t] = []
return out
def export_audit_csv():
"""审计日志导出 CSV"""
rows = db.q('SELECT * FROM audit_logs ORDER BY id DESC LIMIT 10000')
buf = io.StringIO()
w = csv.writer(buf)
w.writerow(['ID', '时间', '操作者', '动作', '目标', '详情', 'IP', 'UA'])
for r in rows:
w.writerow([r['id'], time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(r['created_at'])),
r['actor'], r['action'], r['target'], r['detail'], r['ip'], r['user_agent']])
return buf.getvalue()
def apply_retention():
"""合规:按保留期清理审计日志 / 协作运行 / 评估结果(每天可跑一次)"""
days = int(db.get_setting('compliance_retention_days', '0') or 0)
if days <= 0:
return {'cleaned': 0, 'note': '未配置保留期(0=永久保留)'}
cutoff = db.now() - days * 86400
cleaned = 0
for table in ('audit_logs', 'agent_steps', 'agent_runs', 'eval_results', 'eval_runs'):
try:
cur = db.w(f'DELETE FROM {table} WHERE created_at<?', (cutoff,))
cleaned += cur or 0
except Exception:
pass
return {'cleaned': cleaned, 'retention_days': days}
def generate_consent_token():
"""生成数据使用同意记录 token(审计用途)"""
tok = uuid.uuid4().hex[:12]
audit('system', 'compliance.consent', '数据使用同意', f'consent_token={tok}')
return tok
# ---------------------------------------------------------------------------
# V3 授权:用户 ↔ 项目 / 用户 ↔ Worker(精准权限)
# ---------------------------------------------------------------------------
PERM_LEVEL = {'view': 0, 'use': 1, 'manage': 2, 'admin': 3}
PROJECT_PERMS = ('view', 'manage', 'admin')
WORKER_PERMS = ('view', 'use', 'manage')
def perm_ok(have, need):
"""have 权限是否满足 need 权限(None 视为无权限)"""
if not have:
return False
return PERM_LEVEL.get(have, -1) >= PERM_LEVEL.get(need, 99)
def user_project_perm(user_id, project_id):
"""用户在项目上的权限:None / view / manage / admin"""
r = db.q('SELECT perm FROM user_projects WHERE user_id=? AND project_id=?',
(user_id, project_id), one=True)
return r['perm'] if r else None
def user_worker_perm(user_id, worker_id):
"""用户在 Worker 上的权限:None / view / use / manage(直接授权 权限组授权,取高)"""
r = db.q('SELECT perm FROM user_workers WHERE user_id=? AND worker_id=?',
(user_id, worker_id), one=True)
direct = r['perm'] if r else None
grouped = grouped_worker_perm(user_id, worker_id)
if direct is None:
return grouped
if grouped is None:
return direct
return direct if PERM_LEVEL.get(direct, 0) >= PERM_LEVEL.get(grouped, 0) else grouped
def visible_project_ids(user_id):
"""用户可见的项目 id 列表(admin/auditor 返回 None 表示全部)"""
u = db.q('SELECT role FROM users WHERE id=?', (user_id,), one=True)
if u and u['role'] in ('admin', 'auditor'):
return None
rows = db.q('SELECT project_id FROM user_projects WHERE user_id=?', (user_id,))
return [r['project_id'] for r in rows]
def visible_worker_ids(user_id):
"""用户可见的 Worker id 列表 = 直接授权 权限组授权(admin/auditor 返回 None 表示全部)"""
u = db.q('SELECT role FROM users WHERE id=?', (user_id,), one=True)
if u and u['role'] in ('admin', 'auditor'):
return None
rows = db.q('SELECT worker_id FROM user_workers WHERE user_id=?', (user_id,))
ids = {r['worker_id'] for r in rows}
ids |= set(grouped_worker_ids(user_id))
return sorted(ids)
def set_user_grants(user_id, projects=None, workers=None, roles=None, worker_groups=None):
"""批量覆盖用户授权。projects=[{project_id, perm}], workers=[{worker_id, perm}],
roles=[role_id], worker_groups=[group_id]。perm 传空/None 表示收回该授权。"""
out = {'projects': 0, 'workers': 0, 'roles': 0, 'worker_groups': 0}
if projects is not None:
db.w('DELETE FROM user_projects WHERE user_id=?', (user_id,))
for g in projects:
perm = (g.get('perm') or '').strip()
if perm not in PROJECT_PERMS:
continue
pid = int(g.get('project_id') or 0)
if not db.q('SELECT id FROM projects WHERE id=?', (pid,), one=True):
continue
db.w('INSERT INTO user_projects (user_id, project_id, perm, created_at) VALUES (?,?,?,?)',
(user_id, pid, perm, db.now()))
out['projects'] += 1
if workers is not None:
db.w('DELETE FROM user_workers WHERE user_id=?', (user_id,))
for g in workers:
perm = (g.get('perm') or '').strip()
if perm not in WORKER_PERMS:
continue
wid = int(g.get('worker_id') or 0)
if not db.q('SELECT id FROM workers WHERE id=?', (wid,), one=True):
continue
db.w('INSERT INTO user_workers (user_id, worker_id, perm, created_at) VALUES (?,?,?,?)',
(user_id, wid, perm, db.now()))
out['workers'] += 1
if roles is not None:
set_user_roles(user_id, roles)
out['roles'] = len(roles or [])
if worker_groups is not None:
db.w('DELETE FROM user_worker_groups WHERE user_id=?', (user_id,))
for gid in (worker_groups or []):
if db.q('SELECT id FROM worker_perm_groups WHERE id=?', (int(gid),), one=True):
db.w('INSERT INTO user_worker_groups (user_id, group_id, created_at) VALUES (?,?,?)',
(user_id, int(gid), db.now()))
out['worker_groups'] += 1
return out
def user_grants(user_id):
"""用户现有授权 + 全部可选项目/Worker/角色/权限组,供管理界面展示"""
projects = db.q('SELECT p.id, p.name, p.status FROM projects p ORDER BY p.id DESC')
workers = db.q('SELECT id, name, provider, model, status FROM workers ORDER BY id DESC')
for p in projects:
p['perm'] = user_project_perm(user_id, p['id'])
for w in workers:
w['perm'] = user_worker_perm(user_id, w['id'])
roles = db.q('SELECT * FROM roles ORDER BY is_builtin DESC, id')
for r in roles:
r['perms'] = sorted(role_perms(r))
r['bound'] = r['id'] in user_role_ids(user_id)
groups = db.q('SELECT * FROM worker_perm_groups ORDER BY id')
bound_gids = {g['group_id'] for g in db.q('SELECT group_id FROM user_worker_groups WHERE user_id=?',
(user_id,))}
for g in groups:
g['member_count'] = db.q('SELECT COUNT(*) c FROM worker_perm_group_members WHERE group_id=?',
(g['id'],))[0]['c']
g['bound'] = g['id'] in bound_gids
return {'projects': projects, 'workers': workers, 'roles': roles, 'worker_groups': groups}
# ---------------------------------------------------------------------------
# V3.1 自定义角色(权限功能点)
# ---------------------------------------------------------------------------
PERM_POINTS = [
{'id': 'dashboard.view', 'name': '仪表盘', 'group': '总览'},
{'id': 'project.view', 'name': '项目查看', 'group': '项目'},
{'id': 'project.create', 'name': '创建项目', 'group': '项目'},
{'id': 'project.deliver', 'name': '交付操作(上传/部署/打包/发送/完成)', 'group': '项目'},
{'id': 'worker.view', 'name': 'Worker 查看', 'group': 'Worker'},
{'id': 'worker.create', 'name': 'Worker 创建', 'group': 'Worker'},
{'id': 'worker.manage', 'name': 'Worker 编辑/删除', 'group': 'Worker'},
{'id': 'agent.view', 'name': '多Agent协作-查看', 'group': '协作评估'},
{'id': 'agent.run', 'name': '多Agent协作-运行', 'group': '协作评估'},
{'id': 'eval.view', 'name': '自动评估-查看', 'group': '协作评估'},
{'id': 'eval.run', 'name': '自动评估-运行', 'group': '协作评估'},
{'id': 'template.view', 'name': '模板市场-查看/应用', 'group': '模板'},
{'id': 'template.manage', 'name': '模板市场-管理', 'group': '模板'},
{'id': 'report.view', 'name': '成本报表', 'group': '报表日志'},
{'id': 'log.view', 'name': '运行日志', 'group': '报表日志'},
{'id': 'alert.view', 'name': '告警中心', 'group': '报表日志'},
{'id': 'api.manage', 'name': '开放API Token 管理', 'group': '平台管理'},
{'id': 'setting.manage', 'name': '通知设置', 'group': '平台管理'},
{'id': 'enterprise.manage', 'name': '企业版管理(用户/角色/授权/SSO/审计/合规)', 'group': '平台管理'},
]
ALL_POINTS = {p['id'] for p in PERM_POINTS}
def role_perms(role):
"""角色权限点集合(admin 的 ['*'] 展开为全部)"""
try:
perms = json.loads(role.get('perms') or '[]')
except Exception:
perms = []
if '*' in perms:
return set(ALL_POINTS)
return set(perms) & ALL_POINTS
def user_role_ids(user_id):
return [r['role_id'] for r in db.q('SELECT role_id FROM user_roles WHERE user_id=?', (user_id,))]
def user_perms(user_id):
"""用户有效权限点集合 = 内置角色基础(roles 表,可编辑)∪ 自定义角色并集;admin 返回全部"""
u = db.q('SELECT username, role FROM users WHERE id=?', (user_id,), one=True)
if not u:
return set()
# 内置角色基础:从 roles 表读取(管理员可编辑内置角色权限点)
base_role = db.q('SELECT * FROM roles WHERE name=?', (u['role'],), one=True)
s = role_perms(base_role) if base_role else set()
for rid in user_role_ids(user_id):
r = db.q('SELECT * FROM roles WHERE id=?', (rid,), one=True)
if r:
s |= role_perms(r)
return s
def has_perm(user_id, point):
return point in user_perms(user_id)
def list_roles():
rows = db.q('SELECT * FROM roles ORDER BY is_builtin DESC, id')
for r in rows:
r['perms'] = sorted(role_perms(r))
r['user_count'] = db.q('SELECT COUNT(*) c FROM user_roles WHERE role_id=?', (r['id'],))[0]['c']
return rows
def create_role(name, description='', perms=None):
name = (name or '').strip()
if not name:
return None, '角色名不能为空'
if db.q('SELECT id FROM roles WHERE name=?', (name,), one=True):
return None, '角色已存在'
rid = db.w('INSERT INTO roles (name, description, perms, is_builtin, created_at, updated_at) '
'VALUES (?,?,?,0,?,?)',
(name, description, json.dumps(sorted(set(perms or []) & ALL_POINTS)), db.now(), db.now()))
return rid, None
def update_role(role_id, data):
role = db.q('SELECT * FROM roles WHERE id=?', (role_id,), one=True)
if not role:
return None, '角色不存在'
if 'name' in data and (data['name'] or '').strip():
nm = data['name'].strip()
dup = db.q('SELECT id FROM roles WHERE name=? AND id!=?', (nm, role_id), one=True)
if dup:
return None, '角色名已存在'
db.w('UPDATE roles SET name=? WHERE id=?', (nm, role_id))
if 'description' in data:
db.w('UPDATE roles SET description=? WHERE id=?', (data['description'], role_id))
if 'perms' in data:
db.w('UPDATE roles SET perms=? WHERE id=?',
(json.dumps(sorted(set(data['perms'] or []) & ALL_POINTS)), role_id))
db.w('UPDATE roles SET updated_at=? WHERE id=?', (db.now(), role_id))
return role_id, None
def delete_role(role_id):
role = db.q('SELECT * FROM roles WHERE id=?', (role_id,), one=True)
if not role:
return None, '角色不存在'
if role['is_builtin']:
return None, '内置角色不可删除(可编辑权限点)'
db.w('DELETE FROM user_roles WHERE role_id=?', (role_id,))
db.w('DELETE FROM roles WHERE id=?', (role_id,))
return role_id, None
def set_user_roles(user_id, role_ids):
"""覆盖设置用户绑定的自定义角色"""
db.w('DELETE FROM user_roles WHERE user_id=?', (user_id,))
for rid in (role_ids or []):
if db.q('SELECT id FROM roles WHERE id=?', (rid,), one=True):
db.w('INSERT INTO user_roles (user_id, role_id, created_at) VALUES (?,?,?)',
(user_id, rid, db.now()))
# ---------------------------------------------------------------------------
# V3.1 Worker 权限组(批量授权)
# ---------------------------------------------------------------------------
def list_worker_groups():
rows = db.q('SELECT * FROM worker_perm_groups ORDER BY id')
for g in rows:
g['members'] = db.q('SELECT w.id AS worker_id, w.name AS worker_name, m.perm '
'FROM worker_perm_group_members m JOIN workers w ON w.id=m.worker_id '
'WHERE m.group_id=? ORDER BY m.id', (g['id'],))
g['users'] = db.q('SELECT u.id AS user_id, u.username, u.display_name '
'FROM user_worker_groups ug JOIN users u ON u.id=ug.user_id '
'WHERE ug.group_id=? ORDER BY u.id', (g['id'],))
return rows
def save_worker_group(group_id, data):
"""新建/更新 Worker 权限组:members=[{worker_id, perm}]users=[user_id]"""
name = (data.get('name') or '').strip()
if not name:
return None, '组名不能为空'
if group_id:
g = db.q('SELECT * FROM worker_perm_groups WHERE id=?', (group_id,), one=True)
if not g:
return None, '权限组不存在'
db.w('UPDATE worker_perm_groups SET name=?, description=?, updated_at=? WHERE id=?',
(name, data.get('description', ''), db.now(), group_id))
else:
group_id = db.w('INSERT INTO worker_perm_groups (name, description, created_at, updated_at) '
'VALUES (?,?,?,?)', (name, data.get('description', ''), db.now(), db.now()))
# 成员
db.w('DELETE FROM worker_perm_group_members WHERE group_id=?', (group_id,))
for m in (data.get('members') or []):
wid = int(m.get('worker_id') or 0)
perm = (m.get('perm') or 'view').strip()
if perm not in WORKER_PERMS:
perm = 'view'
if db.q('SELECT id FROM workers WHERE id=?', (wid,), one=True):
db.w('INSERT INTO worker_perm_group_members (group_id, worker_id, perm, created_at) '
'VALUES (?,?,?,?)', (group_id, wid, perm, db.now()))
# 授权用户
db.w('DELETE FROM user_worker_groups WHERE group_id=?', (group_id,))
for uid in (data.get('users') or []):
if db.q('SELECT id FROM users WHERE id=?', (int(uid),), one=True):
db.w('INSERT INTO user_worker_groups (user_id, group_id, created_at) VALUES (?,?,?)',
(int(uid), group_id, db.now()))
return group_id, None
def delete_worker_group(group_id):
db.w('DELETE FROM worker_perm_group_members WHERE group_id=?', (group_id,))
db.w('DELETE FROM user_worker_groups WHERE group_id=?', (group_id,))
db.w('DELETE FROM worker_perm_groups WHERE id=?', (group_id,))
return group_id, None
def grouped_worker_perm(user_id, worker_id):
"""用户通过权限组获得的 Worker 权限(取最高)"""
rows = db.q(
'SELECT m.perm FROM worker_perm_group_members m '
'JOIN user_worker_groups ug ON ug.group_id=m.group_id '
'WHERE ug.user_id=? AND m.worker_id=?', (user_id, worker_id))
best = None
for r in rows:
if best is None or PERM_LEVEL.get(r['perm'], 0) > PERM_LEVEL.get(best, 0):
best = r['perm']
return best
def grouped_worker_ids(user_id):
"""用户通过权限组获得的 worker_id 列表(含组内权限)"""
rows = db.q(
'SELECT DISTINCT m.worker_id FROM worker_perm_group_members m '
'JOIN user_worker_groups ug ON ug.group_id=m.group_id '
'WHERE ug.user_id=?', (user_id,))
return [r['worker_id'] for r in rows]