279 lines
10 KiB
Plaintext
279 lines
10 KiB
Plaintext
"""用户管理路由"""
|
||
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]})
|