189 lines
7.0 KiB
Python
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})
|