支持登录后按客户范围统计运单数量

This commit is contained in:
2026-11-04 19:09:17 +08:00
parent 1a82894b6a
commit f17f355366
6 changed files with 261 additions and 30 deletions
+99 -27
View File
@@ -112,42 +112,111 @@ def import_tracking_records(connection, records, replace=False):
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
connection = connect()
connection.row_factory = sqlite3.Row
scope = tenant_filter(tenant_id, tenant_ids)
if scope is None:
return []
connection = None
try:
query = 'SELECT * FROM order_test_data WHERE (order_no=? OR inbound_code=? OR shipment_id=?)'
values = [reference] * 3
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 []
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)})
if not allowed:
return []
query += ' AND tenant_id IN (' + ','.join('?' for _ in allowed) + ')'
values.extend(sorted(allowed))
elif tenant_id is not None:
if not str(tenant_id).strip():
return []
if tenant and str(tenant_id) != str(tenant):
return []
tenant = str(tenant_id)
if tenant and tenant_ids is None:
query += ' AND tenant_id=?'
values.append(str(tenant))
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:
connection.close()
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 extract_reference(message):
@@ -263,6 +332,9 @@ def is_order_followup(message):
def answer_query(message, conv_id):
references = extract_reference(message)
tenants = customer_tenant_ids()
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 references and not tenants:
return order_login_response()
if len(references) > 1: