From e9357577cb7f13f10bdcf43bf25cadb8008d8e62 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Mon, 27 Apr 2026 17:16:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20+=20=E5=89=8D=E7=AB=AF=E7=94=A8=E6=88=B7=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E7=99=BB=E5=BD=95=E8=B0=83=E7=94=A8backend=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 286 +++++++++++++++++++++++++++++++++++++++++++++++++ www/admin.html | 4 + www/admin.js | 252 +++++++++++++++++++++++++++++++++++++------ www/app.js | 184 +++++++++++++++++++++++-------- 4 files changed, 646 insertions(+), 80 deletions(-) diff --git a/backend/app.py b/backend/app.py index e373873..30b3913 100644 --- a/backend/app.py +++ b/backend/app.py @@ -147,6 +147,25 @@ def init_db(): ) ''') + # 用户表(前端注册用户) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + phone TEXT NOT NULL UNIQUE, + email TEXT, + avatar TEXT DEFAULT '👤', + signature TEXT, + gender TEXT, + age INTEGER, + region TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP + ) + ''') + # 初始化默认大模型配置 cursor.execute('SELECT COUNT(*) FROM llm_configs') if cursor.fetchone()[0] == 0: @@ -295,6 +314,273 @@ def admin_login(): return jsonify({'error': '用户名或密码错误'}), 401 +# ==================== 用户管理 ==================== + +@app.route('/api/register', methods=['POST']) +def user_register(): + """用户注册""" + data = request.json + username = data.get('username') + password = data.get('password') + phone = data.get('phone') + email = data.get('email') + code = data.get('code') # 验证码(暂时只校验格式) + + # 参数验证 + if not username or not password or not phone: + return jsonify({'error': '请填写完整信息'}), 400 + + # 用户名长度检查 + if len(username) < 2 or len(username) > 20: + return jsonify({'error': '用户名长度应为2-20字符'}), 400 + + # 手机号格式检查 + import re + if not re.match(r'^1[3-9]\d{9}$', phone): + return jsonify({'error': '手机号格式不正确'}), 400 + + # 验证码检查(模拟,6位数字) + if not code or not re.match(r'^\d{6}$', code): + return jsonify({'error': '验证码格式不正确'}), 400 + + # 邮箱格式检查(可选) + if email and not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', email): + return jsonify({'error': '邮箱格式不正确'}), 400 + + conn = get_db() + cursor = conn.cursor() + + # 检查用户名是否已存在 + cursor.execute('SELECT id FROM users WHERE username = ?', (username,)) + if cursor.fetchone(): + conn.close() + return jsonify({'error': '用户名已存在'}), 400 + + # 检查手机号是否已存在 + cursor.execute('SELECT id FROM users WHERE phone = ?', (phone,)) + if cursor.fetchone(): + conn.close() + return jsonify({'error': '手机号已注册'}), 400 + + # 创建用户 + password_hash = hashlib.sha256(password.encode()).hexdigest() + cursor.execute(''' + INSERT INTO users (username, password_hash, phone, email) + VALUES (?, ?, ?, ?) + ''', (username, password_hash, phone, email)) + + # 记录注册统计 + cursor.execute('INSERT INTO stats_logs (log_type, log_key) VALUES (?, ?)', ('user_register', 'new')) + + conn.commit() + user_id = cursor.lastrowid + conn.close() + + return jsonify({'success': True, 'user_id': user_id}) + + +@app.route('/api/login', methods=['POST']) +def user_login(): + """用户登录""" + data = request.json + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({'error': '请输入用户名和密码'}), 400 + + password_hash = hashlib.sha256(password.encode()).hexdigest() + conn = get_db() + cursor = conn.cursor() + cursor.execute('SELECT * FROM users WHERE username = ? AND password_hash = ?', (username, password_hash)) + user = cursor.fetchone() + + if user: + # 更新最后登录时间 + cursor.execute('UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],)) + conn.commit() + conn.close() + return jsonify({ + 'success': True, + 'user': { + 'id': user['id'], + 'username': user['username'], + 'phone': user['phone'], + 'email': user['email'], + 'avatar': user['avatar'], + 'signature': user['signature'], + 'gender': user['gender'], + 'age': user['age'], + 'region': user['region'] + } + }) + else: + conn.close() + return jsonify({'error': '用户名或密码错误'}), 401 + + +@app.route('/api/admin/users', methods=['GET']) +def get_users(): + """获取用户列表""" + conn = get_db() + cursor = conn.cursor() + + # 支持搜索 + search = request.args.get('search', '') + if search: + cursor.execute(''' + SELECT id, username, phone, email, avatar, signature, gender, age, region, + created_at, last_login_at FROM users + WHERE username LIKE ? OR phone LIKE ? OR email LIKE ? + ORDER BY created_at DESC + ''', (f'%{search}%', f'%{search}%', f'%{search}%')) + else: + cursor.execute(''' + SELECT id, username, phone, email, avatar, signature, gender, age, region, + created_at, last_login_at FROM users ORDER BY created_at DESC + ''') + + users = [dict(row) for row in cursor.fetchall()] + conn.close() + return jsonify(users) + + +@app.route('/api/admin/users/', methods=['GET']) +def get_user(id): + """获取单个用户""" + conn = get_db() + cursor = conn.cursor() + cursor.execute('SELECT * FROM users WHERE id = ?', (id,)) + user = cursor.fetchone() + conn.close() + + if user: + return jsonify(dict(user)) + else: + return jsonify({'error': '用户不存在'}), 404 + + +@app.route('/api/admin/users/', methods=['PUT']) +def update_user(id): + """更新用户信息""" + data = request.json + conn = get_db() + cursor = conn.cursor() + + # 检查用户是否存在 + cursor.execute('SELECT id FROM users WHERE id = ?', (id,)) + if not cursor.fetchone(): + conn.close() + return jsonify({'error': '用户不存在'}), 404 + + # 检查用户名是否重复(如果要修改用户名) + if data.get('username'): + cursor.execute('SELECT id FROM users WHERE username = ? AND id != ?', (data['username'], id)) + if cursor.fetchone(): + conn.close() + return jsonify({'error': '用户名已存在'}), 400 + + # 更新用户信息 + cursor.execute(''' + UPDATE users SET username=?, email=?, avatar=?, signature=?, gender=?, age=?, region=?, + updated_at=CURRENT_TIMESTAMP WHERE id=? + ''', (data.get('username'), data.get('email'), data.get('avatar', '👤'), + data.get('signature'), data.get('gender'), data.get('age'), + data.get('region'), id)) + + conn.commit() + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/admin/users/', methods=['DELETE']) +def delete_user(id): + """删除用户""" + conn = get_db() + cursor = conn.cursor() + cursor.execute('DELETE FROM users WHERE id = ?', (id,)) + conn.commit() + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/admin/users//password', methods=['PUT']) +def reset_user_password(id): + """重置用户密码""" + data = request.json + new_password = data.get('password') + + if not new_password or len(new_password) < 6: + return jsonify({'error': '密码长度至少6位'}), 400 + + password_hash = hashlib.sha256(new_password.encode()).hexdigest() + conn = get_db() + cursor = conn.cursor() + cursor.execute('UPDATE users SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?', + (password_hash, id)) + conn.commit() + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/user/', methods=['PUT']) +def update_user_profile(id): + """用户更新自己的资料""" + data = request.json + conn = get_db() + cursor = conn.cursor() + + # 检查用户是否存在 + cursor.execute('SELECT id FROM users WHERE id = ?', (id,)) + if not cursor.fetchone(): + conn.close() + return jsonify({'error': '用户不存在'}), 404 + + # 更新用户资料 + cursor.execute(''' + UPDATE users SET avatar=?, signature=?, gender=?, age=?, region=?, email=?, + updated_at=CURRENT_TIMESTAMP WHERE id=? + ''', (data.get('avatar', '👤'), data.get('signature'), data.get('gender'), + data.get('age'), data.get('region'), data.get('email'), id)) + + conn.commit() + conn.close() + return jsonify({'success': True}) + + +@app.route('/api/user//password', methods=['PUT']) +def change_user_password(id): + """用户修改自己的密码""" + data = request.json + old_password = data.get('old_password') + new_password = data.get('new_password') + + if not old_password or not new_password: + return jsonify({'error': '请输入旧密码和新密码'}), 400 + + if len(new_password) < 6: + return jsonify({'error': '新密码长度至少6位'}), 400 + + old_hash = hashlib.sha256(old_password.encode()).hexdigest() + new_hash = hashlib.sha256(new_password.encode()).hexdigest() + + conn = get_db() + cursor = conn.cursor() + + # 验证旧密码 + cursor.execute('SELECT id FROM users WHERE id = ? AND password_hash = ?', (id, old_hash)) + if not cursor.fetchone(): + conn.close() + return jsonify({'error': '旧密码不正确'}), 400 + + # 更新密码 + cursor.execute('UPDATE users SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?', + (new_hash, id)) + conn.commit() + conn.close() + return jsonify({'success': True}) + + # ==================== 大模型接口管理 ==================== @app.route('/api/admin/llm', methods=['GET']) diff --git a/www/admin.html b/www/admin.html index 9bae96f..7e02108 100644 --- a/www/admin.html +++ b/www/admin.html @@ -435,6 +435,10 @@ 📊 统计信息 + -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - `); -} function showEditAgentModal(agentId) { const agent = agents.find(a => a.agent_id === agentId); diff --git a/www/app.js b/www/app.js index 4c19853..f328b55 100644 --- a/www/app.js +++ b/www/app.js @@ -2447,18 +2447,34 @@ function handleLogin() { return; } - // 检查本地存储的用户 - const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]'); - const user = users.find(u => u.username === username && u.password === password); - - if (user) { - currentUser = { username: user.username, registeredAt: user.registeredAt }; - saveCurrentUser(); - showToast('登录成功'); - showMainPage(); - } else { - showToast('用户名或密码错误'); - } + // 调用后台登录API + fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + currentUser = { + id: data.user.id, + username: data.user.username, + phone: data.user.phone, + email: data.user.email || '', + avatar: data.user.avatar || '👤', + signature: data.user.signature || '', + registeredAt: Date.now() + }; + saveCurrentUser(); + showToast('登录成功'); + showMainPage(); + } else { + showToast(data.error || '用户名或密码错误'); + } + }) + .catch(e => { + showToast('网络错误,请重试'); + }); } // ==================== 注册页面 ==================== @@ -2477,12 +2493,21 @@ function showRegisterPage() {
- +
+
+
+ + +
+
+ +
+
@@ -2527,6 +2552,12 @@ function showRegisterPage() { }); } + // 获取验证码按钮 + const getCodeBtn = document.getElementById('getCodeBtn'); + if (getCodeBtn) { + getCodeBtn.addEventListener('click', handleGetCode); + } + // 回车注册 document.getElementById('registerPasswordConfirm')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') handleRegister(); @@ -2539,6 +2570,50 @@ function showRegisterPage() { e.target.value = e.target.value.replace(/\D/g, ''); }); } + + // 验证码输入限制(只允许数字) + const codeInput = document.getElementById('registerCode'); + if (codeInput) { + codeInput.addEventListener('input', (e) => { + e.target.value = e.target.value.replace(/\D/g, ''); + }); + } +} + +// 获取验证码(模拟) +let verifyCode = ''; +let codeExpireTime = 0; + +function handleGetCode() { + const phone = document.getElementById('registerPhone')?.value.trim(); + + if (!validatePhone(phone)) { + showToast('请先输入正确的手机号'); + return; + } + + // 生成6位随机验证码 + verifyCode = Math.floor(100000 + Math.random() * 900000).toString(); + codeExpireTime = Date.now() + 5 * 60 * 1000; // 5分钟有效 + + // 模拟发送(实际项目中应调用短信API) + showToast(`验证码已发送: ${verifyCode}(演示)`); + + // 60秒倒计时 + const getCodeBtn = document.getElementById('getCodeBtn'); + if (getCodeBtn) { + getCodeBtn.disabled = true; + let countdown = 60; + const timer = setInterval(() => { + getCodeBtn.textContent = `${countdown}s`; + countdown--; + if (countdown <= 0) { + clearInterval(timer); + getCodeBtn.disabled = false; + getCodeBtn.textContent = '获取验证码'; + } + }, 1000); + } } // 验证手机号格式 @@ -2559,12 +2634,13 @@ function handleRegister() { const username = document.getElementById('registerUsername')?.value.trim(); const phone = document.getElementById('registerPhone')?.value.trim(); const email = document.getElementById('registerEmail')?.value.trim(); + const code = document.getElementById('registerCode')?.value.trim(); const password = document.getElementById('registerPassword')?.value; const passwordConfirm = document.getElementById('registerPasswordConfirm')?.value; // 验证用户名 - if (!username || username.length < 3 || username.length > 20) { - showToast('用户名需要3-20个字符'); + if (!username || username.length < 2 || username.length > 20) { + showToast('用户名需要2-20个字符'); return; } @@ -2574,8 +2650,20 @@ function handleRegister() { return; } + // 验证验证码 + if (!code || code.length !== 6) { + showToast('请输入6位验证码'); + return; + } + + // 检查验证码是否正确(模拟) + if (code !== verifyCode || Date.now() > codeExpireTime) { + showToast('验证码不正确或已过期'); + return; + } + // 验证邮箱(可选) - if (!validateEmail(email)) { + if (email && !validateEmail(email)) { showToast('请输入正确的邮箱格式'); return; } @@ -2591,37 +2679,39 @@ function handleRegister() { return; } - // 检查用户名是否已存在 - const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]'); - if (users.find(u => u.username === username)) { - showToast('用户名已存在'); - return; - } - - // 检查手机号是否已注册 - if (users.find(u => u.phone === phone)) { - showToast('该手机号已注册'); - return; - } - - // 注册新用户 - const newUser = { - username, - phone, - email: email || '', - password, - registeredAt: Date.now() - }; - - users.push(newUser); - localStorage.setItem('registeredUsers', JSON.stringify(users)); - - // 自动登录 - currentUser = { username: newUser.username, phone: newUser.phone, registeredAt: newUser.registeredAt }; - saveCurrentUser(); - - showToast('注册成功'); - showMainPage(); + // 调用后台注册API + fetch('/api/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username, + phone, + email: email || '', + password, + code + }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + // 注册成功,自动登录 + currentUser = { + id: data.user_id, + username: username, + phone: phone, + email: email || '', + registeredAt: Date.now() + }; + saveCurrentUser(); + showToast('注册成功'); + showMainPage(); + } else { + showToast(data.error || '注册失败'); + } + }) + .catch(e => { + showToast('网络错误,请重试'); + }); } // ==================== 限制提示 ====================