Files
baiyue-logistics-chat/logistics_chat/order_admin_service.py
T

159 lines
7.4 KiB
Python

"""Staff order browsing and reversible import batch records."""
import hashlib
import json
import sqlite3
from datetime import datetime
from . import order_data
MAX_ORDER_ID = 9223372036854775807
ORDER_LIST_FIELDS = ('tenant_id', 'order_no', 'status', 'inbound_code', 'shipment_id',
'country_code', 'transport_mode', 'warehouse_code', 'source_updated_at', 'snapshot_at')
def init_order_admin_tables(connection):
connection.execute('''CREATE TABLE IF NOT EXISTS order_import_batches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
imported_by TEXT NOT NULL,
imported_at TEXT NOT NULL,
total_rows INTEGER NOT NULL,
imported_rows INTEGER NOT NULL,
preserved_rows INTEGER NOT NULL,
state TEXT NOT NULL DEFAULT 'active' CHECK(state IN ('active','undone')),
undone_by TEXT,
undone_at TEXT,
deleted_rows INTEGER NOT NULL DEFAULT 0,
preserved_on_undo INTEGER NOT NULL DEFAULT 0
)''')
connection.execute('''CREATE TABLE IF NOT EXISTS order_import_batch_items (
batch_id INTEGER NOT NULL,
order_id INTEGER NOT NULL,
row_fingerprint TEXT NOT NULL,
PRIMARY KEY(batch_id, order_id)
)''')
connection.execute('CREATE INDEX IF NOT EXISTS idx_order_import_batches_time '
'ON order_import_batches(imported_at DESC, id DESC)')
def _row_fingerprint(row):
values = [row[field] for field in order_data.FIELDS]
encoded = json.dumps(values, ensure_ascii=False, separators=(',', ':'), default=str)
return hashlib.sha256(encoded.encode('utf-8')).hexdigest()
def _safe_filename(filename):
return str(filename or '').replace('\\', '/').split('/')[-1][:255] or 'orders.csv'
def create_import_batch(connection, filename, username, parsed, result, existing_keys):
"""Persist the import audit and fingerprints for newly inserted records."""
now = datetime.now().isoformat(timespec='seconds')
cursor = connection.execute('''INSERT INTO order_import_batches
(filename,imported_by,imported_at,total_rows,imported_rows,preserved_rows)
VALUES(?,?,?,?,?,?)''', (_safe_filename(filename), username, now, parsed['total_rows'],
result['imported'], result['preserved']))
batch_id = cursor.lastrowid
for item in parsed['rows']:
key = (item['tenant_id'], item['order_no'].casefold())
if key in existing_keys:
continue
row = connection.execute('SELECT id,' + ','.join(order_data.FIELDS) +
' FROM order_test_data WHERE tenant_id=? AND order_no=?',
(item['tenant_id'], item['order_no'])).fetchone()
if row is None:
continue
record = dict(zip(('id', *order_data.FIELDS), row))
connection.execute('INSERT INTO order_import_batch_items(batch_id,order_id,row_fingerprint) VALUES(?,?,?)',
(batch_id, record['id'], _row_fingerprint(record)))
return batch_id
def _conditions(query, tenant, status, before):
clauses, values = [], []
if query:
escaped = query.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
clauses.append("(order_no LIKE ? ESCAPE '\\' OR inbound_code LIKE ? ESCAPE '\\' "
"OR shipment_id LIKE ? ESCAPE '\\' OR source_order_id LIKE ? ESCAPE '\\')")
values.extend(['%' + escaped + '%'] * 4)
if tenant:
clauses.append('tenant_id=?')
values.append(tenant)
if status:
clauses.append('status=?')
values.append(status)
if before is not None:
clauses.append('id<?')
values.append(before)
return (' WHERE ' + ' AND '.join(clauses) if clauses else ''), values
def list_orders(connection, query='', tenant='', status='', before=None, limit=50):
query, tenant, status = str(query or '').strip(), str(tenant or '').strip(), str(status or '').strip().upper()
if len(query) > 128 or len(tenant) > 128:
raise ValueError('搜索内容不能超过128个字符。')
if status and status not in order_data.STATUS_LABELS:
raise ValueError('订单状态无效。')
try:
limit = int(limit)
before = int(before) if before is not None else None
except (TypeError, ValueError):
raise ValueError('分页参数无效。') from None
if not 1 <= limit <= 100 or (before is not None and not 0 < before <= MAX_ORDER_ID):
raise ValueError('分页参数无效。')
where, values = _conditions(query, tenant, status, None)
total = connection.execute('SELECT COUNT(*) FROM order_test_data' + where, values).fetchone()[0]
page_where, page_values = _conditions(query, tenant, status, before)
rows = connection.execute('SELECT id,' + ','.join(ORDER_LIST_FIELDS) +
' FROM order_test_data' + page_where + ' ORDER BY id DESC LIMIT ?',
(*page_values, limit + 1)).fetchall()
has_more = len(rows) > limit
rows = rows[:limit]
orders = [dict(zip(('id', *ORDER_LIST_FIELDS), row)) for row in rows]
return {'orders': orders, 'total': total,
'next_before': rows[-1][0] if has_more and rows else None,
'limit': limit}
def list_import_batches(connection, limit=50):
limit = max(1, min(100, int(limit)))
rows = connection.execute('''SELECT id,filename,imported_by,imported_at,total_rows,imported_rows,
preserved_rows,state,undone_by,undone_at,deleted_rows,preserved_on_undo
FROM order_import_batches ORDER BY id DESC LIMIT ?''', (limit,)).fetchall()
fields = ('id','filename','imported_by','imported_at','total_rows','imported_rows',
'preserved_rows','state','undone_by','undone_at','deleted_rows','preserved_on_undo')
return [dict(zip(fields, row)) for row in rows]
def undo_import_batch(connection, batch_id, username):
try:
batch_id = int(batch_id)
except (TypeError, ValueError):
raise ValueError('导入批次编号无效。') from None
if not 0 < batch_id <= MAX_ORDER_ID:
raise ValueError('导入批次编号无效。')
batch = connection.execute('SELECT state,deleted_rows,preserved_on_undo FROM order_import_batches WHERE id=?',
(batch_id,)).fetchone()
if not batch:
return None
if batch[0] == 'undone':
return {'batch_id': batch_id, 'deleted': batch[1], 'preserved': batch[2], 'already_undone': True}
deleted = preserved = 0
items = connection.execute('SELECT order_id,row_fingerprint FROM order_import_batch_items WHERE batch_id=?',
(batch_id,)).fetchall()
for order_id, fingerprint in items:
current = connection.execute('SELECT ' + ','.join(order_data.FIELDS) +
' FROM order_test_data WHERE id=?', (order_id,)).fetchone()
if current is None:
continue
record = dict(zip(order_data.FIELDS, current))
if _row_fingerprint(record) != fingerprint:
preserved += 1
continue
deleted += connection.execute('DELETE FROM order_test_data WHERE id=?', (order_id,)).rowcount
now = datetime.now().isoformat(timespec='seconds')
connection.execute('''UPDATE order_import_batches SET state='undone',undone_by=?,undone_at=?,
deleted_rows=?,preserved_on_undo=? WHERE id=?''', (username, now, deleted, preserved, batch_id))
return {'batch_id': batch_id, 'deleted': deleted, 'preserved': preserved, 'already_undone': False}