Files
baiyue-logistics-chat/logistics_chat/routes/admin.py
T

189 lines
7.0 KiB
Python

"""HTTP adapters for admin; business logic lives in services."""
import sqlite3
from flask import Blueprint, Response, g, jsonify, render_template, request
from werkzeug.exceptions import RequestEntityTooLarge
from .. import business_service, db, order_admin_service, order_data, order_import_service, staff_service
bp = Blueprint('admin', __name__)
@bp.route('/api/admin/stats')
def admin_stats():
return business_service.admin_stats()
@bp.route('/admin/login', methods=['POST'])
def admin_login():
request.max_content_length = 4096
return staff_service.login(request.get_json(silent=True))
@bp.get('/api/admin/session')
def staff_session():
return {'ok': True, **g.staff}
@bp.post('/admin/logout')
def staff_logout():
return staff_service.logout()
@bp.route('/api/export_chat')
def export_chat():
format = 'csv' if request.args.get('format') == 'csv' else 'xlsx'
mimetype = 'text/csv' if format == 'csv' else 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
return Response(business_service.export_chat(format), mimetype=mimetype,
headers={'Content-Disposition': 'attachment;filename=chat_export.' + format})
@bp.route('/api/agents/list')
def agents_list():
return business_service.agents_list()
@bp.route('/api/search_chat')
def search_chat():
return business_service.search_chat(request.args.get('q', '').strip())
@bp.route('/admin')
def admin():
return render_template('admin_login.html')
@bp.route('/admin/dashboard')
def admin_dash():
return render_template('admin.html')
@bp.get('/admin/orders')
def admin_orders():
return render_template('admin_orders.html')
@bp.get('/api/admin/orders/import/template')
def order_import_template():
return Response(order_import_service.template_csv(), mimetype='text/csv; charset=utf-8',
headers={'Content-Disposition': "attachment; filename*=UTF-8''order-import-template.csv"})
def _read_order_import():
request.max_content_length = order_import_service.MAX_UPLOAD_BYTES + 64 * 1024
uploaded = request.files.get('file')
if uploaded is None:
raise order_import_service.ImportFileError('请选择CSV或XLSX文件。')
parsed = order_import_service.parse_order_upload(uploaded.filename, uploaded.read())
parsed['filename'] = uploaded.filename
return parsed
def _order_preview(parsed, connection):
duplicate_count = order_import_service.mark_existing_rows(connection, parsed['rows'])
return {
'summary': {'total_rows': parsed['total_rows'], 'valid_rows': len(parsed['rows']),
'duplicate_rows': duplicate_count, 'error_rows': len(parsed['errors'])},
'preview': [{key: row.get(key) for key in ('row', 'tenant_id', 'order_no', 'status',
'inbound_code', 'shipment_id', 'country_code', 'transport_mode',
'warehouse_code', 'package_count', 'gross_weight_kg', 'volume_cbm', 'duplicate')}
for row in parsed['rows'][:20]],
'preview_limited': len(parsed['rows']) > 20,
'errors': parsed['errors'],
}
@bp.post('/api/admin/orders/import/preview')
def preview_order_import():
try:
parsed = _read_order_import()
except order_import_service.ImportFileError as error:
return jsonify({'ok': False, 'error': str(error)}), 400
except RequestEntityTooLarge:
return jsonify({'ok': False, 'error': '文件不能超过5 MB。'}), 413
connection = db.connect()
try:
parsed = order_import_service.validate_tenants(connection, parsed)
result = _order_preview(parsed, connection)
finally:
connection.close()
return jsonify({'ok': not parsed['errors'], **result}), 400 if parsed['errors'] else 200
@bp.post('/api/admin/orders/import')
def confirm_order_import():
try:
parsed = _read_order_import()
except order_import_service.ImportFileError as error:
return jsonify({'ok': False, 'error': str(error)}), 400
except RequestEntityTooLarge:
return jsonify({'ok': False, 'error': '文件不能超过5 MB。'}), 413
if parsed['errors']:
return jsonify({'ok': False, 'error': '文件中存在错误行,请修正后重新上传。',
'errors': parsed['errors']}), 400
connection = db.connect()
try:
connection.execute('BEGIN IMMEDIATE')
parsed = order_import_service.validate_tenants(connection, parsed)
if parsed['errors']:
connection.rollback()
return jsonify({'ok': False, 'error': '文件中存在错误行,请修正后重新上传。',
**_order_preview(parsed, connection)}), 400
existing_keys = {(row['tenant_id'], str(row['order_no']).casefold()) for row in parsed['rows']
if connection.execute('SELECT 1 FROM order_test_data WHERE tenant_id=? AND order_no=?',
(row['tenant_id'], row['order_no'])).fetchone()}
imported = order_data.import_records(connection, parsed['rows'])
batch_id = order_admin_service.create_import_batch(connection, parsed.get('filename'),
g.staff['username'], parsed, imported, existing_keys)
connection.commit()
except (ValueError, sqlite3.Error):
connection.rollback()
return jsonify({'ok': False, 'error': '订单导入失败,数据库未写入,请检查文件后重试。'}), 400
finally:
connection.close()
return jsonify({'ok': True, **imported, 'total': parsed['total_rows'], 'batch_id': batch_id})
@bp.get('/api/admin/orders/list')
def list_admin_orders():
connection = db.connect()
try:
result = order_admin_service.list_orders(connection, request.args.get('q', ''),
request.args.get('tenant', ''), request.args.get('status', ''),
request.args.get('before'), request.args.get('limit', 50))
except ValueError as error:
return jsonify({'ok': False, 'error': str(error)}), 400
finally:
connection.close()
return jsonify({'ok': True, **result})
@bp.get('/api/admin/orders/import/batches')
def list_order_import_batches():
connection = db.connect()
try:
return jsonify({'ok': True, 'batches': order_admin_service.list_import_batches(connection)})
finally:
connection.close()
@bp.post('/api/admin/orders/import/batches/<batch_id>/undo')
def undo_order_import(batch_id):
connection = db.connect()
try:
connection.execute('BEGIN IMMEDIATE')
result = order_admin_service.undo_import_batch(connection, batch_id, g.staff['username'])
if result is None:
connection.rollback()
return jsonify({'ok': False, 'error': '找不到此导入批次。'}), 404
connection.commit()
except ValueError as error:
connection.rollback()
return jsonify({'ok': False, 'error': str(error)}), 400
except sqlite3.Error:
connection.rollback()
return jsonify({'ok': False, 'error': '撤销失败,请刷新后重试。'}), 500
finally:
connection.close()
return jsonify({'ok': True, **result})