6-27-晚上提交,修复业务员物流信息展示问题

This commit is contained in:
wsb1224 2026-06-27 21:27:17 +08:00
parent d53bf0b536
commit a591b180de
8 changed files with 77 additions and 36 deletions

View File

@ -355,7 +355,7 @@ def get_logistics_trace(
order_id: int, # 订单ID order_id: int, # 订单ID
logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例 logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例
session: Session = Depends(get_db_session), # 数据库会话 session: Session = Depends(get_db_session), # 数据库会话
current_user: dict = Depends(require_roles("manager", "admin", "driver")), # 当前登录用户 current_user: dict = Depends(require_roles("manager", "admin", "driver", "salesman")), # 当前登录用户
_permission_user: dict = Depends(require_permissions("logistics:trace:list")), # 权限校验:轨迹查看权限 _permission_user: dict = Depends(require_permissions("logistics:trace:list")), # 权限校验:轨迹查看权限
) -> dict: ) -> dict:
""" """
@ -369,7 +369,7 @@ def get_logistics_trace(
返回值 返回值
物流轨迹列表按时间倒序排列 物流轨迹列表按时间倒序排列
权限要求manageradmindriver 角色 logistics:trace:list 权限 权限要求manageradmindriversalesman 角色 logistics:trace:list 权限
""" """
return success_payload(logistics_service.get_trace(order_id, session, current_user)) return success_payload(logistics_service.get_trace(order_id, session, current_user))

View File

@ -78,6 +78,7 @@ class AuthService:
"customer:list", "customer:list",
"reminder:list", "reminder:list",
"ai:parse-order", "ai:parse-order",
"logistics:trace:list",
], ],
"manager": [ "manager": [
"order:list", "order:list",

View File

@ -167,6 +167,7 @@ DEFAULT_ROLE_MENU_CODES = {
"customer:list", "customer:list",
"reminder:list", "reminder:list",
"ai:parse-order", "ai:parse-order",
"logistics:trace:list",
], ],
"manager": [ "manager": [
"order:list", "order:list",

View File

@ -666,7 +666,11 @@ class LogisticsService:
} }
for item in traces for item in traces
] ]
try:
third_party_trace_list = self._load_third_party_traces(order_id, session) third_party_trace_list = self._load_third_party_traces(order_id, session)
except AppException as exc:
logger.warning("[get_trace] 第三方轨迹查询失败,降级返回内部轨迹: %s", exc)
third_party_trace_list = []
if trace_list or third_party_trace_list: if trace_list or third_party_trace_list:
return { return {
"order_id": order_id, "order_id": order_id,
@ -1106,6 +1110,11 @@ class LogisticsService:
if task is None or task.driver_id != current_user.get("user_id"): if task is None or task.driver_id != current_user.get("user_id"):
raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人订单轨迹", status_code=403) raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人订单轨迹", status_code=403)
return return
if role_code == "salesman":
order = self.order_repository.get_order(session, order_id)
if order is None or order.salesman_id != current_user.get("user_id"):
raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人订单轨迹", status_code=403)
return
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403) raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
def _notify_driver_task_assigned(self, session: Session, task, order) -> None: def _notify_driver_task_assigned(self, session: Session, task, order) -> None:
@ -1257,12 +1266,11 @@ class LogisticsService:
session, session,
{ {
"reminder_type": "order_status_change", "reminder_type": "order_status_change",
"receiver_id": receiver, "receiver_user_id": receiver,
"title": "订单状态变更", "reminder_title": "订单状态变更",
"content": reminder_content, "reminder_content": reminder_content,
"biz_type": "sales_order", "biz_type": "sales_order",
"biz_id": order.id, "biz_id": order.id,
"source_platform": "backend",
}, },
) )
@ -1481,6 +1489,7 @@ class LogisticsService:
AppException: 配置不完整或 API 调用失败时抛出 AppException: 配置不完整或 API 调用失败时抛出
""" """
import hashlib import hashlib
from urllib.parse import urlencode as _urlencode
settings = self.settings settings = self.settings
if not settings.kuaidi100_key or not settings.kuaidi100_customer: if not settings.kuaidi100_key or not settings.kuaidi100_customer:
@ -1490,36 +1499,33 @@ class LogisticsService:
status_code=400, status_code=400,
) )
# 快递100 传统查询接口GET 方式)
endpoint = (settings.logistics_trace_endpoint or "https://api.kuaidi100.com").strip().rstrip("/") endpoint = (settings.logistics_trace_endpoint or "https://api.kuaidi100.com").strip().rstrip("/")
path = (settings.logistics_trace_path or "/query").strip() path = (settings.logistics_trace_path or "/api").strip()
if not path.startswith("/"): if not path.startswith("/"):
path = f"/{path}" path = f"/{path}"
url = f"{endpoint}{path}" url = f"{endpoint}{path}"
param = { # 签名MD5(key + 运单号 + 快递公司编码 + customer)
"com": express_company, sign_str = f"{settings.kuaidi100_key}{tracking_number}{express_company}{settings.kuaidi100_customer}"
"num": tracking_number,
"phone": "",
"from": "",
"to": "",
}
# 快递100 签名MD5(key + 排序参数拼接 + customer + key)
sorted_params = "".join(f"{k}{v}" for k, v in sorted(param.items()) if v)
sign_str = f"{settings.kuaidi100_key}{sorted_params}{settings.kuaidi100_customer}{settings.kuaidi100_key}"
sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest() sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest()
body = json.dumps({ params = {
"customer": settings.kuaidi100_customer,
"key": settings.kuaidi100_key, "key": settings.kuaidi100_key,
"com": express_company,
"nu": tracking_number,
"show": "0",
"muti": "1",
"order": "desc",
"customer": settings.kuaidi100_customer,
"sign": sign, "sign": sign,
"param": param, }
}, ensure_ascii=False).encode("utf-8") url = f"{url}?{_urlencode(params)}"
req = request.Request( req = request.Request(
url=url, url=url,
data=body, headers={"Content-Type": "application/x-www-form-urlencoded"},
headers={"Content-Type": "application/json; charset=UTF-8"}, method="GET",
method="POST",
) )
try: try:
with request.urlopen(req, timeout=20) as response: with request.urlopen(req, timeout=20) as response:
@ -1608,10 +1614,10 @@ class LogisticsService:
third_party_trace_list: 第三方轨迹列表 third_party_trace_list: 第三方轨迹列表
Returns: Returns:
合并后的轨迹列表 node_time 升序排列 合并后的轨迹列表 node_time 降序排列最新的在前
""" """
merged = list(local_trace_list) + list(third_party_trace_list) merged = list(local_trace_list) + list(third_party_trace_list)
merged.sort(key=lambda item: item.get("node_time") or "") merged.sort(key=lambda item: item.get("node_time") or "", reverse=True)
return merged return merged

