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
logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例
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")), # 权限校验:轨迹查看权限
) -> 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))

View File

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

View File

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

View File

@ -666,7 +666,11 @@ class LogisticsService:
}
for item in traces
]
third_party_trace_list = self._load_third_party_traces(order_id, session)
try:
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:
return {
"order_id": order_id,
@ -1106,6 +1110,11 @@ class LogisticsService:
if task is None or task.driver_id != current_user.get("user_id"):
raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人订单轨迹", status_code=403)
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)
def _notify_driver_task_assigned(self, session: Session, task, order) -> None:
@ -1257,12 +1266,11 @@ class LogisticsService:
session,
{
"reminder_type": "order_status_change",
"receiver_id": receiver,
"title": "订单状态变更",
"content": reminder_content,
"receiver_user_id": receiver,
"reminder_title": "订单状态变更",
"reminder_content": reminder_content,
"biz_type": "sales_order",
"biz_id": order.id,
"source_platform": "backend",
},
)
@ -1481,6 +1489,7 @@ class LogisticsService:
AppException: 配置不完整或 API 调用失败时抛出
"""
import hashlib
from urllib.parse import urlencode as _urlencode
settings = self.settings
if not settings.kuaidi100_key or not settings.kuaidi100_customer:
@ -1490,36 +1499,33 @@ class LogisticsService:
status_code=400,
)
# 快递100 传统查询接口GET 方式)
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("/"):
path = f"/{path}"
url = f"{endpoint}{path}"
param = {
"com": express_company,
"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}"
# 签名MD5(key + 运单号 + 快递公司编码 + customer)
sign_str = f"{settings.kuaidi100_key}{tracking_number}{express_company}{settings.kuaidi100_customer}"
sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest()
body = json.dumps({
"customer": settings.kuaidi100_customer,
params = {
"key": settings.kuaidi100_key,
"com": express_company,
"nu": tracking_number,
"show": "0",
"muti": "1",
"order": "desc",
"customer": settings.kuaidi100_customer,
"sign": sign,
"param": param,
}, ensure_ascii=False).encode("utf-8")
}
url = f"{url}?{_urlencode(params)}"
req = request.Request(
url=url,
data=body,
headers={"Content-Type": "application/json; charset=UTF-8"},
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="GET",
)
try:
with request.urlopen(req, timeout=20) as response:
@ -1608,10 +1614,10 @@ class LogisticsService:
third_party_trace_list: 第三方轨迹列表
Returns:
合并后的轨迹列表 node_time 升序排列
合并后的轨迹列表 node_time 降序排列最新的在前
"""
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

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=23), # customer:update
RoleMenu(role_id=2, menu_id=45), # ai:parse-order
RoleMenu(role_id=2, menu_id=28), # logistics:trace:list
# 管理层订单、审批、物流、报表、主数据、AI
RoleMenu(role_id=3, menu_id=1), # order:list
RoleMenu(role_id=3, menu_id=3), # order:approve

View File

@ -130,13 +130,17 @@
</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" />
<button class="action-btn primary-btn" type="primary" loading="{{actionLoading}}" bindtap="handleAction">
{{ taskInfo.status === 'pending' ? '接受任务' : (taskInfo.status === 'accepted' ? '确认揽货' : '确认送达') }}
{{ taskInfo.status === 'pending' ? '接受任务' : '确认揽货' }}
</button>
</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 class="done-label">任务已完成</view>
</view>

View File

@ -507,7 +507,20 @@
<!-- 自发订单提示 -->
<div v-if="detail.order.trackingNumber" class="approval-hint">
马甸工厂自发 已填写快递单号{{ 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 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">
@ -897,6 +910,7 @@ import {
generateSupplierText,
fetchProductOptions,
cancelApproveOrder,
approveOrder,
updateOrderTracking,
updateLogisticsTracking,
batchMatchLogisticsPreview,
@ -1445,7 +1459,7 @@ async function loadDetail() {
};
detail.items = d.items || [];
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 = [];
@ -1633,11 +1647,11 @@ async function handleApprovalPass() {
//
if (detail.order.trackingNumber) {
//
// in_transit
detailActionBusy.value = true;
try {
await changeOrderStatus(detail.orderId, {target_status: 'completed'});
toast.success('马甸工厂自发 — 审批通过,订单已直接完成');
await approveOrder(detail.orderId, {approve_result: 'pass', approve_opinion: '审批通过'});
toast.success('马甸工厂自发 — 审批通过,订单已进入运输中');
await loadDetail();
await handleSearch();
} catch (e) {

View File

@ -91,6 +91,16 @@
<span class="field-value">{{ selfDeliveryTracking }}</span>
</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>
<!-- 4. 物流信息 -->
<section v-if="trackingInfo" class="waterfall-item">
@ -326,9 +336,13 @@ async function loadDetail() {
async function loadTrace() {
try {
const orderId = route.params.id;
const result = await fetchOrderLogisticsTrace(orderId);
if (result) {
traceList.value = [{ node_time: result.latest_time, node_desc: result.latest_desc }];
const token = localStorage.getItem(SALES_TOKEN_KEY) || "";
const res = await fetch(`${apiBaseUrl}/api/logistics/${orderId}/trace`, {
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 {
//