注册码 + 管理员系统:
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:
147
frontend/app.py
147
frontend/app.py
@@ -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 模式导致进程挂起
|
||||
|
||||
Reference in New Issue
Block a user