View File

@ -311,6 +311,7 @@ def seed_menus(db_session):
RoleMenu(role_id=2, menu_id=22), # customer:create RoleMenu(role_id=2, menu_id=22), # customer:create
RoleMenu(role_id=2, menu_id=23), # customer:update RoleMenu(role_id=2, menu_id=23), # customer:update
RoleMenu(role_id=2, menu_id=45), # ai:parse-order RoleMenu(role_id=2, menu_id=45), # ai:parse-order
RoleMenu(role_id=2, menu_id=28), # logistics:trace:list
# 管理层订单、审批、物流、报表、主数据、AI # 管理层订单、审批、物流、报表、主数据、AI
RoleMenu(role_id=3, menu_id=1), # order:list RoleMenu(role_id=3, menu_id=1), # order:list
RoleMenu(role_id=3, menu_id=3), # order:approve RoleMenu(role_id=3, menu_id=3), # order:approve

View File

@ -130,13 +130,17 @@
</view> </view>
</view> </view>
<view wx:if="{{taskInfo && taskInfo.status !== 'delivered' && taskInfo.status !== 'canceled'}}" class="action-bar"> <view wx:if="{{taskInfo && taskInfo.status !== 'delivered' && taskInfo.status !== 'canceled' && taskInfo.status !== 'picked_up'}}" class="action-bar">
<input wx:if="{{taskInfo.status !== 'pending'}}" class="remark-input" placeholder="备注(可选)" value="{{actionRemark}}" bindinput="handleRemarkInput" /> <input wx:if="{{taskInfo.status !== 'pending'}}" class="remark-input" placeholder="备注(可选)" value="{{actionRemark}}" bindinput="handleRemarkInput" />
<button class="action-btn primary-btn" type="primary" loading="{{actionLoading}}" bindtap="handleAction"> <button class="action-btn primary-btn" type="primary" loading="{{actionLoading}}" bindtap="handleAction">
{{ taskInfo.status === 'pending' ? '接受任务' : (taskInfo.status === 'accepted' ? '确认揽货' : '确认送达') }} {{ taskInfo.status === 'pending' ? '接受任务' : '确认揽货' }}
</button> </button>
</view> </view>
<view wx:if="{{taskInfo && taskInfo.status === 'picked_up'}}" class="action-bar done-bar">
<view class="done-label">揽件完成,等待业务员绑定物流单号</view>
</view>
<view wx:if="{{taskInfo && taskInfo.status === 'delivered'}}" class="action-bar done-bar"> <view wx:if="{{taskInfo && taskInfo.status === 'delivered'}}" class="action-bar done-bar">
<view class="done-label">任务已完成</view> <view class="done-label">任务已完成</view>
</view> </view>

