450 lines
25 KiB
Python
450 lines
25 KiB
Python
"""Local, editable order test data. This repository can later be replaced by MCP."""
|
||
|
||
import json
|
||
import re
|
||
import sqlite3
|
||
from datetime import datetime
|
||
|
||
from .config import get_setting
|
||
from .order_tracking import build_tracking
|
||
from .visitor_service import customer_tenant_ids
|
||
from .customer_service import order_login_response
|
||
|
||
|
||
FIELDS = ('source_order_id', 'tenant_id', 'order_no', 'inbound_code', 'shipment_id',
|
||
'status', 'country_code', 'transport_mode', 'warehouse_code', 'order_role',
|
||
'parent_order_no', 'package_count', 'gross_weight_kg', 'volume_cbm',
|
||
'history_json', 'tracking_json', 'data_note', 'source_updated_at', 'snapshot_at')
|
||
STATUS_LABELS = {'DRAFT': '草稿', 'SUBMITTED': '已提交', 'APPROVED': '审核通过',
|
||
'ACCEPTED': '已接单', 'REJECTED': '审核驳回', 'PROCESSING': '履约中',
|
||
'COMPLETED': '头程订单已完成', 'CANCELLED': '已取消'}
|
||
FIELD_LABELS = {'inbound':'入仓码', 'number':'订单号', 'shipment':'Shipment ID', 'country':'目的国',
|
||
'warehouse':'目的仓', 'transport':'运输方式', 'packages':'件数',
|
||
'weight':'申报毛重', 'volume':'申报体积', 'role':'订单层级',
|
||
'parent':'父订单号', 'updated':'来源更新时间'}
|
||
IDENTIFIER = re.compile(r'(?<![A-Za-z0-9_-])[A-Za-z0-9][A-Za-z0-9_-]{5,63}(?![A-Za-z0-9_-])')
|
||
ORDER_DETAIL = r'状态|进度|轨迹|到哪|开船|到港|清关|提柜|还柜|签收|预计|什么时候|何时|多久|重量|毛重|体积|件数|箱数|目的地|目的国|目的仓|仓库|父订单|运费|费用|价格|几件|几箱|公斤|千克|多少钱'
|
||
|
||
|
||
class OrderDataUnavailable(Exception):
|
||
pass
|
||
|
||
|
||
def init_order_table(connection):
|
||
connection.execute('''CREATE TABLE IF NOT EXISTS order_test_data (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
source_order_id TEXT, tenant_id TEXT NOT NULL DEFAULT 'test',
|
||
order_no TEXT NOT NULL COLLATE NOCASE,
|
||
inbound_code TEXT COLLATE NOCASE, shipment_id TEXT COLLATE NOCASE,
|
||
status TEXT NOT NULL, country_code TEXT, transport_mode TEXT, warehouse_code TEXT,
|
||
order_role TEXT, parent_order_no TEXT,
|
||
package_count INTEGER, gross_weight_kg TEXT, volume_cbm TEXT,
|
||
history_json TEXT NOT NULL DEFAULT '[]', tracking_json TEXT NOT NULL DEFAULT '[]', data_note TEXT,
|
||
source_updated_at TEXT, snapshot_at TEXT,
|
||
UNIQUE(tenant_id, order_no)
|
||
)''')
|
||
columns = {row[1] for row in connection.execute('PRAGMA table_info(order_test_data)')}
|
||
if 'tracking_json' not in columns:
|
||
connection.execute("ALTER TABLE order_test_data ADD COLUMN tracking_json TEXT NOT NULL DEFAULT '[]'")
|
||
connection.execute('CREATE INDEX IF NOT EXISTS idx_order_test_inbound ON order_test_data(inbound_code)')
|
||
connection.execute('CREATE INDEX IF NOT EXISTS idx_order_test_shipment ON order_test_data(shipment_id)')
|
||
|
||
|
||
def import_records(connection, records, replace=False):
|
||
"""Import an explicitly supplied snapshot; preserve manual edits unless requested."""
|
||
init_order_table(connection)
|
||
inserted = skipped = 0
|
||
for source in records:
|
||
if not isinstance(source, dict) or not str(source.get('order_no') or '').strip():
|
||
raise ValueError('Each record requires an order_no')
|
||
row = {key: source.get(key) for key in FIELDS}
|
||
row['tenant_id'] = str(row['tenant_id'] or 'test')
|
||
row['source_order_id'] = str(row['source_order_id']) if row['source_order_id'] is not None else None
|
||
row['order_no'] = str(row['order_no']).strip()
|
||
row['status'] = str(row['status'] or 'DRAFT')
|
||
history = row['history_json'] or []
|
||
if isinstance(history, str): history = json.loads(history)
|
||
if not isinstance(history, list): raise ValueError('history_json must contain a list')
|
||
row['history_json'] = json.dumps(history, ensure_ascii=False)
|
||
tracking = row['tracking_json'] or []
|
||
if isinstance(tracking, str): tracking = json.loads(tracking)
|
||
if not isinstance(tracking, list): raise ValueError('tracking_json must contain a list')
|
||
row['tracking_json'] = json.dumps(tracking, ensure_ascii=False)
|
||
row['snapshot_at'] = row['snapshot_at'] or datetime.now().isoformat(timespec='seconds')
|
||
exists = connection.execute('SELECT 1 FROM order_test_data WHERE tenant_id=? AND order_no=?',
|
||
(row['tenant_id'], row['order_no'])).fetchone()
|
||
if exists and not replace:
|
||
skipped += 1
|
||
continue
|
||
columns = ','.join(FIELDS)
|
||
placeholders = ','.join('?' for _ in FIELDS)
|
||
updates = ','.join(key+'=excluded.'+key for key in FIELDS if key not in ('tenant_id', 'order_no'))
|
||
connection.execute(f'INSERT INTO order_test_data ({columns}) VALUES ({placeholders}) '
|
||
f'ON CONFLICT(tenant_id,order_no) DO UPDATE SET {updates}', [row[key] for key in FIELDS])
|
||
inserted += 1
|
||
return {'imported': inserted, 'preserved': skipped}
|
||
|
||
|
||
def import_tracking_records(connection, records, replace=False):
|
||
"""Update tracking alone; never replace hand-edited order fields or add unknown orders."""
|
||
init_order_table(connection)
|
||
imported = preserved = missing = 0
|
||
for record in records:
|
||
if not isinstance(record, dict) or not record.get('tenant_id') or not record.get('order_no'):
|
||
raise ValueError('Tracking record requires tenant_id and order_no')
|
||
tracking = record.get('tracking_json')
|
||
if isinstance(tracking, str): tracking = json.loads(tracking)
|
||
if not isinstance(tracking, list): raise ValueError('tracking_json must contain a list')
|
||
row = connection.execute('SELECT id,source_order_id,tracking_json FROM order_test_data '
|
||
'WHERE tenant_id=? AND order_no=?',
|
||
(str(record['tenant_id']), record['order_no'])).fetchone()
|
||
if not row:
|
||
missing += 1
|
||
continue
|
||
if not record.get('source_order_id') or str(record['source_order_id']) != str(row[1]):
|
||
raise ValueError('Tracking source_order_id does not match local order')
|
||
if not replace and row[2] not in (None, '', '[]'):
|
||
preserved += 1
|
||
continue
|
||
connection.execute('UPDATE order_test_data SET tracking_json=? WHERE id=?',
|
||
(json.dumps(tracking, ensure_ascii=False), row[0]))
|
||
imported += 1
|
||
return {'imported': imported, 'preserved': preserved, 'missing': missing}
|
||
|
||
|
||
def tenant_filter(tenant_id=None, tenant_ids=None):
|
||
"""Share the same server-side scope intersection for exact and aggregate reads."""
|
||
tenant = get_setting('ORDER_DATA_TENANT_ID')
|
||
if tenant_ids is not None:
|
||
if not isinstance(tenant_ids,(tuple,list,set)) or not 0 < len(tenant_ids) <= 256:
|
||
return None
|
||
allowed = {value for value in tenant_ids if isinstance(value,str) and 0 < len(value.strip()) <= 128}
|
||
if tenant_id is not None:
|
||
allowed.intersection_update({str(tenant_id)})
|
||
if tenant:
|
||
allowed.intersection_update({str(tenant)})
|
||
return (' AND tenant_id IN (' + ','.join('?' for _ in allowed) + ')', sorted(allowed)) if allowed else None
|
||
if tenant_id is not None:
|
||
if not str(tenant_id).strip() or (tenant and str(tenant_id) != str(tenant)):
|
||
return None
|
||
tenant = str(tenant_id)
|
||
return (' AND tenant_id=?', [str(tenant)]) if tenant else ('', [])
|
||
|
||
|
||
def lookup_order(reference, tenant_id=None, tenant_ids=None):
|
||
"""Exact, bounded lookup; scope comes from server configuration, never model SQL."""
|
||
from .db import connect
|
||
|
||
scope = tenant_filter(tenant_id, tenant_ids)
|
||
if scope is None:
|
||
return []
|
||
connection = None
|
||
try:
|
||
connection = connect()
|
||
connection.row_factory = sqlite3.Row
|
||
query = 'SELECT * FROM order_test_data WHERE (order_no=? OR inbound_code=? OR shipment_id=?)' + scope[0]
|
||
values = [reference] * 3 + scope[1]
|
||
return [dict(row) for row in connection.execute(query+' ORDER BY id LIMIT 2', values)]
|
||
except sqlite3.Error:
|
||
raise OrderDataUnavailable from None
|
||
finally:
|
||
if connection is not None:
|
||
connection.close()
|
||
|
||
|
||
def count_orders(tenant_ids, status=None):
|
||
"""Fresh totals require an explicit authorized scope; never fall back to all rows."""
|
||
from .db import connect
|
||
|
||
empty = {'total':0, 'with_inbound':0}
|
||
scope = tenant_filter(tenant_ids=tenant_ids if tenant_ids is not None else ())
|
||
if scope is None or (status is not None and status not in STATUS_LABELS):
|
||
return empty
|
||
connection = None
|
||
try:
|
||
connection = connect()
|
||
query = ("SELECT COUNT(*),COUNT(NULLIF(TRIM(inbound_code,char(9)||char(10)||char(13)||' '),'')) "
|
||
'FROM order_test_data WHERE 1=1' + scope[0])
|
||
values = scope[1]
|
||
if status is not None:
|
||
query += ' AND status=?'
|
||
values = values + [status]
|
||
total, with_inbound = connection.execute(query,values).fetchone()
|
||
return {'total':total, 'with_inbound':with_inbound}
|
||
except sqlite3.Error:
|
||
raise OrderDataUnavailable from None
|
||
finally:
|
||
if connection is not None:
|
||
connection.close()
|
||
|
||
|
||
def parse_count_query(message):
|
||
"""Recognize bounded total/status questions without ignoring unknown filters."""
|
||
if is_lookup_help(message):
|
||
return None
|
||
noun = r'订单|运单|入仓码|入仓号|\borders?\b|\bshipments?\b'
|
||
count = r'多少|几(?:个|票|条|张|笔)|总数|数量|总量|统计|数一下|(?:订单|运单)数(?!量)|\bhow\s+many\b|\bcount\b|\bnumber\b|\btotal\b'
|
||
if not re.search(noun,message,re.I) or not re.search(count,message,re.I):
|
||
return None
|
||
if re.search(r'公斤|千克|\bkg\b|重量|毛重|体积|件数|箱数|运费|费用|价格|多少钱',message,re.I):
|
||
return None
|
||
remaining = message
|
||
statuses = []
|
||
for phrase, status in (('头程订单已完成','COMPLETED'),('已完成','COMPLETED'),('履约中','PROCESSING'),
|
||
('处理中','PROCESSING'),('已取消','CANCELLED'),('审核通过','APPROVED'),
|
||
('审核驳回','REJECTED'),('已接单','ACCEPTED'),('已提交','SUBMITTED'),('草稿','DRAFT')):
|
||
if phrase in remaining:
|
||
statuses.append(status)
|
||
remaining = remaining.replace(phrase,'')
|
||
remaining = re.sub(count+'|'+noun,'',remaining,flags=re.I)
|
||
remaining = re.sub(r'当前账号|当前账户|账号下|账户下|名下|可查询|能查到|可以查到|目前|现在|当前|一共|总共|合计|总计|'
|
||
r'请问|麻烦|帮我|想知道|想查|想看|请|查询|查一下|查下|查查|看一下|看看|看下|一下|下|全部|所有|共有|我的|我|能|有|的|个|票|条|张|笔|呢|吗|呀|啊|共|'
|
||
r'\b(?:what|is|of|do|i|have|are|there|my|in|all|the|currently)\b|[\s,,。.!!??、::]','',remaining,flags=re.I)
|
||
return {'status':statuses[0] if statuses else None, 'unsupported':bool(remaining or len(set(statuses)) > 1)}
|
||
|
||
|
||
def answer_count_query(intent, tenants):
|
||
if not tenants:
|
||
return order_login_response()
|
||
if intent['unsupported']:
|
||
return {'t':'暂不支持按这个条件统计。可以查询订单总数,或按草稿、已提交、履约中、已完成、已取消等业务状态统计。'}
|
||
try:
|
||
counts = count_orders(tenants,intent['status'])
|
||
except OrderDataUnavailable:
|
||
return {'t':'订单信息暂时无法读取,请稍后重试。'}
|
||
if intent['status']:
|
||
text = f"您当前有 {counts['total']} 票业务状态为“{STATUS_LABELS[intent['status']]}”的订单"
|
||
else:
|
||
text = f"您当前共有 {counts['total']} 票订单"
|
||
return {'t':text + f",其中 {counts['with_inbound']} 票已登记入仓码。", '_customer_data':True}
|
||
|
||
|
||
def answer_list_query(message, tenants):
|
||
"""Reuse bounded status parsing; list reads require an explicit customer scope."""
|
||
if is_lookup_help(message) or not re.search(r'订单|运单|\borders?\b|\bshipments?\b',message,re.I):
|
||
return None
|
||
if not re.search(r'列出|列表|全部|所有|\blist\b',message,re.I):
|
||
return None
|
||
if not tenants:
|
||
return order_login_response()
|
||
question=re.sub(r'列出|列表|全部|所有|\blist\b','多少',message,flags=re.I)
|
||
intent=parse_count_query(question)
|
||
if intent is None or intent['unsupported']:
|
||
return {'t':'暂不支持按这个条件列出订单。可查看全部订单或按履约中、已完成等业务状态查询。'}
|
||
scope=tenant_filter(tenant_ids=tenants)
|
||
if scope is None:
|
||
return order_login_response()
|
||
from .db import connect
|
||
connection=None
|
||
try:
|
||
connection=connect()
|
||
sql='SELECT order_no,inbound_code,status,warehouse_code FROM order_test_data WHERE 1=1'+scope[0]
|
||
values=scope[1]
|
||
if intent['status']:
|
||
sql+=' AND status=?';values=values+[intent['status']]
|
||
rows=connection.execute(sql+' ORDER BY id DESC LIMIT 21',values).fetchall()
|
||
if not rows:return {'t':'当前账号下没有符合条件的订单。','_customer_data':True}
|
||
lines=['您当前的订单列表'+('(先列出20票)' if len(rows)>20 else '')+':']
|
||
for number,inbound,status,warehouse in rows[:20]:
|
||
lines.append(f'- 入仓码:{inbound or "未登记"};订单号:{number};业务状态:{STATUS_LABELS.get(status,status)};目的仓:{warehouse or "未登记"}')
|
||
lines.append('发送入仓码可查询该票订单和物流轨迹。')
|
||
return {'t':'\n'.join(lines),'_customer_data':True}
|
||
except sqlite3.Error:
|
||
return {'t':'订单信息暂时无法读取,请稍后重试。'}
|
||
finally:
|
||
if connection is not None:connection.close()
|
||
|
||
|
||
def extract_reference(message):
|
||
# Explicit postal/monetary fields are not order identifiers, even if invalid.
|
||
message = re.sub(r'(?:邮编|zip(?:\s*code)?|post\s*code)\s*[::为是]?\s*[0-9-]+', ' ', message, flags=re.I)
|
||
message = re.sub(r'(?<![A-Za-z0-9_-])\d+(?:\.\d+)?\s*(?:万)?(?:USD|RMB|CNY|美元|美金|人民币|元)(?![A-Za-z0-9_-])',' ',message,flags=re.I)
|
||
message = re.sub(r'\[(?:图片|文件)\]/uploads/\S+|https?://\S+', '', message)
|
||
message = re.sub(r'(?<![A-Za-z0-9_-])\d{4}-\d{1,2}-\d{1,2}'
|
||
r'(?:[T ]\d{1,2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?'
|
||
r'(?![A-Za-z0-9_-])', ' ', message)
|
||
message = re.sub(r'(?<![A-Za-z0-9_.-])\d+(?:\.\d+)?\s*[*xX×]\s*\d+(?:\.\d+)?\s*[*xX×]\s*\d+(?:\.\d+)?'
|
||
r'(?:\s*(?:cm|mm|m|厘米|毫米|米))?(?![A-Za-z0-9_.-])', ' ', message, flags=re.I)
|
||
message = re.sub(r'(?<![A-Za-z0-9_-])\d+(?:\.\d+)?\s*'
|
||
r'(?:kilograms?|kgs?|grams?|g|tons?|lbs?|磅|cbm|cm|mm|公斤|千克|克|吨|立方米|方|m[³3]|箱|件)'
|
||
r'(?![A-Za-z0-9_])', ' ', message, flags=re.I)
|
||
references = []
|
||
for token in IDENTIFIER.findall(message):
|
||
if (re.fullmatch(r'[A-Za-z]{2,4}[0-9][A-Za-z0-9]{0,2}',token)
|
||
and re.search(r'美国|报价|海派|海卡|海运|\bquote\b',message,re.I)
|
||
and not re.search(r'订单|运单|入仓|轨迹|查单|\btrack\b',message,re.I)):
|
||
continue
|
||
if sum(character.isdigit() for character in token) < 3:
|
||
continue
|
||
if re.fullmatch(r'\d{4}[-_]\d{2}[-_]\d{2}|\d+(?:KG|LBS?|CBM|CM|MM)', token, re.I):
|
||
continue
|
||
if token.casefold() not in [value.casefold() for value in references]:
|
||
references.append(token)
|
||
return references
|
||
|
||
|
||
def build_card(row):
|
||
fields = []
|
||
def field(key, value):
|
||
if value is not None and str(value).strip(): fields.append({'key': key, 'value': str(value)})
|
||
field('inbound', row['inbound_code'])
|
||
if row['inbound_code']:
|
||
field('number', row['order_no'])
|
||
field('shipment', row['shipment_id'])
|
||
field('country', row['country_code'])
|
||
field('warehouse', row['warehouse_code'])
|
||
field('transport', {'SEA':'海运', 'AIR':'空运', 'RAIL':'铁路', 'TRUCK':'卡航'}.get(row['transport_mode'], row['transport_mode']))
|
||
field('packages', row['package_count'])
|
||
field('weight', str(row['gross_weight_kg'])+' kg' if row['gross_weight_kg'] is not None else None)
|
||
field('volume', str(row['volume_cbm'])+' m³' if row['volume_cbm'] is not None else None)
|
||
field('role', {'CHILD':'子订单', 'MASTER':'主订单'}.get(row['order_role']))
|
||
field('parent', row['parent_order_no'])
|
||
field('updated', row['source_updated_at'])
|
||
try:
|
||
history = json.loads(row['history_json'])
|
||
except (ValueError, TypeError):
|
||
history = []
|
||
nodes = [{'time': str(event.get('time') or ''),
|
||
'text': STATUS_LABELS.get(str(event.get('status')), str(event.get('status') or '状态更新')), 'place': ''}
|
||
for event in history[-50:] if isinstance(event, dict)] if isinstance(history, list) else []
|
||
nodes.reverse()
|
||
status = STATUS_LABELS.get(row['status'], row['status'])
|
||
inbound = row['inbound_code']
|
||
summary = f"入仓码 {inbound},业务状态:{status}。" if inbound else f"订单号 {row['order_no']},业务状态:{status}。"
|
||
summary += ';'.join(FIELD_LABELS[item['key']]+':'+item['value'] for item in fields if item['key'] != 'inbound')
|
||
card = {'kind':'order', 'no':row['order_no'], 'status':status, 'fields':fields,
|
||
'nodes':nodes, 'summary':summary}
|
||
tracking = build_tracking(row.get('tracking_json'))
|
||
if tracking:
|
||
card.update(node_kind='logistics', nodes=tracking['nodes'],
|
||
estimated_nodes=tracking['estimated_nodes'], tracking_status=tracking['status'])
|
||
facts = tracking['nodes'][:10] + tracking['estimated_nodes'][:5]
|
||
card['summary'] += '。物流状态:' + (tracking['status'] or '暂无实际节点') + '。'
|
||
card['summary'] += ';'.join(f"{node['time']} {node['text']} {node['place']}" for node in facts)
|
||
return card
|
||
|
||
|
||
def is_lookup_help(message):
|
||
if re.search(r'是什么意思|指什么|怎么(?:查|操作|使用)|如何(?:查|操作|使用)|怎样(?:查|操作|使用)|'
|
||
r'\bhow\s+(?:do|can|to)\b', message, re.I):
|
||
return True
|
||
if re.fullmatch(r'\s*(?:请问)?(?:物流轨迹|物流状态|订单状态|入仓码|入仓号)是什么[。?!?!\s]*', message):
|
||
return True
|
||
return bool(re.search(r'\bwhat\s+(?:is|are)\b', message, re.I) and
|
||
not re.search(r'\b(?:my|this|that|our)\b', message, re.I))
|
||
|
||
|
||
def is_tracking_request(message):
|
||
"""Bounded tracking commands also work without an identifier or prior context."""
|
||
question = re.sub(r'[\s,,。.!!??、::]', '', message)
|
||
command = (r'(?:请问|请|麻烦)?(?:帮我)?(?:查(?:询|一下|下)?(?:物流(?:轨迹|进度|状态)?|轨迹|进度|货)|'
|
||
r'(?:物流|轨迹|进度)(?:查询|查一下|查下))')
|
||
personal = (r'(?:我的|(?:我)?(?:这|那)(?:票|个|单)|该)(?:货物?|包裹|快递|订单|运单)'
|
||
r'(?:现在|目前)?(?:到哪(?:里)?了?|什么时候(?:能|会)?到|何时能?到|多久能?到|'
|
||
r'签收了?(?:吗|没)|状态|进度|轨迹)(?:呢|吗|呀|啊)?')
|
||
english = (r'(?:where\s+(?:is|are)\s+my|track(?:\s+my)?|check(?:\s+my)?)\s+'
|
||
r'(?:parcels?|packages?|shipments?|orders?)(?:\s+(?:status|progress))?|'
|
||
r'what\s+is\s+my\s+(?:parcel|package|shipment|order)\s+(?:status|progress)')
|
||
return bool(re.fullmatch(command+'|'+personal, question) or
|
||
re.fullmatch(english, message.strip(' \t\r\n.!?'), re.I))
|
||
|
||
|
||
def is_order_followup(message):
|
||
"""Reuse an order only for an explicit reference or a short detail question."""
|
||
from .order_explanations import detail_question
|
||
if detail_question(message) and not re.search(r'报价|价格|多少钱|公斤|\bKG\b|海派|海卡',message,re.I):
|
||
return True
|
||
if is_lookup_help(message):
|
||
return False
|
||
if is_tracking_request(message):
|
||
return True
|
||
if not re.search(ORDER_DETAIL, message):
|
||
return False
|
||
if re.search(r'(?:这|那|该)(?:个|票|单|笔)|该订单|刚才|上一(?:票|单|个订单)', message):
|
||
return True
|
||
question = re.sub(r'[\s,,。.!!??、::]', '', message)
|
||
details = (
|
||
r'(?:(?:(?:物流|运输)?(?:状态|进度|轨迹)|重量|毛重|体积|件数|箱数|目的地|目的国|目的仓|仓库|父订单|运费|费用|价格)'
|
||
r'(?:(?:是|有|为)?(?:多少|什么|怎么样|怎样|如何|哪个))?|(?:有)?(?:几件|几箱|多少公斤|多少千克|多少钱))'
|
||
r'(?:呢|吗|呀|啊)?')
|
||
tracking = (
|
||
r'(?:(?:到哪(?:里)?|开船|到港|清关|提柜|还柜|签收)(?:了)?(?:呢|吗|没|了吗|没有)?|'
|
||
r'(?:预计)?(?:什么时候|何时|多久)(?:能|会|可以)?(?:到|到达|到港|到仓|到货|送到)(?:呢|呀|啊)?|还要多久)')
|
||
return bool(re.fullmatch(
|
||
r'(?:那|那么|还有|请问|请|查一下|看一下|现在|目前)?(?:它的|订单的)?'
|
||
r'(?:'+details+'|'+tracking+')', question))
|
||
|
||
|
||
def answer_query(message, conv_id):
|
||
references = extract_reference(message)
|
||
tenants = customer_tenant_ids()
|
||
from .order_explanations import general_explanation, detail_question, explain_order
|
||
if not references:
|
||
explanation=general_explanation(message)
|
||
if explanation:return {'t':explanation}
|
||
count_intent = parse_count_query(message) if not references else None
|
||
if count_intent is not None:
|
||
return answer_count_query(count_intent,tenants)
|
||
if not references:
|
||
listed=answer_list_query(message,tenants)
|
||
if listed is not None:return listed
|
||
mutation=bool(re.search(r'改址|修改.*(?:收件|地址|订单)|收件地址.*改|(?:取消|删除).*(?:订单|运单)|(?:订单|运单).*取消',message))
|
||
if references and not tenants:
|
||
return order_login_response()
|
||
if len(references) > 1:
|
||
return {'t':'请一次提供一个入仓码,避免查错订单。'}
|
||
reference = references[0] if references else None
|
||
if not reference and conv_id and is_order_followup(message):
|
||
from .db import connect
|
||
connection = connect()
|
||
try:
|
||
recent = connection.execute('SELECT role,content FROM messages WHERE conv_id=? ORDER BY id DESC LIMIT 12', (conv_id,)).fetchall()
|
||
finally:
|
||
connection.close()
|
||
for role, content in recent:
|
||
if role == 'user':
|
||
earlier_references = extract_reference(content)
|
||
if earlier_references:
|
||
reference = earlier_references[0] if len(earlier_references) == 1 else None
|
||
break
|
||
if not is_order_followup(content):
|
||
break
|
||
if role == 'assistant' and content.startswith('__CARD__'):
|
||
try:
|
||
previous = json.loads(content[8:])
|
||
if previous.get('kind') == 'order':
|
||
reference = previous['no']
|
||
break
|
||
except (ValueError, KeyError, TypeError):
|
||
pass
|
||
if not reference:
|
||
if mutation:
|
||
return order_login_response() if not tenants else {'t':'当前支持查询订单与物流轨迹;改址、取消等操作请联系人工客服办理,尚未修改订单。请提供入仓码。'}
|
||
if detail_question(message) and not is_lookup_help(message):
|
||
return order_login_response() if not tenants else {'t':'请提供入仓码,我先核对该票订单的实际物流记录。'}
|
||
if is_tracking_request(message) or (not is_lookup_help(message) and re.search(r'(?:查|搜|看|找).*(?:订单|运单|物流轨迹|入仓码|入仓号)|(?:入仓码|入仓号).*(?:查|轨迹|状态)|订单状态|物流轨迹|shipment\s*id|'
|
||
r'(?:列出|搜索|所有|全部|多少|我的).*(?:订单|运单|入仓)|(?:订单|运单).*(?:多少|列表|查询|在哪|到哪|进度)|'
|
||
r'^\s*(?:查单|查询|查一下|帮我查一下)[。?!?!\s]*$|'
|
||
r'\b(?:my|find|lookup|track)\s+(?:orders?|shipments?)\b|\b(?:order|shipment)\s+(?:status|tracking|lookup)\b|'
|
||
r'\b(?:track|lookup|find|search)\b.*\binbound\b|\binbound\s+(?:code|number)\s+(?:lookup|tracking|status)\b', message, re.I)):
|
||
return order_login_response() if not tenants else {'t':'请提供入仓码查询订单和物流轨迹。'}
|
||
return None
|
||
if not tenants:
|
||
return order_login_response()
|
||
try:
|
||
matches = lookup_order(reference,tenant_id=tenants[0]) if len(tenants) == 1 else lookup_order(reference,tenant_ids=tenants)
|
||
except OrderDataUnavailable:
|
||
return {'t':'订单信息暂时无法读取,请稍后重试。'}
|
||
if not matches:
|
||
return {'t':f'未找到“{reference}”对应的订单。请核对入仓码,或联系人工客服。'}
|
||
if len(matches) > 1:
|
||
return {'t':'该标识匹配到多个订单,请提供完整入仓码或联系人工客服确认。'}
|
||
if mutation:
|
||
return {'t':f'已查到订单“{reference}”。当前支持查询订单与物流轨迹;改址、取消等操作请联系人工客服办理,尚未修改订单。','_customer_data':True}
|
||
if re.search(r'运费|费用|多少钱|价格', message):
|
||
return {'t':'该订单未记录实际运费,请联系人工客服核实。','_customer_data':True}
|
||
card=build_card(matches[0])
|
||
explanation=explain_order(message,matches[0],card)
|
||
if explanation:return {'t':explanation,'_customer_data':True}
|
||
return {'card':card}
|