增加登陆功能,并修改正式环境用5002端口

This commit is contained in:
2026-07-21 16:00:46 +08:00
parent 22f9bccb09
commit 07e88986ab
11 changed files with 691 additions and 8 deletions

View File

@@ -6,7 +6,7 @@
DEPLOY_RUN_HOST=0.0.0.0
# 服务监听端口
DEPLOY_RUN_PORT=5000
DEPLOY_RUN_PORT=5002
# 数据库配置(正式环境)
DB_HOST=192.168.10.250

60
app.py
View File

@@ -4,7 +4,7 @@ Flask 主应用
"""
import os
from datetime import datetime
from flask import Flask, render_template, Response
from flask import Flask, render_template, Response, request, jsonify, g
from flask_cors import CORS
from apscheduler.schedulers.background import BackgroundScheduler
import pytz
@@ -15,6 +15,7 @@ from lib.api import register_blueprints
from lib.db_init import init_database_tables
from lib.logger import log_info, log_error, log_warning
from lib.api.download_api import cleanup_old_temp_zips
from lib.api.auth_api import get_current_user
# 创建 Flask 应用
app = Flask(__name__,
@@ -36,6 +37,63 @@ else:
log_error('数据库表初始化失败,请检查数据库连接', 'app')
# ==================== 登录验证中间件 ====================
# 不需要登录的路径白名单
WHITELIST_PATHS = [
'/api/auth/login',
'/api/auth/info',
'/public/',
'/static/',
'/favicon.ico',
'/'
]
@app.before_request
def check_login():
"""全局登录验证"""
path = request.path
# 白名单路径直接放行
for wp in WHITELIST_PATHS:
if path.startswith(wp):
return None
# 页面请求HTML直接放行由前端控制登录
if path == '/' or path.endswith('.html'):
return None
# 静态文件直接放行
if path.startswith('/public/') or path.startswith('/static/'):
return None
# API 请求需要验证登录
if path.startswith('/api/'):
# 先从 header 取 token再从 query 参数取(用于下载链接)
token = request.headers.get('Authorization', '')
if token.startswith('Bearer '):
token = token[7:]
if not token:
token = request.args.get('token', '')
user = None
if token:
from lib.api.auth_api import get_user_by_token
user = get_user_by_token(token)
if not user:
return jsonify({
'success': False,
'message': '未登录或登录已过期',
'code': 401
}), 401
g.current_user = user
return None
# ==================== 页面路由 ====================
@app.route('/')

View File

@@ -24,6 +24,9 @@ download_bp = Blueprint('download', __name__, url_prefix='/api')
# 求和数据管理蓝图
sum_data_bp = Blueprint('sum_data', __name__, url_prefix='/api')
# 认证蓝图
from .auth_api import auth_bp
# 导入各模块的路由
from . import config_api, entity_api, field_api, report_api, init_api, download_api, sum_data_api
@@ -37,3 +40,4 @@ def register_blueprints(app):
app.register_blueprint(init_bp)
app.register_blueprint(download_bp)
app.register_blueprint(sum_data_bp)
app.register_blueprint(auth_bp)

221
lib/api/auth_api.py Normal file
View File

@@ -0,0 +1,221 @@
"""
认证相关 API
登录、退出、修改密码
"""
import hashlib
import secrets
from datetime import datetime, timedelta
from flask import Blueprint, request, jsonify, g
from lib.db import execute_query, execute_update
from lib.logger import log_info, log_error, log_warning
auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')
def md5_password(password):
"""MD5加密密码"""
return hashlib.md5(password.encode('utf-8')).hexdigest()
def generate_token():
"""生成登录令牌"""
return secrets.token_hex(32)
def get_user_by_token(token):
"""根据token获取用户信息"""
if not token:
return None
try:
user = execute_query(
"SELECT id, username, real_name, token_expire FROM t_daily_report_admin WHERE token = %s",
(token,)
)
if user and user[0]:
user_info = user[0]
if user_info.get('token_expire') and user_info['token_expire'] > datetime.now():
return user_info
except Exception as e:
log_warning(f"[认证] 获取用户信息失败: {e}", 'auth')
return None
def get_current_user():
"""获取当前登录用户从token"""
token = request.headers.get('Authorization', '')
if token.startswith('Bearer '):
token = token[7:]
if not token:
return None
try:
user = execute_query(
"SELECT id, username, real_name, token_expire FROM t_daily_report_admin WHERE token = %s",
(token,)
)
if user and user[0]:
user_info = user[0]
if user_info.get('token_expire') and user_info['token_expire'] > datetime.now():
return user_info
except Exception as e:
log_warning(f"[认证] 获取用户信息失败: {e}", 'auth')
return None
def login_required(f):
"""登录验证装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
user = get_current_user()
if not user:
return jsonify({'success': False, 'message': '未登录或登录已过期', 'code': 401}), 401
g.current_user = user
return f(*args, **kwargs)
return decorated_function
@auth_bp.route('/login', methods=['POST'])
def login():
"""登录"""
try:
data = request.get_json()
username = data.get('username', '').strip()
password = data.get('password', '').strip()
if not username or not password:
return jsonify({'success': False, 'message': '用户名和密码不能为空'})
# 查询用户
users = execute_query(
"SELECT id, username, password, real_name FROM t_daily_report_admin WHERE username = %s",
(username,)
)
if not users or not users[0]:
log_warning(f"[登录] 用户不存在: {username}", 'auth')
return jsonify({'success': False, 'message': '用户名或密码错误'})
user = users[0]
# 验证密码
password_md5 = md5_password(password)
if user['password'] != password_md5:
log_warning(f"[登录] 密码错误: {username}", 'auth')
return jsonify({'success': False, 'message': '用户名或密码错误'})
# 生成token有效期7天
token = generate_token()
token_expire = datetime.now() + timedelta(days=7)
# 获取客户端IP
client_ip = request.remote_addr
# 更新用户token和登录信息
execute_update(
"""UPDATE t_daily_report_admin
SET token = %s, token_expire = %s, last_login_time = NOW(), last_login_ip = %s
WHERE id = %s""",
(token, token_expire, client_ip, user['id'])
)
log_info(f"[登录] 登录成功: {username}", 'auth')
return jsonify({
'success': True,
'message': '登录成功',
'data': {
'token': token,
'username': user['username'],
'real_name': user.get('real_name', ''),
'expires_in': 7 * 24 * 3600
}
})
except Exception as e:
log_error(f"[登录] 异常: {e}", 'auth', exc_info=True)
return jsonify({'success': False, 'message': '登录失败: ' + str(e)})
@auth_bp.route('/logout', methods=['POST'])
@login_required
def logout():
"""退出登录"""
try:
user = g.current_user
# 清除token
execute_update(
"UPDATE t_daily_report_admin SET token = NULL, token_expire = NULL WHERE id = %s",
(user['id'],)
)
log_info(f"[退出] 用户退出登录: {user['username']}", 'auth')
return jsonify({'success': True, 'message': '退出成功'})
except Exception as e:
log_error(f"[退出] 异常: {e}", 'auth', exc_info=True)
return jsonify({'success': False, 'message': '退出失败: ' + str(e)})
@auth_bp.route('/info', methods=['GET'])
@login_required
def get_user_info():
"""获取当前用户信息"""
try:
user = g.current_user
return jsonify({
'success': True,
'data': {
'id': user['id'],
'username': user['username'],
'real_name': user.get('real_name', '')
}
})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@auth_bp.route('/change-password', methods=['POST'])
@login_required
def change_password():
"""修改密码"""
try:
data = request.get_json()
old_password = data.get('old_password', '').strip()
new_password = data.get('new_password', '').strip()
if not old_password or not new_password:
return jsonify({'success': False, 'message': '旧密码和新密码不能为空'})
if len(new_password) < 6:
return jsonify({'success': False, 'message': '新密码长度不能少于6位'})
user = g.current_user
# 验证旧密码
users = execute_query(
"SELECT password FROM t_daily_report_admin WHERE id = %s",
(user['id'],)
)
if not users or not users[0]:
return jsonify({'success': False, 'message': '用户不存在'})
old_password_md5 = md5_password(old_password)
if users[0]['password'] != old_password_md5:
return jsonify({'success': False, 'message': '旧密码错误'})
# 更新密码
new_password_md5 = md5_password(new_password)
execute_update(
"UPDATE t_daily_report_admin SET password = %s, token = NULL, token_expire = NULL, update_time = NOW() WHERE id = %s",
(new_password_md5, user['id'])
)
log_info(f"[修改密码] 用户 {user['username']} 修改密码成功", 'auth')
return jsonify({'success': True, 'message': '密码修改成功,请重新登录'})
except Exception as e:
log_error(f"[修改密码] 异常: {e}", 'auth', exc_info=True)
return jsonify({'success': False, 'message': '修改失败: ' + str(e)})

