Files
lanhao 223761e717 feat: 登录验证 + 邀请码注册 + 用量监控后台
- 新增用户认证系统(登录/登出/邀请码注册)
- 新增管理后台(邀请码管理/用户管理/使用记录/用量统计)
- 合成接口加登录验证,每次调用记录用户和稿件内容
- SQLite存储用户数据和使用日志
- 默认admin账号: simonkoson

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 18:51:23 +08:00

201 lines
5.8 KiB
Python
Raw Permalink 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.
from flask import Blueprint, request, jsonify
from app.db import get_db
from app.routes.auth import admin_required
from werkzeug.security import generate_password_hash
import secrets
import string
bp = Blueprint('admin', __name__, url_prefix='/api/admin')
def generate_invite_code():
"""生成8位邀请码(大写字母+数字,去掉易混淆字符 O/I/0/1"""
alphabet = (
string.ascii_uppercase.replace('O', '').replace('I', '')
+ string.digits.replace('0', '').replace('1', '')
)
return ''.join(secrets.choice(alphabet) for _ in range(8))
# ========== 邀请码管理 ==========
@bp.route('/invites', methods=['GET'])
@admin_required
def list_invites():
conn = get_db()
invites = conn.execute('''
SELECT ic.*, u.username as used_by_username
FROM invite_codes ic
LEFT JOIN users u ON ic.used_by_user_id = u.id
ORDER BY ic.created_at DESC
''').fetchall()
conn.close()
return jsonify({'invites': [dict(row) for row in invites]})
@bp.route('/invites', methods=['POST'])
@admin_required
def create_invite():
data = request.get_json()
editor_name = data.get('editor_name', '').strip()
if not editor_name:
return jsonify({'error': '请填写编导姓名'}), 400
code = generate_invite_code()
conn = get_db()
try:
conn.execute(
'INSERT INTO invite_codes (code, editor_name) VALUES (?, ?)',
(code, editor_name)
)
conn.commit()
return jsonify({'success': True, 'code': code, 'editor_name': editor_name})
finally:
conn.close()
@bp.route('/invites/<int:invite_id>', methods=['DELETE'])
@admin_required
def delete_invite(invite_id):
conn = get_db()
try:
invite = conn.execute(
'SELECT * FROM invite_codes WHERE id = ? AND is_used = 0', (invite_id,)
).fetchone()
if not invite:
return jsonify({'error': '邀请码不存在或已被使用,无法删除'}), 400
conn.execute('DELETE FROM invite_codes WHERE id = ?', (invite_id,))
conn.commit()
return jsonify({'success': True})
finally:
conn.close()
# ========== 用户管理 ==========
@bp.route('/users', methods=['GET'])
@admin_required
def list_users():
conn = get_db()
users = conn.execute('''
SELECT u.id, u.username, u.display_name, u.role, u.is_active, u.created_at,
COUNT(ul.id) as usage_count,
COALESCE(SUM(ul.text_length), 0) as total_chars
FROM users u
LEFT JOIN usage_logs ul ON u.id = ul.user_id
GROUP BY u.id
ORDER BY u.created_at DESC
''').fetchall()
conn.close()
return jsonify({'users': [dict(row) for row in users]})
@bp.route('/users/<int:user_id>', methods=['PUT'])
@admin_required
def update_user(user_id):
data = request.get_json()
conn = get_db()
try:
user = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
if not user:
return jsonify({'error': '用户不存在'}), 404
if 'is_active' in data:
conn.execute(
'UPDATE users SET is_active = ? WHERE id = ?',
(1 if data['is_active'] else 0, user_id)
)
if 'password' in data and data['password']:
conn.execute(
'UPDATE users SET password_hash = ? WHERE id = ?',
(generate_password_hash(data['password']), user_id)
)
if 'display_name' in data and data['display_name'].strip():
conn.execute(
'UPDATE users SET display_name = ? WHERE id = ?',
(data['display_name'].strip(), user_id)
)
conn.commit()
return jsonify({'success': True})
finally:
conn.close()
# ========== 用量日志 ==========
@bp.route('/logs', methods=['GET'])
@admin_required
def list_logs():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
user_id = request.args.get('user_id', type=int)
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
conn = get_db()
where_clauses = []
params = []
if user_id:
where_clauses.append('user_id = ?')
params.append(user_id)
if date_from:
where_clauses.append('created_at >= ?')
params.append(date_from)
if date_to:
where_clauses.append('created_at <= ?')
params.append(date_to + ' 23:59:59')
where_sql = (' WHERE ' + ' AND '.join(where_clauses)) if where_clauses else ''
total = conn.execute(
f'SELECT COUNT(*) FROM usage_logs{where_sql}', params
).fetchone()[0]
offset = (page - 1) * per_page
logs = conn.execute(
f'SELECT * FROM usage_logs{where_sql} ORDER BY created_at DESC LIMIT ? OFFSET ?',
params + [per_page, offset]
).fetchall()
conn.close()
return jsonify({
'logs': [dict(row) for row in logs],
'total': total,
'page': page,
'per_page': per_page,
'total_pages': (total + per_page - 1) // per_page
})
@bp.route('/stats', methods=['GET'])
@admin_required
def stats():
conn = get_db()
user_stats = conn.execute('''
SELECT username, display_name,
COUNT(*) as call_count,
SUM(text_length) as total_chars,
MAX(created_at) as last_used
FROM usage_logs
GROUP BY user_id
ORDER BY call_count DESC
''').fetchall()
totals = conn.execute('''
SELECT COUNT(*) as total_calls,
COALESCE(SUM(text_length), 0) as total_chars,
COUNT(DISTINCT user_id) as active_users
FROM usage_logs
''').fetchone()
conn.close()
return jsonify({
'user_stats': [dict(row) for row in user_stats],
'totals': dict(totals)
})