From 04f94ad665bbc29bfadf4cc42dc10ec5dd0d11c5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 30 Oct 2026 19:10:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A5=E5=85=A5=E4=BB=93=E7=A0=81=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E6=9F=A5=E8=B4=A7=E5=85=A5=E5=8F=A3=E5=92=8C=E8=BD=A8?= =?UTF-8?q?=E8=BF=B9=E5=8D=A1=E7=89=87=E4=B8=BB=E5=8F=B7=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/architecture.md | 1 + logistics_chat/order_data.py | 22 +++++++++++++--------- static/js/chat.js | 25 +++++++++++++------------ templates/index.html | 4 ++-- tests/frontend/chat.test.cjs | 18 +++++++++++++++++- tests/test_order_data.py | 21 +++++++++++++++++++-- tests/test_visitor.py | 3 ++- 8 files changed, 68 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 2e71753..7a8e69e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **知识库优先**:优先使用本地知识库回答,减少模型编造 - **追问兜底**:当前问题没命中关键词时,用上一句提问去检索 - **运费试算**:识别重量/尺寸/渠道,按报价表阶梯单价计算并展示完整明细 -- **订单查询**:按订单号、入仓码、Shipment ID 查询本地订单测试表;展示概要、申报重量/体积和业务状态历史,不生成模拟运输轨迹 +- **订单查询**:直接输入入仓码查询订单与已有轨迹,卡片主显示入仓码;兼容订单号与 Shipment ID,展示概要、申报重量/体积和业务状态历史,不生成模拟运输轨迹 - **历史会话**:会话保存到 SQLite,支持打开、搜索、删除 - **交互**:停止生成、复制(纯文本)、重答、多行输入、回到底部 - **中英文切换**:右上角「中/EN」按钮或快捷键 `Ctrl + L` diff --git a/docs/architecture.md b/docs/architecture.md index c0e8071..ee425c2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -88,6 +88,7 @@ tests/ # 按业务模块组织的回归 - 单文件围绕一个职责;超过约 300–400 行时检查职责,而非按行数机械拆分。报价数据保持单一来源;兼容转发不复制字典。页面脚本按页面保留既有执行顺序和全局函数,不擅自改为 ES Modules。 - 配色、字体基线和通用控件放在 `theme.css`,页面布局放在对应 CSS。客户页通过 `body.dark` 覆盖主题令牌;新增可翻译文案加入 `chat.js` 的 `I18N`,静态文案用 `data-i18n`。调整样式后检查桌面、390px 窄屏、侧栏、聊天卡片和客服空/活动状态。 - 客户界面不附加测试/演示声明、底部免责声明、键盘说明、快捷入口副标题或附件准备说明;草稿恢复和发送成功静默处理。加载、录音、人工排队状态及操作失败反馈保留,权限校验继续由服务端执行。 +- 客户查货以入仓码为主要入口,输入占位、快捷入口与查询追问均优先使用入仓码;订单卡片从 `fields.inbound` 取得主显示号码,内部订单号放在展开详情。`card.no` 仍保存内部订单号以兼容历史协议和上下文关联;未提供入仓码时保留准确的订单号标签,不生成替代入仓码。 - 客户附件预览由 `clearAttachment` 管理,轨迹展开由 `fillTrackCard` 管理。客服 `drafts` 按 `sid` 保存草稿,发送成功后只清理已确认发送的内容;常用回复仅填入草稿。`renderList` 只在展示数据变化时重绘。 - 客户文字草稿由 `drafts.js` 的 `ChatDrafts.create` 管理,脚本在 `visitor.js` 之后、`chat.js` 之前加载;localStorage 按访客 key 和会话分条存储,sessionStorage 仅记录当前标签页的活动会话。切换前同步保存,输入防抖及页面隐藏时保存;无持久存储时使用页面内存并提示。恢复历史的异步结果受会话版本约束,加载失败禁止继续发送,删除会话同步清除其草稿。草稿不含未发送附件,不跨设备同步。 - 人工申请由服务端以 UTC+8 判断每日 08:00–22:00 营业状态,客户手机时区不参与判断。附件清理保留 `messages` 和 `human_messages` 引用的文件,仅删除超过 7 天的未引用临时文件;读取引用失败时停止清理。附件不存在或不属于当前游客均返回相同的私有、不可缓存 404 页面,破图在聊天中显示可读提示。 diff --git a/logistics_chat/order_data.py b/logistics_chat/order_data.py index d0dcb93..b48b74b 100644 --- a/logistics_chat/order_data.py +++ b/logistics_chat/order_data.py @@ -18,7 +18,7 @@ FIELDS = ('source_order_id', 'tenant_id', 'order_no', 'inbound_code', 'shipment_ STATUS_LABELS = {'DRAFT': '草稿', 'SUBMITTED': '已提交', 'APPROVED': '审核通过', 'ACCEPTED': '已接单', 'REJECTED': '审核驳回', 'PROCESSING': '履约中', 'COMPLETED': '头程订单已完成', 'CANCELLED': '已取消'} -FIELD_LABELS = {'inbound':'入仓码', 'shipment':'Shipment ID', 'country':'目的国', +FIELD_LABELS = {'inbound':'入仓码', 'number':'订单号', 'shipment':'Shipment ID', 'country':'目的国', 'warehouse':'目的仓', 'transport':'运输方式', 'packages':'件数', 'weight':'申报毛重', 'volume':'申报体积', 'role':'订单层级', 'parent':'父订单号', 'updated':'来源更新时间'} @@ -176,6 +176,8 @@ def build_card(row): 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']) @@ -195,8 +197,9 @@ def build_card(row): for event in history[-50:] if isinstance(event, dict)] if isinstance(history, list) else [] nodes.reverse() status = STATUS_LABELS.get(row['status'], row['status']) - summary = f"订单号 {row['order_no']},业务状态:{status}。" - summary += ';'.join(FIELD_LABELS[item['key']]+':'+item['value'] for item in fields) + 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')) @@ -234,7 +237,7 @@ def answer_query(message, conv_id): if references and not tenants: return order_login_response() if len(references) > 1: - return {'t':'请一次提供一个订单号、入仓码或 Shipment ID,避免查错订单。'} + return {'t':'请一次提供一个入仓码,避免查错订单。'} reference = references[0] if references else None if not reference and conv_id and is_order_followup(message): from .db import connect @@ -260,11 +263,12 @@ def answer_query(message, conv_id): except (ValueError, KeyError, TypeError): pass if not reference: - if re.search(r'(?:查|搜|看|找).*(?:订单|运单|物流轨迹|入仓码)|订单状态|物流轨迹|shipment\s*id|' + if 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', message, re.I): - return order_login_response() if not tenants else {'t':'请提供订单号、入仓码或 Shipment ID。'} + 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() @@ -273,9 +277,9 @@ def answer_query(message, conv_id): except OrderDataUnavailable: return {'t':'订单信息暂时无法读取,请稍后重试。'} if not matches: - return {'t':f'未找到“{reference}”对应的订单。请核对订单号、入仓码或 Shipment ID,或联系人工客服。'} + return {'t':f'未找到“{reference}”对应的订单。请核对入仓码,或联系人工客服。'} if len(matches) > 1: - return {'t':'该标识匹配到多个订单,请提供该订单唯一的订单号或入仓码。'} + return {'t':'该标识匹配到多个订单,请提供完整入仓码或联系人工客服确认。'} if re.search(r'运费|费用|多少钱|价格', message): return {'t':'该订单未记录实际运费,请联系人工客服核实。'} return {'card': build_card(matches[0])} diff --git a/static/js/chat.js b/static/js/chat.js index afd9d62..60ba6b3 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -270,7 +270,7 @@ const I18N = { networkFail: "网络连接中断,请检查网络后重试", stopped: "已停止生成", stoppedMark: "(已停止生成)", - inputPh: "输入问题、订单号或入仓码…", + inputPh: "输入入仓码或问题…", send: "发送", stop: "停止", copy: "复制", @@ -284,11 +284,11 @@ const I18N = { welcomeTitle: "物流问题,随时为您解答", welcomeEyebrow: "中欧 · 英国物流服务", serviceTag: "物流咨询服务", - footerTrack: "查订单", + footerTrack: "查入仓码", footerQuote: "专线报价", footerException: "异常处理", footerHuman: "联系人工", - inputShort: "输入问题…", + inputShort: "输入入仓码…", uploading: "附件正在上传,请稍候…", uploadFailed: "附件上传失败,请重新选择文件", serviceUnavailable: "服务暂时不可用,请稍后重试,或联系人工客服。", @@ -317,7 +317,7 @@ const I18N = { logisticsHistory: "物流轨迹", noLogisticsHistory: "暂无已发生的物流节点", estimatedTracking: "预计节点", - order_inbound: "入仓码", order_shipment: "Shipment ID", order_country: "目的国", + order_inbound: "入仓码", order_number: "订单号", order_shipment: "Shipment ID", order_country: "目的国", order_warehouse: "目的仓", order_transport: "运输方式", order_packages: "件数", order_weight: "申报毛重", order_volume: "申报体积", order_role: "订单层级", order_parent: "父订单号", order_updated: "来源更新时间", @@ -331,7 +331,7 @@ const I18N = { langTip: "中英文切换(Ctrl+L)", toBottom: "回到底部", quick: [ - "查询订单状态", "欧洲海运专线报价", "包裹异常处理", "转接人工客服" + "按入仓码查轨迹", "欧洲海运专线报价", "包裹异常处理", "转接人工客服" ], hotList: [ "如何查询物流轨迹?", "欧洲海运时效是多久?", "包裹显示异常签收怎么办?", "如何申请理赔?" @@ -381,7 +381,7 @@ const I18N = { networkFail: "Connection lost, please check your network and retry", stopped: "Stopped", stoppedMark: "(Generation stopped)", - inputPh: "Ask a question or enter an order number…", + inputPh: "Enter an inbound code or question…", send: "Send", stop: "Stop", copy: "Copy", @@ -395,11 +395,11 @@ const I18N = { welcomeTitle: "Your next shipment, made clearer.", welcomeEyebrow: "EUROPE & UK LOGISTICS", serviceTag: "Logistics support", - footerTrack: "Order lookup", + footerTrack: "Inbound lookup", footerQuote: "Get a quote", footerException: "Parcel issue", footerHuman: "Human support", - inputShort: "Message…", + inputShort: "Inbound code…", uploading: "Uploading your attachment…", uploadFailed: "Upload failed. Please select the file again.", serviceUnavailable: "Service is temporarily unavailable. Please try again later or contact support.", @@ -430,7 +430,7 @@ const I18N = { logisticsHistory: "Shipment tracking", noLogisticsHistory: "No actual shipment events available", estimatedTracking: "Estimated milestones", - order_inbound: "Inbound code", order_shipment: "Shipment ID", order_country: "Destination country", + order_inbound: "Inbound code", order_number: "Order number", order_shipment: "Shipment ID", order_country: "Destination country", order_warehouse: "Warehouse", order_transport: "Transport", order_packages: "Packages", order_weight: "Declared gross weight", order_volume: "Declared volume", order_role: "Order level", order_parent: "Parent order", order_updated: "Source updated", @@ -444,7 +444,7 @@ const I18N = { langTip: "Switch language (Ctrl+L)", toBottom: "Back to bottom", quick: [ - "Track my shipment", "Europe sea freight quote", "Handle a parcel exception", "Talk to a human agent" + "Track by inbound code", "Europe sea freight quote", "Handle a parcel exception", "Talk to a human agent" ], hotList: [ "How do I track my shipment?", "How long does Europe sea freight take?", @@ -935,8 +935,9 @@ function fillTrackCard(el, card){ head.className = "track-head"; const no = document.createElement("div"); no.className = "track-no"; - const numberLabel = isOrder ? 'orderNo' : 'trackingNo'; - no.innerHTML = '' + t(numberLabel) + "" + escHtml(card.no); + const inbound = isOrder && (card.fields || []).find(field=>field && field.key === 'inbound' && field.value); + const numberLabel = inbound ? 'order_inbound' : isOrder ? 'orderNo' : 'trackingNo'; + no.innerHTML = '' + t(numberLabel) + "" + escHtml(inbound ? inbound.value : card.no); const st = document.createElement("div"); st.className = "track-status"; st.textContent = card.status; diff --git a/templates/index.html b/templates/index.html index b4e34d0..94e3d38 100644 --- a/templates/index.html +++ b/templates/index.html @@ -50,7 +50,7 @@