View File

@@ -180,6 +180,47 @@ def init_database_tables():
log_info("[数据库] 配置 sort_order 初始化完成", 'db_init')
except Exception as e:
log_warning(f"[数据库] 初始化 sort_order 时出错: {e}", 'db_init')
# 创建管理员用户表
create_admin_table = """
CREATE TABLE IF NOT EXISTS t_daily_report_admin (
id BIGINT NOT NULL COMMENT '用户 ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(200) NOT NULL COMMENT '密码(加密存储)',
real_name VARCHAR(50) NULL COMMENT '真实姓名',
token VARCHAR(200) NULL COMMENT '登录令牌',
token_expire DATETIME NULL COMMENT '令牌过期时间',
last_login_time DATETIME NULL COMMENT '最后登录时间',
last_login_ip VARCHAR(50) NULL COMMENT '最后登录IP',
create_time DATETIME NULL COMMENT '创建时间',
update_time DATETIME NULL COMMENT '更新时间'
)
UNIQUE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES("replication_num" = "1")
"""
execute_update(create_admin_table)
log_info("[数据库] t_daily_report_admin 表已就绪", 'db_init')
# 初始化默认管理员账号
try:
admin_count = execute_query(
"SELECT COUNT(*) as cnt FROM t_daily_report_admin WHERE username = 'admin'"
)
if not admin_count or admin_count[0]['cnt'] == 0:
log_info("[数据库] 初始化默认管理员账号 admin/123586", 'db_init')
import hashlib
default_password = hashlib.md5('123586'.encode('utf-8')).hexdigest()
admin_id = int(__import__('datetime').datetime.now().timestamp() * 1000)
execute_update(
"""INSERT INTO t_daily_report_admin
(id, username, password, real_name, create_time, update_time)
VALUES (%s, %s, %s, %s, NOW(), NOW())""",
(admin_id, 'admin', default_password, '管理员')
)
log_info("[数据库] 默认管理员账号创建成功", 'db_init')
except Exception as e:
log_warning(f"[数据库] 初始化默认管理员账号时出错: {e}", 'db_init')
log_info("[数据库] 初始化完成", 'db_init')
return True

