diff --git a/README.md b/README.md index 2a1d217..a42a4a1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > 以「项目」为中心、以「AI Worker」为执行单元的项目管理平台。 > 把大模型团队变成一支可指挥、可审计、可控成本的"虚拟团队"。 -**当前版本:V3.1**(自定义角色 / Worker 权限组 / 交付体系 / 精准权限 / 多 Agent 协作 / 企业版) +**当前版本:V3.2**(送达者=用户 / 用户邮箱必填 / 自定义角色 / Worker 权限组 / 交付体系) --- @@ -52,12 +52,19 @@ ### 📦 交付体系(V3 新增) | 能力 | 说明 | |---|---| -| 📬 送达者邮箱 | 新建项目**必填**送达者(人)邮箱;项目**完成**或遇到**无法绕开的难关**时自动邮件及时通知 | +| 📬 送达者=用户 | 新建项目必选**送达者用户**(平台用户,含 admin);通知实时发到该用户**最新邮箱**,用户改邮箱自动跟随,项目进行中可随时更换送达者 | | 🗂️ 项目工作目录 | 每个项目独立工作目录 `data/workspace/project_/`,中间产物/交付物分类存放,互不污染;支持网页上传/下载/删除 | | 🌐 网页交付物 | 一键部署到 `data/demo//`,经 `/demo//` **免登录公开访问**,送达者直接打开链接查看 | | 🗜️ 打包交付 | 工作目录一键 zip 打包(`data/packages/`),随邮件附件发送给送达者 | | ✉️ 手动通知 | 难关说明/进展可随时手动邮件通知送达者;交付全流程留痕(交付记录) | +### 👥 用户体系(V3.2) +| 能力 | 说明 | +|---|---| +| 📧 邮箱必填 | 所有用户(含 admin)必须有邮箱,新建/编辑用户强制校验;创建项目选送达者时列出用户供选择 | +| 🔄 邮箱变更跟随 | 用户改邮箱 → 其送达的所有项目通知自动发到新邮箱(实时读取,无需改项目) | +| 🔀 送达者可变 | 项目进行中可在编辑项目中更换送达者用户,旧数据按邮箱自动回填迁移 | + ### 🔑 精准权限(V3 新增) | 能力 | 说明 | |---|---| @@ -160,6 +167,10 @@ ai-worker-platform/ - `POST /api/projects//complete` 完成项目并交付 | `POST .../notify_deliverer` 手动通知送达者 | `GET /api/projects//deliverables` 交付记录 - `GET /demo//` 公开 Demo 地址(送达者免登录访问) +用户与送达者(V3.2): +- `GET /api/users` 轻量用户列表(登录即可,选送达者用)| `GET/POST /api/enterprise/users` 完整用户管理(邮箱必填) +- `PUT /api/enterprise/users/` 改邮箱后项目送达自动跟随;项目 `deliver_user_id` 绑定送达者用户 + ## 路线图 - **V2.1**:Temporal 持久执行、多 Agent 协作接入项目任务、eval 回归对比视图 diff --git a/app.py b/app.py index a7f2412..31f8dfa 100644 --- a/app.py +++ b/app.py @@ -139,7 +139,8 @@ def me(): row = enterprise.get_user(uname) uid = row['id'] if row else None u = {'username': uname, 'role': session.get('role') or (row['role'] if row else 'admin'), - 'display_name': row['display_name'] if row else uname} + 'display_name': row['display_name'] if row else uname, + 'email': row['email'] if row else ''} if uid: perms = enterprise.user_perms(uid) u['perms'] = sorted(perms) @@ -229,7 +230,7 @@ def require_admin(fn): @app.route('/api/health') def health(): - return jsonify({'ok': True, 'service': 'ai-worker-platform', 'version': 'v3.1.0'}) + return jsonify({'ok': True, 'service': 'ai-worker-platform', 'version': 'v3.2.0'}) # --------------------------------------------------------------------------- @@ -331,6 +332,18 @@ def _check_task_perm(tid, need='view'): return None +def _attach_deliver_user(project): + """项目附带送达者用户信息(用户名/邮箱实时取)""" + duid = project.get('deliver_user_id') + project['deliver_user'] = None + if duid: + u = db.q('SELECT id, username, display_name, email FROM users WHERE id=?', (duid,), one=True) + if u: + project['deliver_user'] = dict(u) + project['deliver_email'] = u['email'] or project.get('deliver_email', '') + return project + + # --------------------------------------------------------------------------- # 项目 # --------------------------------------------------------------------------- @@ -342,18 +355,22 @@ def projects(): if err: return err d = request.get_json(force=True) - deliver_email = (d.get('deliver_email') or '').strip() - if not deliver_email: - return jsonify({'ok': False, 'error': '必填:项目送达者(人)邮箱,用于项目完成/难关通知'}), 400 - if '@' not in deliver_email: - return jsonify({'ok': False, 'error': '送达者邮箱格式不正确'}), 400 + # 送达者:必须选择系统用户(绑定 user_id,邮箱实时取) + duid = d.get('deliver_user_id') + if not duid: + return jsonify({'ok': False, 'error': '必填:选择项目送达者用户(项目完成/难关时通知)'}), 400 + du = db.q('SELECT id, email FROM users WHERE id=?', (int(duid),), one=True) + if not du: + return jsonify({'ok': False, 'error': '送达者用户不存在'}), 400 + if not (du.get('email') or '').strip(): + return jsonify({'ok': False, 'error': f'送达者用户「{d.get("deliver_username", "")}」未设置邮箱,请先在用户管理中补全'}), 400 pid = db.w( 'INSERT INTO projects (name, description, objective, acceptance_criteria, ' - 'status, budget_limit, deliver_email, deliver_type, deliver_note, workspace_dir, ' - 'created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', + 'status, budget_limit, deliver_user_id, deliver_email, deliver_type, deliver_note, workspace_dir, ' + 'created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)', (d.get('name', '').strip(), d.get('description', ''), d.get('objective', ''), d.get('acceptance_criteria', ''), d.get('status', 'active'), - float(d.get('budget_limit') or 0), deliver_email, + float(d.get('budget_limit') or 0), int(duid), du['email'], d.get('deliver_type', 'web'), d.get('deliver_note', ''), '', db.now(), db.now())) # 工作目录名依赖自增 id:先拿到 id 再补写 @@ -365,7 +382,7 @@ def projects(): db.w('INSERT INTO user_projects (user_id, project_id, perm, created_at) VALUES (?,?,?,?)', (u['id'], pid, 'admin', db.now())) enterprise.audit(enterprise.current_actor(), 'project.create', f'project#{pid}', - f'「{d.get("name","")}」送达者 {deliver_email}', request.remote_addr or '') + f'「{d.get("name","")}」送达者用户#{duid} {du["email"]}', request.remote_addr or '') return jsonify({'ok': True, 'id': pid}) err = _check_perm_point('project.view') if err: @@ -379,6 +396,7 @@ def projects(): r['done_count'] = db.q('SELECT COUNT(*) c FROM tasks WHERE project_id=? AND status="done" AND deleted=0', (r['id'],))[0]['c'] r['perm'] = _project_perm(r['id']) + _attach_deliver_user(r) return jsonify({'ok': True, 'data': rows}) @@ -396,6 +414,7 @@ def project_detail(pid): if not p: return jsonify({'ok': False, 'error': '项目不存在'}), 404 p['perm'] = _project_perm(pid) + _attach_deliver_user(p) return jsonify({'ok': True, 'data': p}) if request.method == 'DELETE': err = _check_project_perm(pid, 'admin') @@ -432,6 +451,15 @@ def project_detail(pid): if sets: args.append(db.now()) db.w(f'UPDATE projects SET {", ".join(sets)}, updated_at=? WHERE id=?', (*args, pid)) + # 送达者变更:校验用户 + 同步邮箱快照 + if 'deliver_user_id' in d: + du = db.q('SELECT id, email FROM users WHERE id=?', (int(d['deliver_user_id']),), one=True) + if not du: + return jsonify({'ok': False, 'error': '送达者用户不存在'}), 400 + if not (du.get('email') or '').strip(): + return jsonify({'ok': False, 'error': '送达者用户未设置邮箱,请先补全'}), 400 + db.w('UPDATE projects SET deliver_user_id=?, deliver_email=?, updated_at=? WHERE id=?', + (int(d['deliver_user_id']), du['email'], db.now(), pid)) return jsonify({'ok': True}) @@ -1279,8 +1307,8 @@ def project_deliver(pid): if err: return err proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) - if not (proj.get('deliver_email') or '').strip(): - return jsonify({'ok': False, 'error': '项目未配置送达者邮箱,请先在编辑项目中填写'}), 400 + if not delivery.deliver_email_of(proj): + return jsonify({'ok': False, 'error': '项目未配置送达者用户,请先在编辑项目中指定'}), 400 base = request.host_url.rstrip('/') # 网页交付物:确保 Demo 已部署 demo = proj.get('demo_url') or '' @@ -1305,17 +1333,17 @@ def project_deliver(pid): f'交付物已打包为附件({pkg["size"]} B),请查收。') subject = f'📦 项目交付:{proj["name"]}' body = delivery._email_body(proj, extra=extra) - ok, msg = delivery.send_mail(proj['deliver_email'], subject, body, + ok, msg = delivery.send_mail(delivery.deliver_email_of(proj), subject, body, attachments=[attach] if attach else None) if not ok: return jsonify({'ok': False, 'error': msg}), 500 db.w('UPDATE projects SET delivered_at=? WHERE id=?', (db.now(), pid)) db.w('INSERT INTO project_deliverables (project_id, name, kind, path, demo_url, size, note, created_at) ' 'VALUES (?,?,?,?,?,?,?,?)', - (pid, f'交付邮件 → {proj["deliver_email"]}', 'email', pkg['name'], demo, + (pid, f'交付邮件 → {delivery.deliver_email_of(proj)}', 'email', pkg['name'], demo, pkg['size'], '手动交付(含附件)', db.now())) enterprise.audit(enterprise.current_actor(), 'deliver.send', f'project#{pid}', - f'邮件交付 → {proj["deliver_email"]}({pkg["name"]})', request.remote_addr or '') + f'邮件交付 → {delivery.deliver_email_of(proj)}({pkg["name"]})', request.remote_addr or '') return jsonify({'ok': True, 'msg': msg, 'demo_url': demo, 'package': pkg['name']}) @@ -1327,8 +1355,8 @@ def project_complete(pid): if err: return err proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) - if not (proj.get('deliver_email') or '').strip(): - return jsonify({'ok': False, 'error': '项目未配置送达者邮箱,请先填写'}), 400 + if not delivery.deliver_email_of(proj): + return jsonify({'ok': False, 'error': '项目未配置送达者用户,请先在编辑项目中指定'}), 400 if proj['status'] == 'done': return jsonify({'ok': False, 'error': '项目已完成,无需重复交付'}), 400 base = request.host_url.rstrip('/') @@ -1337,7 +1365,7 @@ def project_complete(pid): return jsonify({'ok': False, 'error': msg}), 500 db.w('UPDATE projects SET status="done", updated_at=? WHERE id=?', (db.now(), pid)) enterprise.audit(enterprise.current_actor(), 'project.complete', f'project#{pid}', - f'「{proj["name"]}」完成交付 → {proj["deliver_email"]}', request.remote_addr or '') + f'「{proj["name"]}」完成交付 → {delivery.deliver_email_of(proj)}', request.remote_addr or '') return jsonify({'ok': True, 'msg': msg}) @@ -1350,8 +1378,8 @@ def project_notify_deliverer(pid): return err d = request.get_json(force=True) or {} proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True) - if not (proj.get('deliver_email') or '').strip(): - return jsonify({'ok': False, 'error': '项目未配置送达者邮箱'}), 400 + if not delivery.deliver_email_of(proj): + return jsonify({'ok': False, 'error': '项目未配置送达者用户'}), 400 subject = (d.get('subject') or f'项目动态:{proj["name"]}').strip() text = (d.get('message') or '').strip() if not text: @@ -1918,28 +1946,39 @@ def ent_users(): if request.method == 'POST': d = request.get_json(force=True) username = (d.get('username') or '').strip() + email = (d.get('email') or '').strip() if not username: return jsonify({'ok': False, 'error': '用户名不能为空'}), 400 + if not email or '@' not in email: + return jsonify({'ok': False, 'error': '必填:用户邮箱(用于项目送达/通知)'}), 400 if d.get('source') in ('oidc', 'ldap'): if enterprise.get_user(username): return jsonify({'ok': False, 'error': '用户已存在'}), 400 - db.w('INSERT INTO users (username, password_hash, display_name, role, source, status, created_at) ' - 'VALUES (?,?,?,?,?,?,?)', - (username, '', d.get('display_name', username), d.get('role', 'member'), + db.w('INSERT INTO users (username, password_hash, display_name, email, role, source, status, created_at) ' + 'VALUES (?,?,?,?,?,?,?,?)', + (username, '', d.get('display_name', username), email, d.get('role', 'member'), d.get('source'), 'active', db.now())) else: if not d.get('password'): return jsonify({'ok': False, 'error': '本地用户必须设置密码'}), 400 uid, err = enterprise.create_local_user(username, d['password'], - d.get('display_name', ''), d.get('role', 'member')) + d.get('display_name', ''), d.get('role', 'member'), email) if err: return jsonify({'ok': False, 'error': err}), 400 return jsonify({'ok': True}) - rows = db.q('SELECT id, username, display_name, role, source, status, last_login_at, created_at ' + rows = db.q('SELECT id, username, display_name, email, role, source, status, last_login_at, created_at ' 'FROM users ORDER BY id') return jsonify({'ok': True, 'data': rows}) +@app.route('/api/users') +@require_auth +def users_light(): + """轻量用户列表(登录即可):用于创建项目时选择送达者用户""" + rows = db.q('SELECT id, username, display_name, email, role, status FROM users ORDER BY id') + return jsonify({'ok': True, 'data': rows}) + + @app.route('/api/enterprise/users/', methods=['PUT', 'DELETE']) @require_admin def ent_user_detail(uid): @@ -1960,6 +1999,13 @@ def ent_user_detail(uid): db.w('UPDATE users SET status=? WHERE id=?', (d['status'], uid)) if 'display_name' in d: db.w('UPDATE users SET display_name=? WHERE id=?', (d['display_name'], uid)) + if 'email' in d: + email = (d['email'] or '').strip() + if not email or '@' not in email: + return jsonify({'ok': False, 'error': '邮箱格式不正确'}), 400 + db.w('UPDATE users SET email=? WHERE id=?', (email, uid)) + # 同步:该用户送达的旧项目邮箱快照刷新 + db.w('UPDATE projects SET deliver_email=? WHERE deliver_user_id=?', (email, uid)) if d.get('password'): db.w('UPDATE users SET password_hash=? WHERE id=?', (db.hash_password(d['password']), uid)) return jsonify({'ok': True}) diff --git a/db.py b/db.py index 931ef52..084b062 100644 --- a/db.py +++ b/db.py @@ -16,7 +16,8 @@ CREATE TABLE IF NOT EXISTS projects ( acceptance_criteria TEXT DEFAULT '', status TEXT DEFAULT 'active', -- planning/active/done/archived budget_limit REAL DEFAULT 0, -- 项目预算上限(元),0=不限 - deliver_email TEXT DEFAULT '', -- 送达者(人)邮箱,新建项目必填 + deliver_user_id INTEGER, -- 送达者用户 id(users.id),实时取该用户邮箱 + deliver_email TEXT DEFAULT '', -- 送达者邮箱快照(兼容/展示,发送时以用户最新邮箱为准) deliver_type TEXT DEFAULT 'web', -- 交付物类型 web=网页 / file=文件包 deliver_note TEXT DEFAULT '', -- 交付说明 workspace_dir TEXT DEFAULT '', -- 项目工作目录(相对 data/ 的目录名) @@ -241,6 +242,7 @@ CREATE TABLE IF NOT EXISTS users ( username TEXT NOT NULL UNIQUE, password_hash TEXT DEFAULT '', display_name TEXT DEFAULT '', + email TEXT DEFAULT '', -- 用户邮箱(必填,送达/通知用) role TEXT DEFAULT 'member', -- admin / member / auditor source TEXT DEFAULT 'local', -- local / oidc / ldap status TEXT DEFAULT 'active', -- active / disabled @@ -391,6 +393,22 @@ def _migrate(): ): if col not in pcols: conn.execute(ddl) + # V3.2:projects 送达者改为用户 id(实时取邮箱) + if 'deliver_user_id' not in pcols: + conn.execute('ALTER TABLE projects ADD COLUMN deliver_user_id INTEGER') + # V3.2:users 邮箱列 + 存量用户默认邮箱 + 旧项目按邮箱回填送达者用户 + ucols = {r['name'] for r in conn.execute('PRAGMA table_info(users)')} + if 'email' not in ucols: + conn.execute("ALTER TABLE users ADD COLUMN email TEXT DEFAULT ''") + for r in conn.execute('SELECT id, username, email FROM users WHERE email IS NULL OR email=\'\''): + conn.execute('UPDATE users SET email=? WHERE id=?', + (f'{r["username"]}@tphai.com', r['id'])) + # 旧项目:按 deliver_email 匹配用户回填 deliver_user_id + for r in conn.execute("SELECT id, deliver_email FROM projects WHERE (deliver_user_id IS NULL OR deliver_user_id=0) " + "AND deliver_email IS NOT NULL AND deliver_email != ''"): + u = conn.execute('SELECT id FROM users WHERE email=?', (r['deliver_email'],)).fetchone() + if u: + conn.execute('UPDATE projects SET deliver_user_id=? WHERE id=?', (u['id'], r['id'])) # V3:老项目补齐工作目录名 for r in conn.execute("SELECT id, workspace_dir FROM projects WHERE workspace_dir IS NULL OR workspace_dir=''"): conn.execute('UPDATE projects SET workspace_dir=? WHERE id=?', diff --git a/delivery.py b/delivery.py index 64add1f..0c2d8d4 100644 --- a/delivery.py +++ b/delivery.py @@ -290,11 +290,21 @@ def _email_body(project, extra=''): return '\n'.join(lines) +def deliver_email_of(project): + """送达者邮箱:优先取绑定的用户(deliver_user_id)最新邮箱;无绑定则回退旧快照""" + uid = project.get('deliver_user_id') + if uid: + u = db.q('SELECT email FROM users WHERE id=?', (uid,), one=True) + if u and (u.get('email') or '').strip(): + return u['email'].strip() + return (project.get('deliver_email') or '').strip() + + def notify_deliverer(project, subject, text, attach=None, base_url=''): """发邮件给送达者,写交付记录。返回 (ok, msg)""" - email = (project.get('deliver_email') or '').strip() + email = deliver_email_of(project) if not email: - return False, '项目未配置送达者邮箱' + return False, '项目未配置送达者(请在编辑项目中指定送达者用户)' ok, msg = send_mail(email, subject, text, attachments=[attach] if attach else None) if ok: db.w('UPDATE projects SET delivered_at=? WHERE id=?', (db.now(), project['id'])) @@ -303,7 +313,7 @@ def notify_deliverer(project, subject, text, attach=None, base_url=''): def notify_blocker(project, task_title, detail): """项目遇到无法绕开的难关 → 及时邮件通知送达者(同项目限频防打扰)""" - email = (project.get('deliver_email') or '').strip() + email = deliver_email_of(project) if not email: return # 去重:同项目 30 分钟内只提醒一次(detail 带项目标记) @@ -329,9 +339,9 @@ def notify_blocker(project, task_title, detail): def notify_project_complete(project, base_url=''): """项目完成 → 打包 + 邮件送达(含 Demo 链接与附件)。返回 (ok, msg)""" - email = (project.get('deliver_email') or '').strip() + email = deliver_email_of(project) if not email: - return False, '项目未配置送达者邮箱' + return False, '项目未配置送达者(请在编辑项目中指定送达者用户)' # 网页交付物:确保已部署 Demo if project.get('deliver_type') == 'web' and not project.get('demo_url'): try: diff --git a/enterprise.py b/enterprise.py index e51fbb9..9fa5731 100644 --- a/enterprise.py +++ b/enterprise.py @@ -110,13 +110,15 @@ def is_admin(username): return role_of(username) == 'admin' -def create_local_user(username, password, display_name='', role='member'): +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, role, source, status, created_at) ' - 'VALUES (?,?,?,?,?,?,?)', - (username, db.hash_password(password), display_name or username, role, 'local', 'active', db.now())) + '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 @@ -228,14 +230,17 @@ def sso_login(userinfo): 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, role, source, status, created_at) ' - 'VALUES (?,?,?,?,?,?,?)', - (username, '', display, role, 'oidc', 'active', db.now())) + '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 @@ -276,12 +281,15 @@ def ldap_login(username, password): if sso_config().get('sso_auto_provision') != '1': return None, '用户未开通,请联系管理员' db.w( - 'INSERT INTO users (username, password_hash, display_name, role, source, status, created_at) ' - 'VALUES (?,?,?,?,?,?,?)', - (username, '', info.get('display') or username, 'member', 'ldap', 'active', db.now())) + '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 @@ -296,7 +304,7 @@ def export_all(): '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.1.0'} + 'platform': 'ai-worker-platform', 'version': 'v3.2.0'} for t in tables: try: out[t] = db.q(f'SELECT * FROM {t}') diff --git a/static/app.js b/static/app.js index 2fb8997..1b276ad 100644 --- a/static/app.js +++ b/static/app.js @@ -166,7 +166,7 @@ async function pageProjects() {
${esc(p.objective || p.description || '暂无描述')}
任务 ${p.task_count} · 完成 ${p.done_count} ${p.budget_limit ? '预算 ¥' + p.budget_limit : ''}
-
📧 ${esc(p.deliver_email || '未配置送达者')} +
📬 送达:${p.deliver_user ? esc(p.deliver_user.display_name || p.deliver_user.username) + '(' + esc(p.deliver_user.email || '') + ')' : esc(p.deliver_email || '未绑定用户')} ${p.perm ? `${{view:'查看',manage:'管理',admin:'管理员'}[p.perm] || p.perm}` : ''} ${p.demo_url ? `🌐 Demo` : ''}
@@ -175,6 +175,7 @@ async function pageProjects() { } function openProjectModal(p = {}) { + const usersP = api('/api/users'); openModal(`

${p.id ? '编辑项目' : '新建项目'}

@@ -188,8 +189,9 @@ function openProjectModal(p = {}) {
-
-
+
+ + 送达者 = 平台用户,其邮箱变更后项目通知自动跟随;可在项目进行中随时更换
+ `); $('#eu-save').addEventListener('click', async () => { + const email = $('#eu-email').value.trim(); + if (!email || !email.includes('@')) return toast('必填:用户邮箱', 'err'); try { await api('/api/enterprise/users', {method: 'POST', body: { username: $('#eu-name').value.trim(), display_name: $('#eu-display').value, - role: $('#eu-role').value, source: $('#eu-source').value, password: $('#eu-pwd').value + email, role: $('#eu-role').value, source: $('#eu-source').value, password: $('#eu-pwd').value }}); toast('已创建', 'ok'); closeModal(); pageEnterprise(); } catch (e) { toast(e.message, 'err'); } @@ -2374,17 +2393,21 @@ function entEditUser(id) { openModal(`

编辑用户 ${esc(u.username)}

+ `); $('#eu2-save').addEventListener('click', async () => { + const email = $('#eu2-email').value.trim(); + if (!email || !email.includes('@')) return toast('必填:用户邮箱', 'err'); await api(`/api/enterprise/users/${id}`, {method: 'PUT', body: { - display_name: $('#eu2-display').value, role: $('#eu2-role').value, + display_name: $('#eu2-display').value, email, + role: $('#eu2-role').value, status: $('#eu2-status').value, password: $('#eu2-pwd').value }}); - toast('已保存', 'ok'); closeModal(); pageEnterprise(); + toast('已保存(送达项目自动跟随新邮箱)', 'ok'); closeModal(); pageEnterprise(); }); $('#eu2-del').addEventListener('click', async () => { if (!confirm('确定删除该用户?')) return;