增加登陆功能,并修改正式环境用5002端口
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
DEPLOY_RUN_HOST=0.0.0.0
|
DEPLOY_RUN_HOST=0.0.0.0
|
||||||
|
|
||||||
# 服务监听端口
|
# 服务监听端口
|
||||||
DEPLOY_RUN_PORT=5000
|
DEPLOY_RUN_PORT=5002
|
||||||
|
|
||||||
# 数据库配置(正式环境)
|
# 数据库配置(正式环境)
|
||||||
DB_HOST=192.168.10.250
|
DB_HOST=192.168.10.250
|
||||||
|
|||||||
60
app.py
60
app.py
@@ -4,7 +4,7 @@ Flask 主应用
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
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 flask_cors import CORS
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
import pytz
|
import pytz
|
||||||
@@ -15,6 +15,7 @@ from lib.api import register_blueprints
|
|||||||
from lib.db_init import init_database_tables
|
from lib.db_init import init_database_tables
|
||||||
from lib.logger import log_info, log_error, log_warning
|
from lib.logger import log_info, log_error, log_warning
|
||||||
from lib.api.download_api import cleanup_old_temp_zips
|
from lib.api.download_api import cleanup_old_temp_zips
|
||||||
|
from lib.api.auth_api import get_current_user
|
||||||
|
|
||||||
# 创建 Flask 应用
|
# 创建 Flask 应用
|
||||||
app = Flask(__name__,
|
app = Flask(__name__,
|
||||||
@@ -36,6 +37,63 @@ else:
|
|||||||
log_error('数据库表初始化失败,请检查数据库连接', 'app')
|
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('/')
|
@app.route('/')
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ download_bp = Blueprint('download', __name__, url_prefix='/api')
|
|||||||
# 求和数据管理蓝图
|
# 求和数据管理蓝图
|
||||||
sum_data_bp = Blueprint('sum_data', __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
|
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(init_bp)
|
||||||
app.register_blueprint(download_bp)
|
app.register_blueprint(download_bp)
|
||||||
app.register_blueprint(sum_data_bp)
|
app.register_blueprint(sum_data_bp)
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
|||||||
221
lib/api/auth_api.py
Normal file
221
lib/api/auth_api.py
Normal 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)})
|
||||||
@@ -181,6 +181,47 @@ def init_database_tables():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_warning(f"[数据库] 初始化 sort_order 时出错: {e}", 'db_init')
|
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')
|
log_info("[数据库] 初始化完成", 'db_init')
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -867,6 +867,117 @@ color: #666;
|
|||||||
opacity: 0.5;
|
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 {
|
.page-btn.active {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -16,6 +16,199 @@ let entityPageSize = 20;
|
|||||||
let selectedEntityIds = new Set();
|
let selectedEntityIds = new Set();
|
||||||
let fieldCustomNames = {};
|
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) {
|
function switchTab(tabName) {
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ function renderHistory(list) {
|
|||||||
<td><span class="${statusClass}">${statusText}</span></td>
|
<td><span class="${statusClass}">${statusText}</span></td>
|
||||||
<td>
|
<td>
|
||||||
${item.status === 1 && item.file_path ?
|
${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>
|
<button class="btn btn-sm btn-danger" onclick="deleteHistory(${item.id})">删除</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from waitress import serve
|
|||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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')
|
host = os.environ.get('DEPLOY_RUN_HOST', '0.0.0.0')
|
||||||
|
|
||||||
print(f'订单日报系统 - 生产模式')
|
print(f'订单日报系统 - 生产模式')
|
||||||
|
|||||||
@@ -7,8 +7,35 @@
|
|||||||
<link rel="stylesheet" href="/public/static/css/style.css?v=2026071507">
|
<link rel="stylesheet" href="/public/static/css/style.css?v=2026071507">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<!-- 登录遮罩层 -->
|
||||||
|
<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>
|
<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">
|
<div class="tabs">
|
||||||
<button class="tab active" onclick="switchTab('config')">配置管理</button>
|
<button class="tab active" onclick="switchTab('config')">配置管理</button>
|
||||||
@@ -368,8 +395,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</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()">×</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 文件 -->
|
<!-- 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/config.js?v=2026071507"></script>
|
||||||
<script src="/public/static/js/entity.js?v=2026071507"></script>
|
<script src="/public/static/js/entity.js?v=2026071507"></script>
|
||||||
<script src="/public/static/js/report.js?v=2026072103"></script>
|
<script src="/public/static/js/report.js?v=2026072103"></script>
|
||||||
|
|||||||
2
启动.bat
2
启动.bat
@@ -114,7 +114,7 @@ REM ============================================
|
|||||||
echo.
|
echo.
|
||||||
echo [5/5] 启动服务...
|
echo [5/5] 启动服务...
|
||||||
echo.
|
echo.
|
||||||
echo [信息] 服务启动后,访问 http://localhost:5000
|
echo [信息] 服务启动后,访问 http://localhost:5002
|
||||||
echo [信息] 按 Ctrl+C 停止服务
|
echo [信息] 按 Ctrl+C 停止服务
|
||||||
echo.
|
echo.
|
||||||
echo ========================================
|
echo ========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user