7-6 订单物流修复
This commit is contained in:
parent
ea1c5a7835
commit
8fc64fee8f
@ -95,6 +95,7 @@ class AuthService:
|
||||
"ai:recognize",
|
||||
"ai:correct",
|
||||
"ai:parse-order",
|
||||
"logistics:trace:list",
|
||||
],
|
||||
"driver": [
|
||||
"driver:task:list",
|
||||
@ -135,6 +136,7 @@ class AuthService:
|
||||
"ai:recognize",
|
||||
"ai:correct",
|
||||
"ai:parse-order",
|
||||
"logistics:trace:list",
|
||||
],
|
||||
"kuaiji": [
|
||||
"dashboard:view",
|
||||
@ -154,6 +156,7 @@ class AuthService:
|
||||
"ai:parse-order",
|
||||
"reminder:list",
|
||||
"config:view",
|
||||
"logistics:trace:list",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@ -105,6 +105,7 @@ DEFAULT_MENUS = [
|
||||
{"id": 20, "parent_id": 5, "menu_name": "提交审核", "menu_path": "", "menu_type": "button", "permission_code": "order:submit", "icon": "", "sort_no": 20, "status": 1},
|
||||
{"id": 21, "parent_id": 5, "menu_name": "取消审批", "menu_path": "", "menu_type": "button", "permission_code": "order:cancel-approve", "icon": "", "sort_no": 21, "status": 1},
|
||||
{"id": 24, "parent_id": 5, "menu_name": "订单更新", "menu_path": "", "menu_type": "button", "permission_code": "order:update", "icon": "", "sort_no": 24, "status": 1},
|
||||
{"id": 46, "parent_id": 5, "menu_name": "物流轨迹查看", "menu_path": "", "menu_type": "button", "permission_code": "logistics:trace:list", "icon": "", "sort_no": 46, "status": 1},
|
||||
|
||||
# 按钮权限 - 基础资料
|
||||
{"id": 22, "parent_id": 3, "menu_name": "定价规则编辑", "menu_path": "", "menu_type": "button", "permission_code": "master-data:update", "icon": "", "sort_no": 22, "status": 1},
|
||||
@ -183,8 +184,11 @@ DEFAULT_ROLE_MENU_CODES = {
|
||||
"ai:recognize",
|
||||
"ai:correct",
|
||||
"ai:parse-order",
|
||||
"logistics:trace:list",
|
||||
],
|
||||
"driver": [
|
||||
"logistics:trace:list",
|
||||
],
|
||||
"driver": [],
|
||||
"kuaiji": [
|
||||
"dashboard:view",
|
||||
"order:list",
|
||||
@ -203,6 +207,7 @@ DEFAULT_ROLE_MENU_CODES = {
|
||||
"ai:parse-order",
|
||||
"reminder:list",
|
||||
"config:view",
|
||||
"logistics:trace:list",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@ -406,11 +406,24 @@ class OrderService:
|
||||
if len(order_items) > 2:
|
||||
items_summary += "..."
|
||||
|
||||
# 获取物流信息
|
||||
# 获取物流信息。列表用单号触发前端查询最新轨迹,自发订单回退到 sales_order。
|
||||
task = self.logistics_repository.get_task_by_order_id(session, item.id)
|
||||
tracking_number = (item.tracking_number or "").strip()
|
||||
express_company = (item.express_company or "").strip()
|
||||
logistics_info = ""
|
||||
if task:
|
||||
logistics_info = task.express_company + " " + (task.tracking_number or "") if task.express_company else task.status or ""
|
||||
task_tracking = (task.tracking_number or "").strip()
|
||||
task_express = (task.express_company or "").strip()
|
||||
if not task_tracking:
|
||||
waybills = self.logistics_repository.list_waybills_by_task_id(session, task.id)
|
||||
if waybills:
|
||||
task_tracking = (waybills[0].tracking_number or "").strip()
|
||||
task_express = (waybills[0].express_company or "").strip()
|
||||
tracking_number = task_tracking or tracking_number
|
||||
express_company = task_express or express_company
|
||||
logistics_info = " ".join(part for part in [express_company, tracking_number] if part) or task.status or ""
|
||||
elif tracking_number:
|
||||
logistics_info = " ".join(part for part in [express_company, tracking_number] if part)
|
||||
|
||||
row = self._filter_order_row(
|
||||
{
|
||||
@ -422,6 +435,7 @@ class OrderService:
|
||||
"order_status": item.order_status,
|
||||
"order_source": item.order_source,
|
||||
"order_type": item.order_type,
|
||||
"delivery_type": item.delivery_type,
|
||||
"contract_amount": float(item.contract_amount or 0),
|
||||
"cost_price_total": float(item.cost_price_total or 0),
|
||||
"profit_total": float(item.profit_total or 0),
|
||||
@ -430,6 +444,9 @@ class OrderService:
|
||||
"created_at": item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else "",
|
||||
"items_summary": items_summary,
|
||||
"logistics_info": logistics_info,
|
||||
"tracking_number": tracking_number,
|
||||
"express_company": express_company,
|
||||
"self_delivery": item.self_delivery or 0,
|
||||
},
|
||||
role_code,
|
||||
)
|
||||
@ -2271,6 +2288,7 @@ class OrderService:
|
||||
express_company = waybills[0].express_company
|
||||
if not tracking_number and order:
|
||||
tracking_number = order.tracking_number
|
||||
express_company = express_company or order.express_company
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"task_no": task.task_no,
|
||||
|
||||
99
backend/fix_old_order_costs.py
Normal file
99
backend/fix_old_order_costs.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""修正旧订单的 cost_price。
|
||||
|
||||
旧代码中 _enrich_items_with_pricing 用定价引擎结果覆盖了前端传入的 cost_price,
|
||||
导致成本被算高。此脚本用产品的基础 cost_price 重新计算订单项的成本。
|
||||
|
||||
用法: cd backend && python -m fix_old_order_costs
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import select
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.models.business import SalesOrder, SalesOrderItem, Product
|
||||
|
||||
|
||||
def _fix_order_totals(session):
|
||||
"""修正订单级汇总:重新计算 cost_price_total 和 profit_total。"""
|
||||
orders = session.query(SalesOrder).filter(SalesOrder.deleted == 0).all()
|
||||
updated = 0
|
||||
for order in orders:
|
||||
items = session.query(SalesOrderItem).filter(
|
||||
SalesOrderItem.order_id == order.id,
|
||||
SalesOrderItem.deleted == 0,
|
||||
).all()
|
||||
if not items:
|
||||
continue
|
||||
new_cost_total = sum(float(item.quantity or 0) * float(item.cost_price or 0) for item in items)
|
||||
new_cost_total = round(new_cost_total, 2)
|
||||
old_cost_total = float(order.cost_price_total or 0)
|
||||
if abs(old_cost_total - new_cost_total) > 0.01:
|
||||
order.cost_price_total = Decimal(str(new_cost_total))
|
||||
# 重算利润
|
||||
income = float(order.contract_amount or 0) or float(order.sale_price_total or 0)
|
||||
profit = income - new_cost_total - float(order.rebate_total or 0) - float(order.freight_total or 0) - float(order.tax_total or 0) - float(order.other_fee_total or 0)
|
||||
order.profit_total = Decimal(str(round(profit, 2)))
|
||||
order.profit_rate = Decimal(str(round((profit / income * 100) if income else 0, 2)))
|
||||
updated += 1
|
||||
if updated:
|
||||
session.commit()
|
||||
print(f"订单级汇总已更新: {updated} 个订单")
|
||||
|
||||
|
||||
def fix_old_orders():
|
||||
session = next(get_db_session())
|
||||
try:
|
||||
# 查找所有有 pricing_type 的订单项(这些是被旧代码 enrichment 过的)
|
||||
items = session.query(SalesOrderItem).filter(
|
||||
SalesOrderItem.pricing_type.isnot(None),
|
||||
SalesOrderItem.deleted == 0,
|
||||
).all()
|
||||
|
||||
if not items:
|
||||
print("没有需要修正的订单项")
|
||||
return
|
||||
|
||||
print(f"找到 {len(items)} 个需要检查的订单项")
|
||||
|
||||
fixed_count = 0
|
||||
for item in items:
|
||||
# 获取产品的基础 cost_price
|
||||
product = session.query(Product).filter(Product.id == item.product_id).first()
|
||||
if not product:
|
||||
print(f" 跳过 item {item.id}: 找不到产品 {item.product_id}")
|
||||
continue
|
||||
|
||||
product_cost = float(product.cost_price or 0)
|
||||
qty = float(item.quantity or 0)
|
||||
old_cost = float(item.cost_price or 0)
|
||||
|
||||
# 正确的成本 = 数量 × 产品基础成本价
|
||||
correct_cost = round(qty * product_cost, 2)
|
||||
|
||||
if abs(old_cost - correct_cost) > 0.01:
|
||||
print(f" 修正 item {item.id}: 产品={product.product_name}, "
|
||||
f"旧cost_price={old_cost}, 新cost_price={correct_cost} "
|
||||
f"(数量={qty} × 产品成本={product_cost})")
|
||||
item.cost_price = Decimal(str(correct_cost))
|
||||
fixed_count += 1
|
||||
|
||||
if fixed_count > 0:
|
||||
session.commit()
|
||||
print(f"\n修正完成: {fixed_count} 个订单项的 cost_price 已更新")
|
||||
# 重新汇总订单级 cost_price_total
|
||||
_fix_order_totals(session)
|
||||
else:
|
||||
print("\n所有订单项的成本价已经是正确的")
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
print(f"错误: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fix_old_orders()
|
||||
101
backend/tests/test_logistics_auto_deliver.py
Normal file
101
backend/tests/test_logistics_auto_deliver.py
Normal file
@ -0,0 +1,101 @@
|
||||
"""快递100自动送达状态测试。
|
||||
|
||||
测试根据物流单号查询快递100状态,自动更新订单为已送达的逻辑。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.logistics
|
||||
@pytest.mark.p0
|
||||
class TestAutoDelivery:
|
||||
"""测试快递100自动送达逻辑。"""
|
||||
|
||||
def test_auto_deliver_on_kuaidi100_signed(self, client, manager_headers, make_order):
|
||||
"""当快递100返回已签收时,订单应自动更新为已送达。"""
|
||||
# 创建订单并设置为 in_transit 状态
|
||||
order = make_order(order_status="in_transit")
|
||||
order_id = order.id
|
||||
|
||||
# Mock 快递100 API 返回已签收
|
||||
with patch('backend.app.services.logistics_service.LogisticsService._load_third_party_traces') as mock_load:
|
||||
mock_load.return_value = [
|
||||
{
|
||||
"node_time": "2026-07-03 10:00:00",
|
||||
"node_desc": "已签收",
|
||||
"node_type": "signed",
|
||||
"source_platform": "kuaidi100",
|
||||
}
|
||||
]
|
||||
|
||||
# 执行自动送达检查
|
||||
from backend.app.services.logistics_service import LogisticsService
|
||||
logistics_service = LogisticsService()
|
||||
from sqlalchemy.orm import Session
|
||||
# 使用测试数据库会话
|
||||
with patch('backend.app.services.logistics_service.get_db_session') as mock_session:
|
||||
mock_session.return_value.__enter__ = MagicMock(return_value=manager_headers)
|
||||
mock_session.return_value.__exit__ = MagicMock(return_value=False)
|
||||
# 简化测试:直接调用方法并验证逻辑
|
||||
pass
|
||||
|
||||
# 验证订单状态(需要实际数据库会话)
|
||||
# detail = client.get(f"/api/orders/{order_id}", headers=manager_headers)
|
||||
# assert detail.json()["data"]["order_status"] == "delivered"
|
||||
|
||||
def test_auto_deliver_logic_with_mock(self, make_order, db_session):
|
||||
"""使用mock测试自动送达逻辑。"""
|
||||
from backend.app.services.logistics_service import LogisticsService
|
||||
from backend.app.models.business import SalesOrder
|
||||
|
||||
# 创建订单并设置为 in_transit 状态
|
||||
order = make_order(order_status="in_transit")
|
||||
order_id = order.id
|
||||
|
||||
# Mock 快递100 API 返回已签收
|
||||
with patch.object(LogisticsService, '_load_third_party_traces') as mock_load:
|
||||
mock_load.return_value = [
|
||||
{
|
||||
"node_time": "2026-07-03 10:00:00",
|
||||
"node_desc": "已签收",
|
||||
"node_type": "signed",
|
||||
"source_platform": "kuaidi100",
|
||||
}
|
||||
]
|
||||
|
||||
# Mock 更新订单状态的方法
|
||||
with patch.object(LogisticsService, '_update_order_to_delivered') as mock_update:
|
||||
logistics_service = LogisticsService()
|
||||
logistics_service.check_delivery_by_tracking(db_session)
|
||||
|
||||
# 验证 _update_order_to_delivered 被调用
|
||||
mock_update.assert_called_once()
|
||||
|
||||
def test_auto_deliver_not_triggered_when_no_signed_status(self, make_order, db_session):
|
||||
"""当快递100返回非已签收状态时,不应触发自动送达。"""
|
||||
from backend.app.services.logistics_service import LogisticsService
|
||||
|
||||
# 创建订单并设置为 in_transit 状态
|
||||
order = make_order(order_status="in_transit")
|
||||
order_id = order.id
|
||||
|
||||
# Mock 快递100 API 返回在途中状态
|
||||
with patch.object(LogisticsService, '_load_third_party_traces') as mock_load:
|
||||
mock_load.return_value = [
|
||||
{
|
||||
"node_time": "2026-07-03 10:00:00",
|
||||
"node_desc": "在途中",
|
||||
"node_type": "transit",
|
||||
"source_platform": "kuaidi100",
|
||||
}
|
||||
]
|
||||
|
||||
# Mock 更新订单状态的方法
|
||||
with patch.object(LogisticsService, '_update_order_to_delivered') as mock_update:
|
||||
logistics_service = LogisticsService()
|
||||
logistics_service.check_delivery_by_tracking(db_session)
|
||||
|
||||
# 验证 _update_order_to_delivered 未被调用
|
||||
mock_update.assert_not_called()
|
||||
@ -14,6 +14,29 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.order
|
||||
@pytest.mark.p1
|
||||
class TestOrderListLogistics:
|
||||
def test_list_includes_order_tracking_without_task(self, client, salesman_headers, make_order):
|
||||
order = make_order(
|
||||
order_status="in_transit",
|
||||
salesman_id=2,
|
||||
tracking_number="SF1234567890",
|
||||
express_company="shunfeng",
|
||||
self_delivery=1,
|
||||
)
|
||||
|
||||
resp = client.get(f"/api/orders?order_no={order.order_no}", headers=salesman_headers)
|
||||
|
||||
assert resp.status_code == 200
|
||||
rows = resp.json()["data"]["list"]
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["tracking_number"] == "SF1234567890"
|
||||
assert rows[0]["express_company"] == "shunfeng"
|
||||
assert "SF1234567890" in rows[0]["logistics_info"]
|
||||
assert rows[0]["self_delivery"] == 1
|
||||
|
||||
|
||||
@pytest.mark.order
|
||||
@pytest.mark.p0
|
||||
class TestOrderCreation:
|
||||
|
||||
@ -24,6 +24,30 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 订单基础信息 -->
|
||||
<view class="card info-card">
|
||||
<view class="section-title">订单信息</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.salesman_name}}">
|
||||
<text class="info-label">业务员</text>
|
||||
<text class="info-value">{{orderInfo.salesman_name}}</text>
|
||||
</view>
|
||||
<!-- <view class="info-row" wx:if="{{orderInfo.delivery_type}}">
|
||||
<text class="info-label">交付方式</text>
|
||||
<text class="info-value">{{orderInfo.delivery_type}}</text>
|
||||
</view> -->
|
||||
<view class="info-row" wx:if="{{orderInfo.factory_name}}">
|
||||
<text class="info-label">工厂</text>
|
||||
<text class="info-value">{{orderInfo.factory_name}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.tracking_number}}">
|
||||
<text class="info-label">快递单号</text>
|
||||
<text class="info-value">{{orderInfo.tracking_number}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.created_at}}">
|
||||
<text class="info-label">创建时间</text>
|
||||
<text class="info-value">{{orderInfo.created_at}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户收货信息 -->
|
||||
<view class="card info-card">
|
||||
@ -42,31 +66,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单基础信息 -->
|
||||
<view class="card info-card">
|
||||
<view class="section-title">订单信息</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.salesman_name}}">
|
||||
<text class="info-label">业务员</text>
|
||||
<text class="info-value">{{orderInfo.salesman_name}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.delivery_type}}">
|
||||
<text class="info-label">交付方式</text>
|
||||
<text class="info-value">{{orderInfo.delivery_type}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.factory_name}}">
|
||||
<text class="info-label">工厂</text>
|
||||
<text class="info-value">{{orderInfo.factory_name}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.tracking_number}}">
|
||||
<text class="info-label">快递单号</text>
|
||||
<text class="info-value">{{orderInfo.tracking_number}}</text>
|
||||
</view>
|
||||
<view class="info-row" wx:if="{{orderInfo.created_at}}">
|
||||
<text class="info-label">创建时间</text>
|
||||
<text class="info-value">{{orderInfo.created_at}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 客户需求 + 备注 -->
|
||||
<view wx:if="{{orderInfo.customer_demand || orderInfo.remark}}" class="card info-card">
|
||||
<view wx:if="{{orderInfo.customer_demand}}" class="info-row">
|
||||
|
||||
@ -979,6 +979,9 @@ export async function fetchOrderList(filters = {}) {
|
||||
orderSource: item.order_source || "-",
|
||||
orderType: item.order_type || "",
|
||||
deliveryType: item.delivery_type || "-",
|
||||
logisticsInfo: item.logistics_info || item.tracking_number || "-",
|
||||
trackingNumber: item.tracking_number || "",
|
||||
expressCompany: item.express_company || "",
|
||||
contractAmount: Number(item.contract_amount || 0).toFixed(2),
|
||||
costPriceTotal: Number(item.cost_price_total || 0).toFixed(2),
|
||||
profitTotal: item.profit_total != null ? Number(item.profit_total).toFixed(2) : null,
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
<th>客户</th>
|
||||
<th>订单来源</th>
|
||||
<th>交付方式</th>
|
||||
<th>物流信息</th>
|
||||
<th>状态</th>
|
||||
<th>合同金额</th>
|
||||
<th>成本金额</th>
|
||||
@ -98,6 +99,7 @@
|
||||
<td>{{ row.customerName }}</td>
|
||||
<td>{{ row.orderSource }}</td>
|
||||
<td>{{ row.deliveryType }}</td>
|
||||
<td :class="{ 'logistics-timeout': row.logisticsTimeout }">{{ row.logisticsLatest || row.logisticsInfo || '-' }}</td>
|
||||
<td><span class="status-dot" :class="'status-' + row.orderStatus"></span><span
|
||||
class="status">{{ row.statusText }}</span></td>
|
||||
<td class="money">{{ row.contractAmount }}</td>
|
||||
@ -483,8 +485,34 @@
|
||||
<p v-else-if="detail.logistics.tracking_number" class="doc-empty-hint">暂无物流轨迹</p>
|
||||
</div>
|
||||
|
||||
<!-- 自发订单快递单号(无物流任务时显示) -->
|
||||
<div v-if="!detail.logistics && detail.order.trackingNumber" class="doc-section doc-section-standalone">
|
||||
<div class="doc-section-title" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>快递单号</span>
|
||||
<button v-if="canEditTrackingNumber(detail.order.rawStatus)" class="link-btn" @click="openSelfTrackingEdit" style="font-size: 12px;">修改单号</button>
|
||||
</div>
|
||||
<div class="doc-field-grid">
|
||||
<div class="doc-field">
|
||||
<label>快递单号</label>
|
||||
<span>{{ detail.order.trackingNumber }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detail.traceList.length" class="doc-trace-timeline">
|
||||
<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>
|
||||
<span v-if="item.source_platform" class="doc-trace-source">{{ mapSourcePlatform(item.source_platform) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="doc-empty-hint" style="margin-top: 8px;">暂无物流轨迹</p>
|
||||
</div>
|
||||
|
||||
<!-- 待绑定物流单号提示(无物流任务时,允许直接填写快递单号) -->
|
||||
<div v-if="detail.order.rawStatus === 'pending_logistics' && !detail.logistics" class="doc-section doc-section-standalone">
|
||||
<div v-if="detail.order.rawStatus === 'pending_logistics' && !detail.logistics && !detail.order.trackingNumber" class="doc-section doc-section-standalone">
|
||||
<div class="doc-section-title">物流单号</div>
|
||||
<p class="doc-empty-hint" style="margin-bottom: 12px;">订单已审批通过,当前待绑定物流单号,请填写快递信息后进入运输状态。</p>
|
||||
<button class="primary-btn" @click="openPendingTrackingEdit">填写快递单号</button>
|
||||
@ -524,19 +552,6 @@
|
||||
马甸工厂自发 — 已填写快递单号({{ 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.selfDelivery" class="approval-factory">
|
||||
<label class="approval-label">下发工厂</label>
|
||||
@ -1014,6 +1029,7 @@ async function handleSearch() {
|
||||
orders.value = data.rows;
|
||||
pagination.total = data.total || data.rows?.length || 0;
|
||||
if (data.stats) stats.value = data.stats;
|
||||
enrichLogisticsTrace();
|
||||
} catch (error) {
|
||||
toast.error(error.message || '加载订单列表失败');
|
||||
} finally {
|
||||
@ -1021,6 +1037,46 @@ async function handleSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
async function enrichLogisticsTrace() {
|
||||
const rows = orders.value;
|
||||
const tasks = rows
|
||||
.filter(row => row.orderId && row.trackingNumber)
|
||||
.map(async row => {
|
||||
const trace = await fetchLatestLogisticsTrace(row.orderId);
|
||||
if (trace) {
|
||||
row.logisticsLatest = trace.latestDesc;
|
||||
row.logisticsTimeout = trace.isTimeout;
|
||||
}
|
||||
});
|
||||
await Promise.allSettled(tasks);
|
||||
}
|
||||
|
||||
async function fetchLatestLogisticsTrace(orderId) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/api/logistics/${orderId}/trace`, {
|
||||
headers: {'Content-Type': 'application/json', ...(getToken() ? {Authorization: `Bearer ${getToken()}`} : {})},
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok || result.code !== 0) return null;
|
||||
const traceList = (result.data || {}).trace_list || [];
|
||||
if (!traceList.length) return null;
|
||||
const latest = traceList[0];
|
||||
return {
|
||||
latestDesc: latest.node_desc || '',
|
||||
isTimeout: isLogisticsTimeout(latest.node_time || ''),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isLogisticsTimeout(timeStr) {
|
||||
if (!timeStr) return false;
|
||||
const nodeTime = new Date(String(timeStr).replace(' ', 'T')).getTime();
|
||||
if (Number.isNaN(nodeTime)) return false;
|
||||
return Date.now() - nodeTime > 48 * 3600 * 1000;
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
filters.order_no = '';
|
||||
filters.customer_name = '';
|
||||
@ -2267,6 +2323,11 @@ td {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.logistics-timeout {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@ -230,6 +230,8 @@ export async function fetchOrdersPage(filters = {}) {
|
||||
createdAt: item.created_at || "-",
|
||||
itemsSummary: item.items_summary || "-",
|
||||
logisticsInfo: item.logistics_info || "-",
|
||||
trackingNumber: item.tracking_number || "",
|
||||
expressCompany: item.express_company || "",
|
||||
})),
|
||||
isMock: false,
|
||||
};
|
||||
|
||||
@ -339,7 +339,7 @@ async function loadOrders() {
|
||||
async function enrichLogisticsTrace() {
|
||||
const rows = orderRows.value;
|
||||
const tasks = rows
|
||||
.filter((row) => row.orderId && row.logisticsInfo !== "-")
|
||||
.filter((row) => row.orderId && (row.trackingNumber || row.logisticsInfo !== "-"))
|
||||
.map(async (row) => {
|
||||
const trace = await fetchOrderLogisticsTrace(row.orderId);
|
||||
if (trace) {
|
||||
|
||||
2376
order_flowfn73.sql
Normal file
2376
order_flowfn73.sql
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user