6-27-晚上提交,修复业务员物流信息展示问题
This commit is contained in:
parent
d53bf0b536
commit
a591b180de
@ -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(
|
|||||||
返回值:
|
返回值:
|
||||||
物流轨迹列表,按时间倒序排列
|
物流轨迹列表,按时间倒序排列
|
||||||
|
|
||||||
权限要求:manager、admin、driver 角色,需 logistics:trace:list 权限
|
权限要求:manager、admin、driver、salesman 角色,需 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))
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 {
|
||||||
// 忽略错误
|
// 忽略错误
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user