"""用户管理路由""" import csv, io, base64, uuid from flask import Blueprint, request, jsonify, session, redirect, url_for, render_template from services.db import query from services.passwords import hash_password, validate_password from routes.auth import login_required, role_required users_bp = Blueprint('users', __name__) @users_bp.route('/api/users/positions') @login_required def position_list(): """获取启用的岗位列表,供用户管理中的岗位选择和筛选使用。""" rows = query( "SELECT position_code AS code, position_name AS name " "FROM user_positions WHERE status=1 ORDER BY sort_order ASC, id ASC" ) return jsonify({"code": 0, "data": rows}) @users_bp.route('/api/users/positions/create', methods=['POST']) @login_required def position_create(): """新增岗位。岗位编码由系统生成,避免中文岗位名影响现有业务匹配。""" if session.get('role') not in ('admin', 'super_admin'): return jsonify({"code": 403, "msg": "仅管理员可新增岗位"}) data = request.get_json(silent=True) or {} name = str(data.get('name', '')).strip() if not name: return jsonify({"code": 400, "msg": "岗位名称不能为空"}) if len(name) > 100: return jsonify({"code": 400, "msg": "岗位名称不能超过100个字符"}) try: exists = query( "SELECT id FROM user_positions WHERE position_name=%s LIMIT 1", (name,) ) if exists: return jsonify({"code": 400, "msg": "该岗位已存在"}) position_code = 'custom_' + uuid.uuid4().hex[:16] query( "INSERT INTO user_positions (position_code, position_name, sort_order, status) " "VALUES (%s, %s, 0, 1)", (position_code, name) ) return jsonify({ "code": 0, "msg": "岗位新增成功", "data": {"code": position_code, "name": name} }) except Exception as e: return jsonify({"code": 500, "msg": str(e)}) @users_bp.route('/users') @login_required def users_page(): if not role_required('users'): return redirect(url_for('auth.index')) return render_template('base.html', page='users') # ─── API: 用户管理 ─── @users_bp.route('/api/users/list') @login_required def user_list(): search = request.args.get('search', '').strip() role = request.args.get('role', '').strip() position = request.args.get('position', '').strip() status = request.args.get('status', '').strip() sort = request.args.get('sort', '').strip() sql = "SELECT id, username, real_name, role, user_level, position, phone, email, status, last_login, created_at, is_protected FROM dashboard_users WHERE 1=1" params = [] if search: sql += " AND (real_name LIKE %s OR username LIKE %s OR position LIKE %s)" params.extend(['%' + search + '%', '%' + search + '%', '%' + search + '%']) if role: sql += " AND role = %s" params.append(role) if position: sql += " AND position = %s" params.append(position) if status: sql += " AND status = %s" params.append(int(status)) if sort == 'role_asc': sql += " ORDER BY role ASC, id ASC" elif sort == 'role_desc': sql += " ORDER BY role DESC, id ASC" else: sql += " ORDER BY CASE WHEN role IN ('super_admin','admin') THEN 0 ELSE 1 END, FIELD(role,'super_admin','admin'), id ASC" users = query(sql, tuple(params) if params else None) return jsonify({"code": 0, "data": users}) @users_bp.route('/api/users/create', methods=['POST']) @login_required def user_create(): if session.get('role') not in ('admin', 'super_admin'): return jsonify({"code": 403, "msg": "仅管理员可操作"}) data = request.get_json(silent=True) or {} valid, message = validate_password(data.get('password', '')) if not valid: return jsonify({"code": 400, "msg": message}) try: query(""" INSERT INTO dashboard_users (username, password, real_name, role, user_level, position, phone, email, must_change_password) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 1) """, ( data['username'], hash_password(data['password']), data.get('real_name', ''), data.get('role', 'viewer'), data.get('user_level', 'employee'), data.get('position', ''), data.get('phone', ''), data.get('email', '') )) return jsonify({"code": 0, "msg": "创建成功"}) except Exception as e: return jsonify({"code": 500, "msg": str(e)}) @users_bp.route('/api/users/update', methods=['POST']) @login_required def user_update(): if session.get('role') != 'super_admin': return jsonify({"code": 403, "msg": "权限不足,仅超级管理员可以编辑用户"}) data = request.json user_id = data['id'] if data.get('password'): valid, message = validate_password(data['password']) if not valid: return jsonify({"code": 400, "msg": message}) query(""" UPDATE dashboard_users SET real_name=%s, role=%s, user_level=%s, position=%s, phone=%s, email=%s, status=%s, password=%s, must_change_password=1 WHERE id=%s """, ( data.get('real_name', ''), data.get('role', 'viewer'), data.get('user_level', 'employee'), data.get('position', ''), data.get('phone', ''), data.get('email', ''), int(data.get('status', 1)), hash_password(data['password']), user_id )) else: query(""" UPDATE dashboard_users SET real_name=%s, role=%s, user_level=%s, position=%s, phone=%s, email=%s, status=%s WHERE id=%s """, ( data.get('real_name', ''), data.get('role', 'viewer'), data.get('user_level', 'employee'), data.get('position', ''), data.get('phone', ''), data.get('email', ''), int(data.get('status', 1)), user_id )) return jsonify({"code": 0, "msg": "更新成功"}) @users_bp.route('/api/users/delete', methods=['POST']) @login_required def user_delete(): if session.get('role') not in ('admin', 'super_admin'): return jsonify({"code": 403, "msg": "仅管理员可操作"}) user_id = request.json.get('id') if int(user_id) == session['user_id']: return jsonify({"code": 500, "msg": "不能删除自己"}) protected = query("SELECT is_protected FROM dashboard_users WHERE id=%s", (user_id,)) if protected and protected[0].get('is_protected') == 1: return jsonify({"code": 500, "msg": "该用户为受保护账号,不可删除"}) query("DELETE FROM dashboard_users WHERE id=%s", (user_id,)) return jsonify({"code": 0, "msg": "删除成功"}) @users_bp.route('/api/users/template') @login_required def user_template(): """下载用户导入CSV模板""" csv_content = '\ufeff用户名,密码,真实姓名,角色,员工级别,岗位,手机,邮箱,状态\n' csv_content += 'zhangsan,Abc123!@#45,张三,procurement_planner,employee,采购专员,13800138000,zhangsan@example.com,1\n' csv_content += 'lisi,Xyz789$%^01,李四,admin,employee,系统管理员,13900139000,lisi@example.com,1\n' from urllib.parse import quote filename = '用户导入模板.csv' encoded = quote(filename, safe='') return csv_content, 200, { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': f"attachment; filename*=UTF-8''{encoded}" } @users_bp.route('/api/users/import', methods=['POST']) @login_required def user_import(): """导入用户CSV""" if session.get('role') not in ('admin', 'super_admin'): return jsonify({"code": 403, "msg": "仅管理员可操作"}) data = request.get_json() if not data or 'csv_base64' not in data: return jsonify({"code": 400, "msg": "请提供CSV数据"}) raw_bytes = base64.b64decode(data['csv_base64']) content = None for enc in ['utf-8-sig', 'gbk', 'gb18030', 'utf-8']: try: decoded = raw_bytes.decode(enc) test_reader = csv.DictReader(io.StringIO(decoded)) for test_row in test_reader: if test_row and ('用户名' in test_row or '密码' in test_row): content = decoded break break if content: break except: continue if content is None: content = raw_bytes.decode('utf-8', errors='replace') reader = csv.DictReader(io.StringIO(content)) success = 0 errors = [] for i, row in enumerate(reader, start=2): username = (row.get('用户名') or '').strip() password = (row.get('密码') or '').strip() if not username or not password: errors.append(f"第{i}行:用户名和密码不能为空") continue valid, message = validate_password(password) if not valid: errors.append(f"第{i}行:{message}") continue try: query( """INSERT INTO dashboard_users (username, password, real_name, role, user_level, position, phone, email, status, must_change_password) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 1)""", ( username, hash_password(password), (row.get('真实姓名') or '').strip(), (row.get('角色') or 'viewer').strip(), (row.get('员工级别') or 'employee').strip(), (row.get('岗位') or '').strip(), (row.get('手机') or '').strip(), (row.get('邮箱') or '').strip(), int((row.get('状态') or '1').strip()) ) ) success += 1 except Exception as e: errors.append(f"第{i}行({username}):{str(e)}") return jsonify({ "code": 0, "data": {"success": success, "errors": errors}, "msg": f"成功导入{success}个用户" + (f",{len(errors)}个失败" if errors else "") }) @users_bp.route('/api/users/followers') @login_required def user_followers(): """获取采购跟单人员列表""" rows = query("SELECT real_name FROM dashboard_users WHERE position='procurement_follower' AND status=1 ORDER BY real_name") return jsonify({"code": 0, "data": [r['real_name'] for r in rows]})