注册码 + 管理员系统:

User 模型新增 is_admin 字段
新增 InviteCode 模型(邀请码表)
注册接口必须提供有效邀请码,使用后自动标记
管理员接口:查看所有用户、启用/禁用用户、生成/删除邀请码
前端新增管理面板页面 /admin,导航栏对管理员显示入口
注册页面新增邀请码输入框
选择性超话签到:

新增 GET /api/v1/accounts/{id}/topics 接口获取超话列表
POST /signin 接口支持 {"topic_indices": [0,1,3]} 选择性签到
新增超话选择页面 /accounts/{id}/topics,支持全选/手动勾选
账号详情页新增"选择超话签到"按钮
This commit is contained in:
2026-03-17 17:05:28 +08:00
parent 2fb27aa714
commit e514a11e62
26 changed files with 649 additions and 18 deletions

View File

@@ -112,15 +112,20 @@ def register():
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
invite_code = request.form.get('invite_code', '').strip()
if password != confirm_password:
flash('两次输入的密码不一致', 'danger')
return redirect(url_for('register'))
if not invite_code:
flash('请输入邀请码', 'danger')
return redirect(url_for('register'))
try:
response = requests.post(
f'{AUTH_BASE_URL}/auth/register',
json={'username': username, 'email': email, 'password': password},
json={'username': username, 'email': email, 'password': password, 'invite_code': invite_code},
timeout=10
)
@@ -694,7 +699,7 @@ def verify_account(account_id):
def manual_signin(account_id):
"""手动触发签到"""
try:
response = api_request('POST', f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin')
response = api_request('POST', f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin', json={})
data = response.json()
if data.get('success'):
result = data.get('data', {})
@@ -910,6 +915,144 @@ def not_found(error):
def server_error(error):
return render_template('500.html'), 500
# ===================== Admin Routes =====================
def admin_required(f):
"""管理员权限装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
flash('请先登录', 'warning')
return redirect(url_for('login'))
if not session.get('user', {}).get('is_admin'):
flash('需要管理员权限', 'danger')
return redirect(url_for('dashboard'))
return f(*args, **kwargs)
return decorated_function
@app.route('/admin')
@admin_required
def admin_panel():
"""管理员面板"""
# 获取用户列表
try:
resp = api_request('GET', f'{AUTH_BASE_URL}/admin/users')
users = resp.json().get('data', []) if resp.status_code == 200 else []
except Exception:
users = []
# 获取邀请码列表
try:
resp = api_request('GET', f'{AUTH_BASE_URL}/admin/invite-codes')
codes = resp.json().get('data', []) if resp.status_code == 200 else []
except Exception:
codes = []
return render_template('admin.html', users=users, invite_codes=codes, user=session.get('user'))
@app.route('/admin/invite-codes/create', methods=['POST'])
@admin_required
def create_invite_code():
"""生成邀请码"""
try:
resp = api_request('POST', f'{AUTH_BASE_URL}/admin/invite-codes')
data = resp.json()
if resp.status_code == 200 and data.get('success'):
code = data['data']['code']
flash(f'邀请码已生成: {code}', 'success')
else:
flash('生成邀请码失败', 'danger')
except Exception as e:
flash(f'连接错误: {str(e)}', 'danger')
return redirect(url_for('admin_panel'))
@app.route('/admin/invite-codes/<code_id>/delete', methods=['POST'])
@admin_required
def delete_invite_code(code_id):
"""删除邀请码"""
try:
resp = api_request('DELETE', f'{AUTH_BASE_URL}/admin/invite-codes/{code_id}')
data = resp.json()
if resp.status_code == 200 and data.get('success'):
flash('邀请码已删除', 'success')
else:
flash(data.get('detail', '删除失败'), 'danger')
except Exception as e:
flash(f'连接错误: {str(e)}', 'danger')
return redirect(url_for('admin_panel'))
@app.route('/admin/users/<user_id>/toggle', methods=['POST'])
@admin_required
def toggle_user(user_id):
"""启用/禁用用户"""
try:
resp = api_request('PUT', f'{AUTH_BASE_URL}/admin/users/{user_id}/toggle')
data = resp.json()
if resp.status_code == 200 and data.get('success'):
status_text = '已启用' if data.get('is_active') else '已禁用'
flash(f'用户{status_text}', 'success')
else:
flash(data.get('detail', '操作失败'), 'danger')
except Exception as e:
flash(f'连接错误: {str(e)}', 'danger')
return redirect(url_for('admin_panel'))
# ===================== Topic Selection Signin =====================
@app.route('/accounts/<account_id>/topics')
@login_required
def account_topics(account_id):
"""获取超话列表页面,供用户勾选签到"""
try:
resp = api_request('GET', f'{API_BASE_URL}/api/v1/accounts/{account_id}/topics')
data = resp.json()
topics = data.get('data', {}).get('topics', []) if data.get('success') else []
# 获取账号信息
acc_resp = api_request('GET', f'{API_BASE_URL}/api/v1/accounts/{account_id}')
acc_data = acc_resp.json()
account = acc_data.get('data') if acc_data.get('success') else None
if not account:
flash('账号不存在', 'danger')
return redirect(url_for('dashboard'))
return render_template('topics.html', account=account, topics=topics, user=session.get('user'))
except Exception as e:
flash(f'获取超话列表失败: {str(e)}', 'danger')
return redirect(url_for('account_detail', account_id=account_id))
@app.route('/accounts/<account_id>/signin-selected', methods=['POST'])
@login_required
def signin_selected(account_id):
"""签到选中的超话"""
try:
indices = request.json.get('topic_indices', [])
resp = api_request(
'POST',
f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin',
json={'topic_indices': indices},
)
data = resp.json()
if data.get('success'):
result = data.get('data', {})
return jsonify({
'success': True,
'data': result,
'message': f"签到完成: {result.get('signed', 0)} 成功, {result.get('already_signed', 0)} 已签, {result.get('failed', 0)} 失败",
})
else:
return jsonify({'success': False, 'message': data.get('message', '签到失败')}), 400
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
if __name__ == '__main__':
debug_mode = os.getenv('FLASK_DEBUG', 'True').lower() in ('true', '1', 'yes')
# use_reloader=False 避免 Windows 终端 QuickEdit 模式导致进程挂起