This commit is contained in:
2025-08-08 10:38:10 +08:00
commit c7dc4d6c02
114 changed files with 11010 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
from flask import render_template
from flask_login import login_required
def init_about_routes(app):
@app.route('/about')
@login_required
def about():
return render_template('about.html')
+26
View File
@@ -0,0 +1,26 @@
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_user, login_required, logout_user, current_user
from scripts.models import db, User
def init_auth_routes(app):
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
phone = request.form['phone']
user = User.query.filter_by(phone=phone).first()
if user:
login_user(user)
app.logger.info(f'用户 {user.name}({user.phone}) 登录成功')
# 无论是统计员还是装维员,都跳转到仪表盘
return redirect(url_for('dashboard'))
else:
app.logger.warning(f'登录失败:用户不存在 (手机号: {phone})')
flash('用户不存在')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
app.logger.info(f'用户 {current_user.name}({current_user.phone}) 登出')
logout_user()
return redirect(url_for('login'))
+197
View File
@@ -0,0 +1,197 @@
from flask import render_template, flash, redirect, url_for
from flask_login import login_required, current_user
from scripts.models import db, Device, WorkOrder
from collections import defaultdict
from sqlalchemy import func
from utils.db_utils import cache_query, db_operation
def init_dashboard_routes(app):
@app.route('/dashboard')
@login_required
def dashboard():
if current_user.role not in ['装维员', '统计员']:
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试访问仪表盘页面')
flash('您没有权限访问此页面')
return redirect(url_for('login'))
try:
app.logger.info(f'用户 {current_user.name} 访问仪表盘页面')
# 获取设备在线率数据
overall_stats = get_overall_device_stats()
branch_stats = get_branch_device_stats()
town_stats = get_town_device_stats()
return render_template('dashboard.html',
overall_stats=overall_stats,
branch_stats=branch_stats,
town_stats=town_stats)
except Exception as e:
app.logger.error(f"仪表盘页面加载失败: {str(e)}")
flash('数据加载失败,请稍后重试')
return redirect(url_for('login'))
@cache_query(ttl=300) # 缓存5分钟
def get_overall_device_stats():
"""获取总体设备在线率统计"""
stats = {}
# 获取设备类型列表
device_types = db.session.query(Device.device_type).distinct().all()
device_types = [d[0] for d in device_types if d[0]]
for device_type in device_types:
# 获取该类型的设备总数
total_devices = db.session.query(func.count(Device.device_id))\
.filter(Device.device_type == device_type)\
.scalar() or 0
# 获取该类型的派单数
work_orders = db.session.query(func.count(WorkOrder.order_id))\
.join(Device, WorkOrder.device_id == Device.device_id)\
.filter(Device.device_type == device_type)\
.scalar() or 0
# 计算在线率
if total_devices > 0:
online_rate = ((total_devices - work_orders) / total_devices) * 100
else:
online_rate = 0
stats[device_type] = {
'total': total_devices,
'work_orders': work_orders,
'online_rate': round(online_rate, 2)
}
return stats
@cache_query(ttl=300) # 缓存5分钟
def get_branch_device_stats():
"""获取各支局设备在线率统计"""
stats = {}
# 获取所有支局
branches = db.session.query(Device.branch).distinct().all()
branches = [b[0] for b in branches if b[0]]
# 获取设备类型列表
device_types = db.session.query(Device.device_type).distinct().all()
device_types = [d[0] for d in device_types if d[0]]
for branch in branches:
branch_stats = {}
for device_type in device_types:
# 获取该支局该类型的设备总数
total_devices = db.session.query(func.count(Device.device_id))\
.filter(Device.branch == branch, Device.device_type == device_type)\
.scalar() or 0
# 获取该支局该类型的派单数
work_orders = db.session.query(func.count(WorkOrder.order_id))\
.join(Device, WorkOrder.device_id == Device.device_id)\
.filter(Device.branch == branch, Device.device_type == device_type)\
.scalar() or 0
# 计算在线率
if total_devices > 0:
online_rate = ((total_devices - work_orders) / total_devices) * 100
else:
online_rate = 0
branch_stats[device_type] = {
'total': total_devices,
'work_orders': work_orders,
'online_rate': round(online_rate, 2)
}
# 计算该支局总体在线率
total_branch_devices = db.session.query(func.count(Device.device_id))\
.filter(Device.branch == branch)\
.scalar() or 0
total_branch_orders = db.session.query(func.count(WorkOrder.order_id))\
.join(Device, WorkOrder.device_id == Device.device_id)\
.filter(Device.branch == branch)\
.scalar() or 0
if total_branch_devices > 0:
branch_online_rate = ((total_branch_devices - total_branch_orders) / total_branch_devices) * 100
else:
branch_online_rate = 0
branch_stats['总计'] = {
'total': total_branch_devices,
'work_orders': total_branch_orders,
'online_rate': round(branch_online_rate, 2)
}
stats[branch] = branch_stats
return stats
@cache_query(ttl=300) # 缓存5分钟
def get_town_device_stats():
"""获取各乡镇设备在线率统计"""
stats = {}
# 获取所有乡镇
towns = db.session.query(Device.town).distinct().all()
towns = [t[0] for t in towns if t[0]]
# 获取设备类型列表
device_types = db.session.query(Device.device_type).distinct().all()
device_types = [d[0] for d in device_types if d[0]]
for town in towns:
town_stats = {}
for device_type in device_types:
# 获取该乡镇该类型的设备总数
total_devices = db.session.query(func.count(Device.device_id))\
.filter(Device.town == town, Device.device_type == device_type)\
.scalar() or 0
# 获取该乡镇该类型的派单数
work_orders = db.session.query(func.count(WorkOrder.order_id))\
.join(Device, WorkOrder.device_id == Device.device_id)\
.filter(Device.town == town, Device.device_type == device_type)\
.scalar() or 0
# 计算在线率
if total_devices > 0:
online_rate = ((total_devices - work_orders) / total_devices) * 100
else:
online_rate = 0
town_stats[device_type] = {
'total': total_devices,
'work_orders': work_orders,
'online_rate': round(online_rate, 2)
}
# 计算该乡镇总体在线率
total_town_devices = db.session.query(func.count(Device.device_id))\
.filter(Device.town == town)\
.scalar() or 0
total_town_orders = db.session.query(func.count(WorkOrder.order_id))\
.join(Device, WorkOrder.device_id == Device.device_id)\
.filter(Device.town == town)\
.scalar() or 0
if total_town_devices > 0:
town_online_rate = ((total_town_devices - total_town_orders) / total_town_devices) * 100
else:
town_online_rate = 0
town_stats['总计'] = {
'total': total_town_devices,
'work_orders': total_town_orders,
'online_rate': round(town_online_rate, 2)
}
stats[town] = town_stats
return stats
+297
View File
@@ -0,0 +1,297 @@
from flask import jsonify, request, send_file, session, current_app
from flask_login import login_required, current_user
from scripts.update.zte import LoginClient
from scripts.models import db, Device, WorkOrder, Channel
from datetime import datetime
from sqlalchemy import text, create_engine
import hashlib
import pymysql
import io
import os
def init_device_routes(app):
@app.route('/sync_offline_devices', methods=['POST'])
@login_required
def sync_offline_devices():
if current_user.role not in ['装维员', '统计员']:
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试同步离线设备')
return jsonify({'success': False, 'message': '没有权限执行此操作'})
try:
app.logger.info(f'用户 {current_user.name} 开始同步离线设备')
# 从配置中获取数据库连接信息
db_config = current_app.config['KEY110_DB']
conn = pymysql.connect(**db_config)
cursor = conn.cursor()
# 获取所有教学点的状态
cursor.execute("""
SELECT 教学点名称, 连通状态
FROM IP数据
""")
schools = cursor.fetchall()
# 分别处理在线和离线的教学点
offline_school_names = [school[0].strip().replace(' ', ' ') for school in schools if school[1] == '离线']
online_school_names = [school[0].strip().replace(' ', ' ') for school in schools if school[1] == '在线']
# 关闭1key110数据库连接
cursor.close()
conn.close()
# 处理恢复在线的设备
online_devices = Device.query.filter(
Device.device_id.in_(online_school_names),
Device.is_normal == False # 当前状态为故障的设备
).all()
# 删除恢复在线设备的在途工单
recovered_count = 0
for device in online_devices:
# 查找并删除该设备的在途工单
pending_orders = WorkOrder.query.filter_by(
device_id=device.device_id,
status='待处理'
).all()
for order in pending_orders:
db.session.delete(order)
recovered_count += 1
# 更新设备状态为正常
device.is_normal = True
db.session.add(device)
# 处理离线设备
offline_devices = Device.query.filter(
Device.device_id.in_(offline_school_names),
Device.in_use == True,
Device.is_normal == True
).all()
# 生成工单号
today = datetime.now().strftime('%Y%m%d')
last_work_order = WorkOrder.query.filter(
WorkOrder.order_id.like(f'{today}%')
).order_by(WorkOrder.order_id.desc()).first()
start_number = int(last_work_order.order_id[-4:]) + 1 if last_work_order else 1
# 创建工单
created_count = 0
for index, device in enumerate(offline_devices):
# 检查是否已有在途工单
existing_order = WorkOrder.query.filter_by(
device_id=device.device_id,
status='待处理'
).first()
if existing_order:
continue
current_number = start_number + index
new_number = f"{today}{str(current_number).zfill(4)}"
work_order = WorkOrder(
order_id=new_number,
device_id=device.device_id,
dispatch_time=datetime.now(),
status='待处理',
branch=device.branch
)
db.session.add(work_order)
device.is_normal = False
db.session.add(device)
created_count += 1
db.session.commit()
app.logger.info(f'同步完成:创建了 {created_count} 个工单,恢复了 {recovered_count} 个设备')
return jsonify({
'success': True,
'count': created_count
})
except Exception as e:
app.logger.error(f'同步离线设备失败:{str(e)}')
return jsonify({
'success': False,
'message': str(e)
})
@app.route('/check_existing_orders', methods=['POST'])
@login_required
def check_existing_orders():
device_ids = request.json.get('device_ids', [])
app.logger.info(f'用户 {current_user.name} 检查设备工单状态,设备数量:{len(device_ids)}')
# 查询设备状态
existing_devices = Device.query.filter(
Device.device_id.in_(device_ids),
Device.is_normal == False # 检查设备是否处于故障状态
).with_entities(Device.device_id).all()
# 提取设备ID列表
existing_device_ids = [device.device_id for device in existing_devices]
app.logger.info(f'发现 {len(existing_device_ids)} 个设备存在故障工单')
return jsonify({'existing_devices': existing_device_ids})
@app.route('/get_captcha', methods=['GET'])
def get_captcha():
app.logger.info('用户请求验证码')
try:
client = LoginClient()
cid = client.get_captcha()
# 将 cid 存储在 session 中供后续使用
session['zte_cid'] = cid
# 读取验证码图片并返回给前端
captcha_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'static', 'captcha.png')
with open(captcha_path, "rb") as f:
app.logger.info('验证码获取成功')
return send_file(
io.BytesIO(f.read()),
mimetype='image/png'
)
except Exception as e:
app.logger.error(f'获取验证码失败:{str(e)}')
return jsonify({'success': False, 'message': '获取验证码失败'})
@app.route('/sync_camera_step1', methods=['POST'])
def sync_camera_step1():
try:
app.logger.info(f'用户 {current_user.name if current_user else "未登录"} 开始同步摄像头第一步')
data = request.get_json()
captcha = data.get('captcha')
cid = session.get('zte_cid')
if not captcha or not cid:
app.logger.warning('同步摄像头失败:验证码或会话ID缺失')
return jsonify({'success': False, 'message': 'Invalid request'})
client = LoginClient()
# 登录
app.logger.info('尝试登录中兴平台')
login_result = client.login(
account="DHDX001",
password=hashlib.sha256("Root1234#".encode()).hexdigest(),
validcode=captcha,
cid=cid
)
if login_result.get("result") != 0:
app.logger.warning('登录中兴平台失败:验证码错误')
return jsonify({'success': False, 'message': 'INVALID_CAPTCHA'})
token = login_result.get("token")
app.logger.info('登录成功,开始获取在线信息')
# 获取在线信息
online_info = client.get_online_info(token)
if online_info.get("result") == 0:
rows = online_info.get("rows", [])
updated_count = 0
offline_count = 0
app.logger.info(f'获取到 {len(rows)} 个设备信息,开始更新数据库')
# 更新 channels 表
for row in rows:
db.session.execute(
text("""
INSERT INTO channels (puid, pu_name, status)
VALUES (:puid, :pu_name, :status)
ON DUPLICATE KEY UPDATE
pu_name = :pu_name, status = :status
"""),
{
'puid': row['puid'],
'pu_name': row['pu_name'],
'status': row['status']
}
)
updated_count += 1
if row['status'] != 'online':
offline_count += 1
db.session.commit()
app.logger.info(f'数据库更新完成,更新了 {updated_count} 条记录,其中离线设备 {offline_count}')
return jsonify({
'success': True,
'updated_count': updated_count,
'offline_count': offline_count
})
app.logger.error('获取在线信息失败')
return jsonify({'success': False, 'message': 'Failed to get online info'})
except Exception as e:
app.logger.error(f'同步摄像头步骤1失败:{str(e)}')
return jsonify({'success': False, 'message': str(e)})
@app.route('/sync_camera_step2', methods=['POST'])
def sync_camera_step2():
try:
app.logger.info(f'用户 {current_user.name if current_user else "未登录"} 开始同步摄像头第二步')
# 更新设备状态
devices = Device.query.filter(
Device.puid.isnot(None),
Device.in_use == True # 添加这个条件,只检查在用设备
).all()
app.logger.info(f'查询到 {len(devices)} 个需要同步的设备')
offline_devices = []
for device in devices:
channel = db.session.query(Channel).filter_by(puid=device.puid).first()
if channel and channel.status != 'online':
device.is_normal = 0
# 检查是否已有在途工单
existing_order = WorkOrder.query.filter_by(
device_id=device.device_id,
status='待处理'
).first()
if not existing_order: # 只添加没有在途工单的设备
offline_devices.append(device)
db.session.commit()
app.logger.info(f'发现 {len(offline_devices)} 个离线设备需要创建工单')
# 为离线设备创建工单
created_count = 0
if offline_devices:
today = datetime.now().strftime('%Y%m%d')
last_work_order = WorkOrder.query.filter(
WorkOrder.order_id.like(f'{today}%')
).order_by(WorkOrder.order_id.desc()).first()
start_number = int(last_work_order.order_id[-4:]) + 1 if last_work_order else 1
for index, device in enumerate(offline_devices):
order_id = f"{today}{str(start_number + index).zfill(4)}"
work_order = WorkOrder(
order_id=order_id,
device_id=device.device_id,
status='待处理',
dispatch_time=datetime.now(),
branch=device.branch # 添加支局信息
)
db.session.add(work_order)
created_count += 1
db.session.commit()
app.logger.info(f'成功创建 {created_count} 个工单')
return jsonify({
'success': True,
'count': created_count
})
except Exception as e:
app.logger.error(f'同步摄像头步骤2失败:{str(e)}')
return jsonify({'success': False, 'message': str(e)})
+133
View File
@@ -0,0 +1,133 @@
from flask import render_template, request, flash, redirect, url_for
from flask_login import login_required, current_user
from scripts.models import db, WorkOrderHandling, ManualWorkOrder
from datetime import datetime
def init_history_routes(app):
@app.route('/history')
@login_required
def history():
if current_user.role not in ['装维员', '统计员']:
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试访问历史记录')
flash('您没有权限访问此页面')
return redirect(url_for('statistics'))
app.logger.info(f'用户 {current_user.name} 访问历史记录页面')
page = request.args.get('page', 1, type=int)
per_page = 20
search = request.args.get('search', '')
# 查询普通工单
if current_user.role == '统计员':
query1 = WorkOrderHandling.query
query2 = ManualWorkOrder.query
else:
# 装维员只能看到自己处理的工单
query1 = WorkOrderHandling.query.filter_by(handler=current_user.name)
query2 = ManualWorkOrder.query.filter_by(handler=current_user.name)
# 添加搜索条件
if search:
query1 = query1.filter(
db.or_(
WorkOrderHandling.order_id.contains(search),
WorkOrderHandling.device_id.contains(search),
WorkOrderHandling.branch.contains(search),
WorkOrderHandling.handler.contains(search),
WorkOrderHandling.fault_type.contains(search),
WorkOrderHandling.handle_description.contains(search),
WorkOrderHandling.contact_person.contains(search),
WorkOrderHandling.remark.contains(search)
)
)
query2 = query2.filter(
db.or_(
ManualWorkOrder.order_id.contains(search),
ManualWorkOrder.device_id.contains(search),
ManualWorkOrder.branch.contains(search),
ManualWorkOrder.handler.contains(search),
ManualWorkOrder.fault_type.contains(search),
ManualWorkOrder.handle_description.contains(search),
ManualWorkOrder.contact_person.contains(search),
ManualWorkOrder.remark.contains(search)
)
)
# 合并两种工单并按时间排序
handlings1 = query1.all()
handlings2 = query2.all()
# 将手工故障单转换为与普通工单相同的格式
combined_handlings = []
for h in handlings1:
combined_handlings.append({
'order_id': h.order_id,
'branch': h.branch,
'device_id': h.device_id,
'handler': h.handler,
'time': h.restore_time,
'fault_type': h.fault_type,
'type': 'normal'
})
for h in handlings2:
combined_handlings.append({
'order_id': h.order_id,
'branch': h.branch,
'device_id': h.device_id,
'handler': h.handler,
'time': h.create_time,
'fault_type': h.fault_type,
'type': 'manual'
})
# 按时间排序
combined_handlings.sort(key=lambda x: x['time'], reverse=True)
# 手动分页
total = len(combined_handlings)
start = (page - 1) * per_page
end = start + per_page
current_page = combined_handlings[start:end]
# 创建分页对象
class Pagination:
def __init__(self, items, page, per_page, total):
self.items = items
self.page = page
self.per_page = per_page
self.total = total
self.pages = (total + per_page - 1) // per_page
self.has_prev = page > 1
self.has_next = page < self.pages
self.prev_num = page - 1
self.next_num = page + 1
handlings = Pagination(current_page, page, per_page, total)
return render_template('history.html', handlings=handlings, search=search)
@app.route('/history/<string:order_id>')
@login_required
def history_detail(order_id):
if current_user.role not in ['装维员', '统计员']:
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试查看工单详情 {order_id}')
flash('您没有权限访问此页面')
return redirect(url_for('statistics'))
app.logger.info(f'用户 {current_user.name} 查看工单 {order_id} 的详情')
# 根据工单号前缀判断是普通工单还是手工故障单
if order_id.startswith('M'):
handling = ManualWorkOrder.query.get_or_404(order_id)
else:
handling = WorkOrderHandling.query.filter_by(order_id=order_id).first_or_404()
# 装维员只能查看自己处理的工单
if current_user.role == '装维员' and handling.handler != current_user.name:
flash('您没有权限查看此工单')
return redirect(url_for('history'))
# 传递工单类型到模板
order_type = 'manual' if order_id.startswith('M') else 'normal'
return render_template('history_detail.html', handling=handling, order_type=order_type)
+85
View File
@@ -0,0 +1,85 @@
from flask import render_template, request, jsonify, flash, redirect, url_for
from flask_login import login_required, current_user
from scripts.models import db, Device, ManualWorkOrder
from datetime import datetime
import os
from utils.file_utils import allowed_file, compress_image
from utils.lzmx import send_manual_lzmx_message
from config import Config
def init_manual_routes(app):
@app.route('/manual', methods=['GET', 'POST'])
@login_required
def manual():
if current_user.role not in ['装维员', '统计员']:
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试访问手工故障单页面')
flash('您没有权限访问此页面')
return redirect(url_for('statistics'))
if request.method == 'POST':
app.logger.info(f'用户 {current_user.name} 开始创建手工故障单')
# 处理文件上传
photo_paths = []
if 'photo' in request.files:
files = request.files.getlist('photo')
if len(files) > 5:
flash('最多只能上传5张图片')
return redirect(url_for('manual'))
for file in files:
if file and allowed_file(file.filename, Config.ALLOWED_EXTENSIONS): # 修改这里
filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{file.filename}"
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
compress_image(file_path)
photo_paths.append(filename)
# 生成工单号
today = datetime.now().strftime('%Y%m%d')
last_order = ManualWorkOrder.query.filter(
ManualWorkOrder.order_id.like(f'M{today}%')
).order_by(ManualWorkOrder.order_id.desc()).first()
if last_order:
last_number = int(last_order.order_id[-4:])
new_number = f"M{today}{str(last_number + 1).zfill(4)}"
else:
new_number = f"M{today}0001"
# 创建手工故障单
manual_order = ManualWorkOrder(
order_id=new_number,
project_type=request.form['project_type'],
device_id=request.form['device_id'],
device_location=request.form['device_location'],
fault_time=datetime.strptime(request.form['fault_time'], '%Y-%m-%dT%H:%M'),
handler=request.form['handler'],
fault_type=request.form['fault_type'],
handle_description=request.form['handle_description'],
photo_paths=','.join(photo_paths) if photo_paths else None,
contact_person=request.form['contact_person'],
contact_phone=request.form['contact_phone'],
distance_from_branch=request.form['distance_from_branch'],
remark=request.form.get('remark', ''),
branch=current_user.branch
)
db.session.add(manual_order)
db.session.commit()
# 发送量子密信通知
send_manual_lzmx_message(
Config.LZMX_WEBHOOK_URL, # 修改这里
request.form['handler'],
request.form['device_id'],
request.form['fault_type'],
request.form['distance_from_branch'],
manual_order.fault_time,
request.form['project_type'],
request.form['device_location']
)
app.logger.info(f'用户 {current_user.name} 创建手工故障单成功:{new_number}')
flash('手工故障单创建成功')
return redirect(url_for('history'))
return render_template('manual.html')
+325
View File
@@ -0,0 +1,325 @@
from flask import render_template, flash, redirect, url_for, send_file
from flask_login import login_required, current_user
from scripts.models import db, WorkOrder, WorkOrderHandling, ManualWorkOrder
from datetime import datetime, timedelta
from collections import defaultdict
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
from utils.reward import get_reward_by_distance
from scripts.models import Device
import io
def init_statistics_routes(app):
@app.route('/statistics')
@login_required
def statistics():
if current_user.role not in ['装维员', '统计员']:
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试访问统计页面')
flash('您没有权限访问此页面')
return redirect(url_for('login'))
app.logger.info(f'用户 {current_user.name} 访问统计页面')
# 获取当前时间
now = datetime.now()
# 计算本周开始和结束时间
week_start = now - timedelta(days=now.weekday())
week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
week_end = week_start + timedelta(days=7)
# 计算本月开始和结束时间
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if now.month == 12:
month_end = now.replace(year=now.year + 1, month=1, day=1)
else:
month_end = now.replace(month=now.month + 1, day=1)
# 统计普通工单
def get_work_order_stats(start_time, end_time):
total = WorkOrder.query.filter(
WorkOrder.dispatch_time >= start_time,
WorkOrder.dispatch_time < end_time
).count()
completed = WorkOrder.query.filter(
WorkOrder.dispatch_time >= start_time,
WorkOrder.dispatch_time < end_time,
WorkOrder.status == '已处理'
).count()
pending = total - completed
return {'total': total, 'completed': completed, 'pending': pending}
# 统计手工故障单
def get_manual_order_stats(start_time, end_time):
total = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= start_time,
ManualWorkOrder.create_time < end_time
).count()
return total
# 计算奖励金额
def calculate_reward(start_time, end_time):
rewards = defaultdict(float)
# 查询所有已处理的工单
handlings = WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= start_time,
WorkOrderHandling.restore_time < end_time
).all()
# 查询所有手工故障单
manual_orders = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= start_time,
ManualWorkOrder.create_time < end_time
).all()
# 计算普通工单奖励
for handling in handlings:
device = Device.query.get(handling.device_id)
if device:
distance = float(handling.distance_from_branch)
reward = get_reward_by_distance(device.project_type, distance)
rewards[device.project_type] += reward
# 计算手工故障单奖励
for order in manual_orders:
distance = float(order.distance_from_branch)
reward = get_reward_by_distance(order.project_type, distance)
rewards[order.project_type] += reward
return rewards
def get_handler_stats(start_time, end_time):
handler_stats = defaultdict(lambda: {
'work_orders': 0,
'manual_orders': 0,
'rewards': defaultdict(float)
})
# 统计普通工单
handlings = WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= start_time,
WorkOrderHandling.restore_time < end_time
).all()
for handling in handlings:
handler = handling.handler
handler_stats[handler]['work_orders'] += 1
device = Device.query.get(handling.device_id)
if device:
distance = float(handling.distance_from_branch)
reward = get_reward_by_distance(device.project_type, distance)
handler_stats[handler]['rewards'][device.project_type] += reward
# 统计手工故障单
manual_orders = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= start_time,
ManualWorkOrder.create_time < end_time
).all()
for order in manual_orders:
handler = order.handler
handler_stats[handler]['manual_orders'] += 1
distance = float(order.distance_from_branch)
reward = get_reward_by_distance(order.project_type, distance)
handler_stats[handler]['rewards'][order.project_type] += reward
return handler_stats
# 获取统计数据
week_work_stats = get_work_order_stats(week_start, week_end)
month_work_stats = get_work_order_stats(month_start, month_end)
week_manual_stats = get_manual_order_stats(week_start, week_end)
month_manual_stats = get_manual_order_stats(month_start, month_end)
week_rewards = calculate_reward(week_start, week_end)
month_rewards = calculate_reward(month_start, month_end)
week_handler_stats = get_handler_stats(week_start, week_end)
month_handler_stats = get_handler_stats(month_start, month_end)
# 如果是装维员,只显示自己的数据
if current_user.role == '装维员':
week_work_stats = {
'completed': WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= week_start,
WorkOrderHandling.restore_time < week_end,
WorkOrderHandling.handler == current_user.name
).count()
}
month_work_stats = {
'completed': WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= month_start,
WorkOrderHandling.restore_time < month_end,
WorkOrderHandling.handler == current_user.name
).count()
}
week_manual_stats = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= week_start,
ManualWorkOrder.create_time < week_end,
ManualWorkOrder.handler == current_user.name
).count()
month_manual_stats = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= month_start,
ManualWorkOrder.create_time < month_end,
ManualWorkOrder.handler == current_user.name
).count()
# 修改这里:只获取当前装维员的奖励统计
week_rewards = calculate_reward(week_start, week_end)
month_rewards = calculate_reward(month_start, month_end)
week_handler_stats = {current_user.name: week_handler_stats.get(current_user.name, defaultdict(int))}
month_handler_stats = {current_user.name: month_handler_stats.get(current_user.name, defaultdict(int))}
# 从 handler_stats 中提取当前用户的奖励数据
week_rewards = week_handler_stats[current_user.name]['rewards']
month_rewards = month_handler_stats[current_user.name]['rewards']
return render_template('statistics.html',
week_work_stats=week_work_stats,
month_work_stats=month_work_stats,
week_manual_stats=week_manual_stats,
month_manual_stats=month_manual_stats,
week_rewards=week_rewards,
month_rewards=month_rewards,
week_handler_stats=week_handler_stats,
month_handler_stats=month_handler_stats)
@app.route('/export_monthly_stats/<int:year>/<int:month>')
@login_required
def export_monthly_stats(year, month):
if current_user.role != '统计员':
app.logger.warning(f'未授权访问:用户 {current_user.name} 尝试导出月度统计')
flash('您没有权限执行此操作')
return redirect(url_for('statistics'))
app.logger.info(f'用户 {current_user.name} 导出 {year}{month}月 统计报表')
# 计算指定月份的开始和结束时间
month_start = datetime(year, month, 1, 0, 0, 0)
if month == 12:
month_end = datetime(year + 1, 1, 1, 0, 0, 0)
else:
month_end = datetime(year, month + 1, 1, 0, 0, 0)
# 检查是否有数据
handlings = WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= month_start,
WorkOrderHandling.restore_time < month_end
).first()
if not handlings:
flash(f'{year}{month}月系统无检修工单')
return redirect(url_for('statistics'))
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = "运维计件奖励"
# 设置标题
ws.merge_cells('A1:J1')
title = ws.cell(1, 1, f"{year}{month}月政企重点项目运维各支局检修激励汇总")
title.font = Font(size=14, bold=True)
title.alignment = Alignment(horizontal='center')
# 设置表头
headers = ['序号', '检修单位名称', '检修项目', '检修设备总数(台)',
'单位距支局维护人员所在地址(公里)', '检修设备每个单价(元)',
'检修项目合计数(路)', '本月检修金额(元)', '支局', '检修人员','本月个人检修合计','检修时间']
for col, header in enumerate(headers, 1):
cell = ws.cell(2, col, header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal='center', wrap_text=True)
# 获取指定月份数据
handlings = WorkOrderHandling.query.filter(
WorkOrderHandling.restore_time >= month_start,
WorkOrderHandling.restore_time < month_end
).all()
# 查询本月所有手工故障单
manual_orders = ManualWorkOrder.query.filter(
ManualWorkOrder.create_time >= month_start,
ManualWorkOrder.create_time < month_end
).order_by(ManualWorkOrder.create_time).all()
# 填充数据
row = 3
idx = 0 # 初始化序号计数器
# 处理普通工单
for handling in handlings:
device = db.session.get(Device, handling.device_id)
if not device:
continue
idx += 1 # 增加序号
ws.cell(row, 1, idx) # 序号
ws.cell(row, 2, device.device_id) # 检修单位名称
ws.cell(row, 3, device.project_type) # 检修项目
ws.cell(row, 4, 1) # 检修设备总数
ws.cell(row, 5, handling.distance_from_branch) # 距离
# 计算单价
distance = float(handling.distance_from_branch)
reward = get_reward_by_distance(device.project_type, distance)
ws.cell(row, 6, reward) # 单价
ws.cell(row, 7, 1) # 检修项目合计数
ws.cell(row, 8, reward) # 本月检修金额
ws.cell(row, 9, handling.branch) # 支局
ws.cell(row, 10, handling.handler) # 检修人员
ws.cell(row, 12, handling.restore_time.strftime('%Y-%m-%d %H:%M:%S')) # 检修时间
row += 1
# 处理手工故障单
for manual_order in manual_orders:
idx += 1 # 增加序号
ws.cell(row, 1, idx) # 序号
ws.cell(row, 2, manual_order.device_location) # 检修单位名称
ws.cell(row, 3, manual_order.project_type) # 检修项目
ws.cell(row, 4, 1) # 检修设备总数
ws.cell(row, 5, manual_order.distance_from_branch) # 距离
# 计算单价
distance = float(manual_order.distance_from_branch)
reward = get_reward_by_distance(manual_order.project_type, distance)
ws.cell(row, 6, reward) # 单价
ws.cell(row, 7, 1) # 检修项目合计数
ws.cell(row, 8, reward) # 本月检修金额
ws.cell(row, 9, manual_order.branch) # 支局
ws.cell(row, 10, manual_order.handler) # 检修人员
ws.cell(row, 12, manual_order.create_time.strftime('%Y-%m-%d %H:%M:%S')) # 检修时间
row += 1
idx += 1
# 添加合计行
ws.cell(row, 7, f"合计")
ws.cell(row, 8, f"=SUM(H3:H{row-1})")
ws.cell(row, 8).font = Font(bold=True)
# 设置列宽
ws.column_dimensions['B'].width = 20
ws.column_dimensions['C'].width = 15
ws.column_dimensions['E'].width = 15
ws.column_dimensions['F'].width = 15
ws.column_dimensions['H'].width = 15
# 保存到内存中
excel_file = io.BytesIO()
wb.save(excel_file)
excel_file.seek(0)
# 设置文件名
filename = f'{year}{month}月政企重点项目运维各支局检修激励汇总.xlsx'
return send_file(
excel_file,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=filename
)
+331
View File
@@ -0,0 +1,331 @@
from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from scripts.models import db, Device, WorkOrder, WorkOrderHandling
from datetime import datetime
import os
from utils.file_utils import allowed_file, compress_image
from utils.lzmx import send_lzmx_message
from config import Config
def init_work_order_routes(app):
# 添加获取支局列表的辅助函数
def get_branches():
branches = db.session.query(Device.branch).distinct().all()
return [branch[0] for branch in branches if branch[0]]
# 派单平台路由
@app.route('/dispatch', methods=['GET', 'POST'])
@login_required
def dispatch():
if current_user.role not in ['装维员', '统计员']:
flash('您没有权限访问此页面')
return redirect(url_for('statistics'))
# 处理 POST 请求
if request.method == 'POST':
device_ids = request.form.getlist('device_ids')
if not device_ids:
flash('请选择至少一个设备')
return redirect(url_for('dispatch'))
today = datetime.now().strftime('%Y%m%d')
last_work_order = WorkOrder.query.filter(WorkOrder.order_id.like(f'{today}%')).order_by(WorkOrder.order_id.desc()).first()
# 获取起始编号
if last_work_order:
last_number = int(last_work_order.order_id[-4:])
start_number = last_number + 1
else:
start_number = 1
# 为每个设备生成唯一的工单号
for index, device_id in enumerate(device_ids):
device = db.session.get(Device, device_id)
if not device:
continue
current_number = start_number + index
new_number = f"{today}{str(current_number).zfill(4)}"
work_order = WorkOrder(
order_id=new_number,
device_id=device_id,
dispatch_time=datetime.now(),
status='待处理',
branch=device.branch
)
db.session.add(work_order)
device.is_normal = False
db.session.add(device)
db.session.commit()
flash('派单成功')
return redirect(url_for('dispatch'))
# 处理 GET 请求
# 获取分页参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int) # 从请求参数中获取每页显示数量
# 获取搜索词和筛选条件
search = request.args.get('search', '')
selected_town = request.args.get('town', '')
selected_branch = request.args.get('branch', '')
towns = db.session.query(Device.town).distinct().all()
towns = [town[0] for town in towns if town[0]]
branches = db.session.query(Device.branch).distinct().all()
branches = [branch[0] for branch in branches if branch[0]]
query = Device.query
if search:
query = query.filter(
(Device.device_id.contains(search)) |
(Device.town.contains(search)) |
(Device.branch.contains(search)) |
(Device.unit_name.contains(search)) |
(Device.project_type.contains(search)) |
(Device.device_type.contains(search)) |
(Device.remark.contains(search))
)
if selected_town:
query = query.filter(Device.town == selected_town)
if selected_branch:
query = query.filter(Device.branch == selected_branch)
# 分页时使用从请求获取的 per_page 参数
devices = query.filter_by(in_use=True).paginate(page=page, per_page=per_page)
return render_template('dispatch.html', devices=devices, towns=towns, branches=branches)
@app.route('/update_device_remark', methods=['POST'])
@login_required
def update_device_remark():
try:
data = request.get_json()
device_id = data.get('device_id')
remark = data.get('remark', '')
device = Device.query.get(device_id)
if not device:
return jsonify({'success': False, 'message': '设备不存在'})
device.remark = remark
db.session.commit()
return jsonify({'success': True})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
# 接单平台路由
@app.route('/receive')
@login_required
def receive():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
branch = request.args.get('branch', '')
device_type = request.args.get('device_type', '')
project_type = request.args.get('project_type', '')
# 基础查询
query = WorkOrder.query.filter_by(status='待处理')
# 根据用户权限筛选工单
if current_user.branch != '交付中心':
query = query.join(Device).filter(Device.branch == current_user.branch)
# 获取总数统计
total_orders = query.count()
# 获取各支局统计
branch_stats = {}
if current_user.branch == '交付中心':
branch_counts = db.session.query(
Device.branch,
db.func.count(WorkOrder.order_id)
).join(WorkOrder, WorkOrder.device_id == Device.device_id
).filter(WorkOrder.status == '待处理'
).group_by(Device.branch).all()
branch_stats = dict(branch_counts)
# 应用分支筛选(仅交付中心可用)
if branch and current_user.branch == '交付中心':
query = query.join(Device).filter(Device.branch == branch)
# 应用设备类型筛选
if device_type:
query = query.join(Device).filter(Device.device_type == device_type)
# 应用项目类型筛选
if project_type:
query = query.join(Device).filter(Device.project_type == project_type)
# 获取筛选选项
device_types_query = db.session.query(Device.device_type).distinct().join(WorkOrder).filter(WorkOrder.status == '待处理')
project_types_query = db.session.query(Device.project_type).distinct().join(WorkOrder).filter(WorkOrder.status == '待处理')
# 根据用户权限进一步筛选选项
if current_user.branch != '交付中心':
device_types_query = device_types_query.filter(Device.branch == current_user.branch)
project_types_query = project_types_query.filter(Device.branch == current_user.branch)
elif branch:
device_types_query = device_types_query.filter(Device.branch == branch)
project_types_query = project_types_query.filter(Device.branch == branch)
device_types = [dt[0] for dt in device_types_query.all() if dt[0]]
project_types = [pt[0] for pt in project_types_query.all() if pt[0]]
# 获取分页数据
work_orders = query.paginate(page=page, per_page=per_page)
# 计算派单时长
for work_order in work_orders.items:
dispatch_time = work_order.dispatch_time
now = datetime.now()
delta = now - dispatch_time
work_order.duration = f"{delta.days}{delta.seconds // 3600}小时{(delta.seconds % 3600) // 60}分钟"
return render_template('receive.html',
work_orders=work_orders,
total_orders=total_orders,
branch_stats=branch_stats,
show_filter=current_user.branch == '交付中心', # 只有交付中心可以看到筛选
branches=get_branches() if current_user.branch == '交付中心' else [], # 只有交付中心可以看到支局列表
selected_branch=branch,
device_types=device_types,
project_types=project_types
)
@app.route('/recall_all_orders', methods=['POST'])
@login_required
def recall_all_orders():
try:
# 获取所有在途工单
orders = WorkOrder.query.filter_by(status='待处理').all()
count = len(orders)
# 恢复设备状态并删除工单
for order in orders:
# 恢复设备状态
device = Device.query.get(order.device_id)
if device:
device.is_normal = True
db.session.add(device)
# 直接删除工单
db.session.delete(order)
db.session.commit()
return jsonify({
'success': True,
'count': count
})
except Exception as e:
return jsonify({
'success': False,
'message': str(e)
})
# 处理工单
@app.route('/receive/<string:work_order_id>', methods=['GET', 'POST'])
@login_required
def receive_detail(work_order_id):
if current_user.role not in ['装维员', '统计员']:
flash('您没有权限访问此页面')
return redirect(url_for('statistics'))
work_order = WorkOrder.query.get_or_404(work_order_id)
device = Device.query.filter_by(device_id=work_order.device_id).first()
if request.method == 'POST':
photo_paths = []
if 'photo' in request.files:
files = request.files.getlist('photo')
if len(files) > 5:
flash('最多只能上传5张图片')
return redirect(url_for('receive_detail', work_order_id=work_order_id))
for file in files:
if file and allowed_file(file.filename, Config.ALLOWED_EXTENSIONS): # 修改这里
filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{file.filename}"
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
compress_image(file_path)
photo_paths.append(filename)
# 创建处理工单信息
handling_info = WorkOrderHandling(
order_id=work_order.order_id,
device_id=work_order.device_id,
branch=device.branch if device else None,
restore_time=datetime.now(),
fault_type=request.form['fault_type'],
contact_person=request.form['contact_person'],
contact_phone=request.form['contact_phone'],
handler=request.form['handler'],
handle_description=request.form['handle_description'],
photo_paths=','.join(photo_paths) if photo_paths else None,
distance_from_branch=request.form['distance_from_branch'],
remark=request.form.get('remark', '')
)
# 更新工单状态为已处理
work_order.status = '已处理'
# 更新设备状态为正常
if device:
device.is_normal = True
# 将所有更改添加到会话
db.session.add(handling_info)
db.session.add(work_order)
if device:
db.session.add(device)
# 一次性提交所有更改
db.session.commit()
# 发送量子密信通知
send_lzmx_message(
Config.LZMX_WEBHOOK_URL, # 修改这里
request.form['handler'],
work_order.device_id,
request.form['fault_type'],
request.form['distance_from_branch'],
datetime.now()
)
flash('工单处理成功')
return redirect(url_for('receive'))
return render_template('receive_detail.html', work_order=work_order, device=device)
@app.route('/recall_order/<order_id>')
@login_required
def recall_order(order_id):
# 检查用户权限
if current_user.role != '统计员':
flash('没有权限执行此操作')
return redirect(url_for('receive'))
# 获取工单
work_order = WorkOrder.query.get_or_404(order_id)
# 更新设备状态
device = Device.query.get(work_order.device_id)
if device:
device.is_normal = True
db.session.add(device)
# 删除工单
db.session.delete(work_order)
db.session.commit()
flash('工单已成功追回')
return redirect(url_for('receive'))