View File

@ -507,7 +507,20 @@
<!-- 自发订单提示 --> <!-- 自发订单提示 -->
<div v-if="detail.order.trackingNumber" class="approval-hint"> <div v-if="detail.order.trackingNumber" class="approval-hint">
马甸工厂自发 已填写快递单号{{ detail.order.trackingNumber }}审批通过后跳过工厂下发 马甸工厂自发 已填写快递单号{{ detail.order.trackingNumber }}审批通过后跳过工厂下发
<button v-if="canEditTrackingNumber(detail.order.rawStatus)" class="link-btn" @click="openSelfTrackingEdit" style="margin-left: 8px; font-size: 12px;">修改单号</button>
</div> </div>
<!-- 自发订单物流轨迹 -->
<div v-if="detail.order.trackingNumber && detail.traceList.length" class="doc-trace-timeline" style="margin-top: 12px;">
<div v-for="(item, index) in detail.traceList" :key="index" class="doc-trace-item">
<div class="doc-trace-dot" :class="{ active: index === 0 }"></div>
<div v-if="index < detail.traceList.length - 1" class="doc-trace-line"></div>
<div class="doc-trace-card">
<span class="doc-trace-time">{{ item.node_time }}</span>
<span class="doc-trace-desc">{{ item.node_desc }}</span>
</div>
</div>
</div>
<p v-else-if="detail.order.trackingNumber" class="doc-empty-hint" style="margin-top: 8px;">暂无物流轨迹</p>
<!-- 下发工厂选择非自发订单 --> <!-- 下发工厂选择非自发订单 -->
<div v-if="!detail.order.trackingNumber" class="approval-factory"> <div v-if="!detail.order.trackingNumber" class="approval-factory">
@ -897,6 +910,7 @@ import {
generateSupplierText, generateSupplierText,
fetchProductOptions, fetchProductOptions,
cancelApproveOrder, cancelApproveOrder,
approveOrder,
updateOrderTracking, updateOrderTracking,
updateLogisticsTracking, updateLogisticsTracking,
batchMatchLogisticsPreview, batchMatchLogisticsPreview,
@ -1445,7 +1459,7 @@ async function loadDetail() {
}; };
detail.items = d.items || []; detail.items = d.items || [];
detail.approveLogs = d.approve_logs || []; detail.approveLogs = d.approve_logs || [];
detail.logistics = d.logistics_info?.task || null; detail.logistics = d.logistics_info?.task?.task_id ? d.logistics_info.task : null;
// //
detail.traceList = []; detail.traceList = [];
@ -1633,11 +1647,11 @@ async function handleApprovalPass() {
// //
if (detail.order.trackingNumber) { if (detail.order.trackingNumber) {
// // in_transit
detailActionBusy.value = true; detailActionBusy.value = true;
try { try {
await changeOrderStatus(detail.orderId, {target_status: 'completed'}); await approveOrder(detail.orderId, {approve_result: 'pass', approve_opinion: '审批通过'});
toast.success('马甸工厂自发 — 审批通过,订单已直接完成'); toast.success('马甸工厂自发 — 审批通过,订单已进入运输中');
await loadDetail(); await loadDetail();
await handleSearch(); await handleSearch();
} catch (e) { } catch (e) {

View File

@ -91,6 +91,16 @@
<span class="field-value">{{ selfDeliveryTracking }}</span> <span class="field-value">{{ selfDeliveryTracking }}</span>
</div> </div>
</div> </div>
<!-- 物流轨迹 -->
<div v-if="traceList.length" class="trace-timeline">
<div v-for="(item, index) in traceList" :key="index" class="trace-item">
<div class="trace-dot" :class="{ active: index === 0 }"></div>
<div class="trace-content">
<span class="trace-time">{{ item.node_time }}</span>
<span class="trace-desc">{{ item.node_desc }}</span>
</div>
</div>
</div>
</section> </section>
<!-- 4. 物流信息 --> <!-- 4. 物流信息 -->
<section v-if="trackingInfo" class="waterfall-item"> <section v-if="trackingInfo" class="waterfall-item">
@ -326,9 +336,13 @@ async function loadDetail() {
async function loadTrace() { async function loadTrace() {
try { try {
const orderId = route.params.id; const orderId = route.params.id;
const result = await fetchOrderLogisticsTrace(orderId); const token = localStorage.getItem(SALES_TOKEN_KEY) || "";
if (result) { const res = await fetch(`${apiBaseUrl}/api/logistics/${orderId}/trace`, {
traceList.value = [{ node_time: result.latest_time, node_desc: result.latest_desc }]; headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
});
const result = await res.json().catch(() => ({}));
if (res.ok && result.code === 0) {
traceList.value = (result.data || {}).trace_list || [];
} }
} catch { } catch {
// //