7-6 订单物流修复
This commit is contained in:
parent
ea1c5a7835
commit
8fc64fee8f
@ -95,6 +95,7 @@ class AuthService:
|
|||||||
"ai:recognize",
|
"ai:recognize",
|
||||||
"ai:correct",
|
"ai:correct",
|
||||||
"ai:parse-order",
|
"ai:parse-order",
|
||||||
|
"logistics:trace:list",
|
||||||
],
|
],
|
||||||
"driver": [
|
"driver": [
|
||||||
"driver:task:list",
|
"driver:task:list",
|
||||||
@ -135,6 +136,7 @@ class AuthService:
|
|||||||
"ai:recognize",
|
"ai:recognize",
|
||||||
"ai:correct",
|
"ai:correct",
|
||||||
"ai:parse-order",
|
"ai:parse-order",
|
||||||
|
"logistics:trace:list",
|
||||||
],
|
],
|
||||||
"kuaiji": [
|
"kuaiji": [
|
||||||
"dashboard:view",
|
"dashboard:view",
|
||||||
@ -154,6 +156,7 @@ class AuthService:
|
|||||||
"ai:parse-order",
|
"ai:parse-order",
|
||||||
"reminder:list",
|
"reminder:list",
|
||||||
"config:view",
|
"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": 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": 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": 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},
|
{"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:recognize",
|
||||||
"ai:correct",
|
"ai:correct",
|
||||||
"ai:parse-order",
|
"ai:parse-order",
|
||||||
|
"logistics:trace:list",
|
||||||
|
],
|
||||||
|
"driver": [
|
||||||
|
"logistics:trace:list",
|
||||||
],
|
],
|
||||||
"driver": [],
|
|
||||||
"kuaiji": [
|
"kuaiji": [
|
||||||
"dashboard:view",
|
"dashboard:view",
|
||||||
"order:list",
|
"order:list",
|
||||||
@ -203,6 +207,7 @@ DEFAULT_ROLE_MENU_CODES = {
|
|||||||
"ai:parse-order",
|
"ai:parse-order",
|
||||||
"reminder:list",
|
"reminder:list",
|
||||||
"config:view",
|
"config:view",
|
||||||
|
"logistics:trace:list",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -406,11 +406,24 @@ class OrderService:
|
|||||||
if len(order_items) > 2:
|
if len(order_items) > 2:
|
||||||
items_summary += "..."
|
items_summary += "..."
|
||||||
|
|
||||||
# 获取物流信息
|
# 获取物流信息。列表用单号触发前端查询最新轨迹,自发订单回退到 sales_order。
|
||||||
task = self.logistics_repository.get_task_by_order_id(session, item.id)
|
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 = ""
|
logistics_info = ""
|
||||||
if task:
|
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(
|
row = self._filter_order_row(
|
||||||
{
|
{
|
||||||
@ -422,6 +435,7 @@ class OrderService:
|
|||||||
"order_status": item.order_status,
|
"order_status": item.order_status,
|
||||||
"order_source": item.order_source,
|
"order_source": item.order_source,
|
||||||
"order_type": item.order_type,
|
"order_type": item.order_type,
|
||||||
|
"delivery_type": item.delivery_type,
|
||||||
"contract_amount": float(item.contract_amount or 0),
|
"contract_amount": float(item.contract_amount or 0),
|
||||||
"cost_price_total": float(item.cost_price_total or 0),
|
"cost_price_total": float(item.cost_price_total or 0),
|
||||||
"profit_total": float(item.profit_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 "",
|
"created_at": item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else "",
|
||||||
"items_summary": items_summary,
|
"items_summary": items_summary,
|
||||||
"logistics_info": logistics_info,
|
"logistics_info": logistics_info,
|
||||||
|
"tracking_number": tracking_number,
|
||||||
|
"express_company": express_company,
|
||||||
|
"self_delivery": item.self_delivery or 0,
|
||||||
},
|
},
|
||||||
role_code,
|
role_code,
|
||||||
)
|
)
|
||||||
@ -2271,6 +2288,7 @@ class OrderService:
|
|||||||
express_company = waybills[0].express_company
|
express_company = waybills[0].express_company
|
||||||
if not tracking_number and order:
|
if not tracking_number and order:
|
||||||
tracking_number = order.tracking_number
|
tracking_number = order.tracking_number
|
||||||
|
express_company = express_company or order.express_company
|
||||||
return {
|
return {
|
||||||
"task_id": task.id,
|
"task_id": task.id,
|
||||||
"task_no": task.task_no,
|
"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
|
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.order
|
||||||
@pytest.mark.p0
|
@pytest.mark.p0
|
||||||
class TestOrderCreation:
|
class TestOrderCreation:
|
||||||
|
|||||||
@ -24,6 +24,30 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</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">
|
<view class="card info-card">
|
||||||
@ -42,31 +66,7 @@
|
|||||||
</view>
|
</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 wx:if="{{orderInfo.customer_demand || orderInfo.remark}}" class="card info-card">
|
<view wx:if="{{orderInfo.customer_demand || orderInfo.remark}}" class="card info-card">
|
||||||
<view wx:if="{{orderInfo.customer_demand}}" class="info-row">
|
<view wx:if="{{orderInfo.customer_demand}}" class="info-row">
|
||||||
|
|||||||
@ -979,6 +979,9 @@ export async function fetchOrderList(filters = {}) {
|
|||||||
orderSource: item.order_source || "-",
|
orderSource: item.order_source || "-",
|
||||||
orderType: item.order_type || "",
|
orderType: item.order_type || "",
|
||||||
deliveryType: item.delivery_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),
|
contractAmount: Number(item.contract_amount || 0).toFixed(2),
|
||||||
costPriceTotal: Number(item.cost_price_total || 0).toFixed(2),
|
costPriceTotal: Number(item.cost_price_total || 0).toFixed(2),
|
||||||
profitTotal: item.profit_total != null ? Number(item.profit_total).toFixed(2) : null,
|
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>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>合同金额</th>
|
<th>合同金额</th>
|
||||||
<th>成本金额</th>
|
<th>成本金额</th>
|
||||||
@ -98,6 +99,7 @@
|
|||||||
<td>{{ row.customerName }}</td>
|
<td>{{ row.customerName }}</td>
|
||||||
<td>{{ row.orderSource }}</td>
|
<td>{{ row.orderSource }}</td>
|
||||||
<td>{{ row.deliveryType }}</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
|
<td><span class="status-dot" :class="'status-' + row.orderStatus"></span><span
|
||||||
class="status">{{ row.statusText }}</span></td>
|
class="status">{{ row.statusText }}</span></td>
|
||||||
<td class="money">{{ row.contractAmount }}</td>
|
<td class="money">{{ row.contractAmount }}</td>
|
||||||
@ -483,8 +485,34 @@
|
|||||||
<p v-else-if="detail.logistics.tracking_number" class="doc-empty-hint">暂无物流轨迹</p>
|
<p v-else-if="detail.logistics.tracking_number" class="doc-empty-hint">暂无物流轨迹</p>
|
||||||
</div>
|
</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>
|
<div class="doc-section-title">物流单号</div>
|
||||||
<p class="doc-empty-hint" style="margin-bottom: 12px;">订单已审批通过,当前待绑定物流单号,请填写快递信息后进入运输状态。</p>
|
<p class="doc-empty-hint" style="margin-bottom: 12px;">订单已审批通过,当前待绑定物流单号,请填写快递信息后进入运输状态。</p>
|
||||||
<button class="primary-btn" @click="openPendingTrackingEdit">填写快递单号</button>
|
<button class="primary-btn" @click="openPendingTrackingEdit">填写快递单号</button>
|
||||||
@ -524,19 +552,6 @@
|
|||||||
马甸工厂自发 — 已填写快递单号({{ 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>
|
<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.selfDelivery" class="approval-factory">
|
<div v-if="!detail.order.selfDelivery" class="approval-factory">
|
||||||
<label class="approval-label">下发工厂</label>
|
<label class="approval-label">下发工厂</label>
|
||||||
@ -1014,6 +1029,7 @@ async function handleSearch() {
|
|||||||
orders.value = data.rows;
|
orders.value = data.rows;
|
||||||
pagination.total = data.total || data.rows?.length || 0;
|
pagination.total = data.total || data.rows?.length || 0;
|
||||||
if (data.stats) stats.value = data.stats;
|
if (data.stats) stats.value = data.stats;
|
||||||
|
enrichLogisticsTrace();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.message || '加载订单列表失败');
|
toast.error(error.message || '加载订单列表失败');
|
||||||
} finally {
|
} 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() {
|
function handleReset() {
|
||||||
filters.order_no = '';
|
filters.order_no = '';
|
||||||
filters.customer_name = '';
|
filters.customer_name = '';
|
||||||
@ -2267,6 +2323,11 @@ td {
|
|||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logistics-timeout {
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@ -230,6 +230,8 @@ export async function fetchOrdersPage(filters = {}) {
|
|||||||
createdAt: item.created_at || "-",
|
createdAt: item.created_at || "-",
|
||||||
itemsSummary: item.items_summary || "-",
|
itemsSummary: item.items_summary || "-",
|
||||||
logisticsInfo: item.logistics_info || "-",
|
logisticsInfo: item.logistics_info || "-",
|
||||||
|
trackingNumber: item.tracking_number || "",
|
||||||
|
expressCompany: item.express_company || "",
|
||||||
})),
|
})),
|
||||||
isMock: false,
|
isMock: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -339,7 +339,7 @@ async function loadOrders() {
|
|||||||
async function enrichLogisticsTrace() {
|
async function enrichLogisticsTrace() {
|
||||||
const rows = orderRows.value;
|
const rows = orderRows.value;
|
||||||
const tasks = rows
|
const tasks = rows
|
||||||
.filter((row) => row.orderId && row.logisticsInfo !== "-")
|
.filter((row) => row.orderId && (row.trackingNumber || row.logisticsInfo !== "-"))
|
||||||
.map(async (row) => {
|
.map(async (row) => {
|
||||||
const trace = await fetchOrderLogisticsTrace(row.orderId);
|
const trace = await fetchOrderLogisticsTrace(row.orderId);
|
||||||
if (trace) {
|
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