View File

@@ -867,6 +867,117 @@ color: #666;
opacity: 0.5;
}
/* ========== 登录页面 ========== */
.login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.login-box {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 400px;
max-width: 90%;
}
.login-box h2 {
text-align: center;
color: #333;
margin-bottom: 8px;
font-size: 24px;
}
.login-subtitle {
text-align: center;
color: #888;
margin-bottom: 30px;
font-size: 14px;
}
.login-box .form-group {
margin-bottom: 20px;
}
.login-box label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
.login-box input {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.3s;
}
.login-box input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.btn-block {
width: 100%;
padding: 12px;
font-size: 16px;
margin-top: 10px;
}
.login-tip {
text-align: center;
color: #aaa;
font-size: 12px;
margin-top: 20px;
}
/* 顶部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
font-size: 14px;
color: #555;
}
.user-info span {
font-weight: 500;
}
.btn-link {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
padding: 4px 8px;
font-size: 14px;
}
.btn-link:hover {
text-decoration: underline;
}
.page-btn.active {
background: var(--primary);
color: white;

View File

@@ -16,6 +16,199 @@ let entityPageSize = 20;
let selectedEntityIds = new Set();
let fieldCustomNames = {};
// ============== 登录相关 ==============
const AUTH_TOKEN_KEY = 'daily_report_auth_token';
const AUTH_USER_KEY = 'daily_report_auth_user';
// 获取token
function getToken() {
return localStorage.getItem(AUTH_TOKEN_KEY) || '';
}
// 保存登录状态
function setAuth(token, user) {
localStorage.setItem(AUTH_TOKEN_KEY, token);
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(user));
}
// 清除登录状态
function clearAuth() {
localStorage.removeItem(AUTH_TOKEN_KEY);
localStorage.removeItem(AUTH_USER_KEY);
}
// 检查是否已登录
function isLoggedIn() {
return !!getToken();
}
// 获取当前用户
function getCurrentUser() {
const userStr = localStorage.getItem(AUTH_USER_KEY);
return userStr ? JSON.parse(userStr) : null;
}
// 全局拦截 fetch自动加 token
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
const token = getToken();
if (token) {
options.headers = options.headers || {};
options.headers['Authorization'] = 'Bearer ' + token;
}
return originalFetch(url, options).then(response => {
if (response.status === 401) {
clearAuth();
showLoginPage();
}
return response;
});
};
// 显示登录页
function showLoginPage() {
document.getElementById('login-overlay').style.display = 'flex';
document.getElementById('main-container').style.display = 'none';
document.getElementById('login-password').value = '';
document.getElementById('login-password').focus();
}
// 隐藏登录页
function hideLoginPage() {
document.getElementById('login-overlay').style.display = 'none';
document.getElementById('main-container').style.display = 'block';
const user = getCurrentUser();
if (user) {
document.getElementById('user-display').textContent = user.real_name || user.username;
}
}
// 登录
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value.trim();
if (!username || !password) {
alert('请输入用户名和密码');
return;
}
const btn = document.getElementById('login-btn');
btn.disabled = true;
btn.textContent = '登录中...';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await response.json();
if (result.success) {
setAuth(result.data.token, {
username: result.data.username,
real_name: result.data.real_name
});
hideLoginPage();
// 登录成功后加载数据
loadConfigs();
} else {
alert(result.message || '登录失败');
}
} catch (error) {
alert('登录失败: ' + error.message);
} finally {
btn.disabled = false;
btn.textContent = '登 录';
}
}
// 退出登录
async function handleLogout() {
if (!confirm('确定要退出登录吗?')) return;
try {
await fetch('/api/auth/logout', { method: 'POST' });
} catch (e) {
// 忽略错误
}
clearAuth();
showLoginPage();
}
// 修改密码弹窗
function showChangePasswordModal() {
document.getElementById('old-password').value = '';
document.getElementById('new-password').value = '';
document.getElementById('confirm-password').value = '';
document.getElementById('change-password-modal').style.display = 'block';
}
function closeChangePasswordModal() {
document.getElementById('change-password-modal').style.display = 'none';
}
async function saveChangePassword() {
const oldPassword = document.getElementById('old-password').value.trim();
const newPassword = document.getElementById('new-password').value.trim();
const confirmPassword = document.getElementById('confirm-password').value.trim();
if (!oldPassword || !newPassword || !confirmPassword) {
alert('请填写完整信息');
return;
}
if (newPassword.length < 6) {
alert('新密码长度不能少于6位');
return;
}
if (newPassword !== confirmPassword) {
alert('两次输入的新密码不一致');
return;
}
try {
const response = await fetch('/api/auth/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
old_password: oldPassword,
new_password: newPassword
})
});
const result = await response.json();
if (result.success) {
alert('密码修改成功,请重新登录');
closeChangePasswordModal();
clearAuth();
showLoginPage();
} else {
alert(result.message || '修改失败');
}
} catch (error) {
alert('修改失败: ' + error.message);
}
}
// 页面加载时检查登录状态
document.addEventListener('DOMContentLoaded', function() {
if (isLoggedIn()) {
hideLoginPage();
} else {
showLoginPage();
}
});
// 标签切换
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));

View File

@@ -209,7 +209,7 @@ function renderHistory(list) {
<td><span class="${statusClass}">${statusText}</span></td>
<td>
${item.status === 1 && item.file_path ?
`<a href="/api/download?path=${encodeURIComponent(item.file_path)}" class="btn btn-sm btn-success">下载</a>` :
`<a href="/api/download?path=${encodeURIComponent(item.file_path)}&token=${encodeURIComponent(getToken())}" class="btn btn-sm btn-success">下载</a>` :
'-'}
<button class="btn btn-sm btn-danger" onclick="deleteHistory(${item.id})">删除</button>
</td>

View File

@@ -7,7 +7,7 @@ from waitress import serve
from app import app
if __name__ == '__main__':
port = int(os.environ.get('DEPLOY_RUN_PORT', 5000))
port = int(os.environ.get('DEPLOY_RUN_PORT', 5002))
host = os.environ.get('DEPLOY_RUN_HOST', '0.0.0.0')
print(f'订单日报系统 - 生产模式')

View File

@@ -7,8 +7,35 @@
<link rel="stylesheet" href="/public/static/css/style.css?v=2026071507">
</head>
<body>
<div class="container">
<h1>订单日报系统</h1>
<!-- 登录遮罩层 -->
<div id="login-overlay" class="login-overlay">
<div class="login-box">
<h2>订单日报系统</h2>
<p class="login-subtitle">请登录以继续</p>
<form id="login-form" onsubmit="handleLogin(event)">
<div class="form-group">
<label>用户名</label>
<input type="text" id="login-username" placeholder="请输入用户名" value="admin">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="login-password" placeholder="请输入密码">
</div>
<button type="submit" class="btn btn-primary btn-block" id="login-btn">登 录</button>
</form>
<p class="login-tip">默认账号admin / 123586</p>
</div>
</div>
<div class="container" id="main-container" style="display: none;">
<div class="header-bar">
<h1>订单日报系统</h1>
<div class="user-info">
<span id="user-display">admin</span>
<button class="btn btn-link" onclick="showChangePasswordModal()">修改密码</button>
<button class="btn btn-link" onclick="handleLogout()">退出登录</button>
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('config')">配置管理</button>
@@ -368,8 +395,36 @@
</div>
</div>
<!-- 修改密码弹窗 -->
<div id="change-password-modal" class="modal">
<div class="modal-content" style="max-width: 450px;">
<div class="modal-header">
<h2>修改密码</h2>
<span class="close" onclick="closeChangePasswordModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label>旧密码 <span style="color: red;">*</span></label>
<input type="password" id="old-password" class="form-control" placeholder="请输入旧密码">
</div>
<div class="form-group">
<label>新密码 <span style="color: red;">*</span></label>
<input type="password" id="new-password" class="form-control" placeholder="请输入新密码至少6位">
</div>
<div class="form-group">
<label>确认新密码 <span style="color: red;">*</span></label>
<input type="password" id="confirm-password" class="form-control" placeholder="请再次输入新密码">
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeChangePasswordModal()">取消</button>
<button class="btn btn-primary" onclick="saveChangePassword()">保存</button>
</div>
</div>
</div>
<!-- JavaScript 文件 -->
<script src="/public/static/js/common.js?v=2026071507"></script>
<script src="/public/static/js/common.js?v=2026072102"></script>
<script src="/public/static/js/config.js?v=2026071507"></script>
<script src="/public/static/js/entity.js?v=2026071507"></script>
<script src="/public/static/js/report.js?v=2026072103"></script>

View File

@@ -114,7 +114,7 @@ REM ============================================
echo.
echo [5/5] 启动服务...
echo.
echo [信息] 服务启动后,访问 http://localhost:5000
echo [信息] 服务启动后,访问 http://localhost:5002
echo [信息] 按 Ctrl+C 停止服务
echo.
echo ========================================