6-27-中午提交,修复订单流程记录

This commit is contained in:
wsb1224 2026-06-27 15:31:35 +08:00
parent 47e4d33f09
commit d67cdd6123
17 changed files with 130 additions and 62 deletions

View File

@ -125,8 +125,8 @@ def order_stats(
"pending_driver": stats.get("pending_driver", 0),
"accepted": stats.get("accepted", 0),
"picked_up": stats.get("picked_up", 0),
"delivered": stats.get("delivered", 0),
"production": stats.get("production", 0),
"pending_logistics": stats.get("pending_logistics", 0),
"in_transit": stats.get("in_transit", 0),
"shipped": stats.get("shipped", 0),
"completed": stats.get("completed", 0),
"pending_settle": stats.get("pending_settle", 0),

View File

@ -115,15 +115,15 @@ class ArrearsService:
"""判断当前订单状态是否满足欠款生成条件。
参数
trigger_mode: 触发模式'shipped'已发货 'delivered'已送达
trigger_mode: 触发模式'shipped'已发货 'pending_logistics'待绑定物流单号
order_status: 订单当前状态
返回
满足条件返回 True否则返回 False
"""
if trigger_mode == "shipped":
return order_status in {"shipped", "delivered", "completed", "settled"}
return order_status in {"delivered", "completed", "settled"}
return order_status in {"shipped", "pending_logistics", "in_transit", "completed", "settled"}
return order_status in {"pending_logistics", "in_transit", "completed", "settled"}
arrears_service = ArrearsService()

View File

@ -152,7 +152,7 @@ class AuthService:
self.revoked_tokens = self._load_revoked_tokens()
# 后台管理系统允许登录的角色列表
ADMIN_ALLOWED_ROLES = {"admin", "manager", "kuaiji", "wuliu"}
ADMIN_ALLOWED_ROLES = {"admin", "manager", "kuaiji", "wuliu", "salesman"}
def login(self, username: str, password: str, role_type: str, session: Session | None = None) -> dict | None:
"""用户登录。

View File

@ -32,7 +32,7 @@ DEFAULT_ROLES = [
# ---------------------------------------------------------------------------
DEFAULT_CONFIGS = [
{"config_key": "arrears_generate_mode", "config_value": "delivered", "config_name": "欠款生成模式", "remark": "shipped=发货后记账,delivered=送达后记账", "status": 1},
{"config_key": "arrears_generate_mode", "config_value": "pending_logistics", "config_name": "欠款生成模式", "remark": "shipped=发货后记账,pending_logistics=待绑定物流单号后记账", "status": 1},
{"config_key": "arrears_settlement_start_type", "config_value": "order_date", "config_name": "欠款起算节点", "remark": "order_date=订单日期delivery_date=发货日期reconciliation_date=对账日期", "status": 1},
{"config_key": "arrears_overdue_days", "config_value": "30", "config_name": "欠款超期天数", "remark": "超过该天数未结算触发欠款提醒", "status": 1},
{"config_key": "logistics_timeout_hours", "config_value": "48", "config_name": "物流超时小时数", "remark": "首个物流节点超过该小时数无流转提醒", "status": 1},

View File

@ -181,8 +181,8 @@ class ConfigService:
if not value:
raise AppException(code=ErrorCode.PARAM_ERROR, message="配置值不能为空", status_code=400)
if config_key == "arrears_generate_mode" and value not in {"shipped", "delivered"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="欠款生成模式仅支持 shipped 或 delivered", status_code=400)
if config_key == "arrears_generate_mode" and value not in {"shipped", "pending_logistics"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="欠款生成模式仅支持 shipped 或 pending_logistics", status_code=400)
if config_key in {"logistics_timeout_hours", "inactive_customer_days"}:
if not value.isdigit() or int(value) <= 0:
raise AppException(code=ErrorCode.PARAM_ERROR, message="配置值必须为正整数", status_code=400)

View File

@ -47,6 +47,7 @@ class LogisticsService:
self.settings = get_settings()
self.logistics_repository = LogisticsRepository()
self.order_repository = OrderRepository()
self.reminder_repository = ReminderRepository()
self.trace_records: dict[int, list] = {}
def list_tasks(
@ -455,6 +456,13 @@ class LogisticsService:
task.tracking_number = new_tracking
task.express_company = (payload.get("express_company") or "").strip() or None
# 如果订单处于待绑定物流单号状态,绑定快递单号后自动变为运输中
order = self.order_repository.get_order(session, task.order_id)
if order and order.order_status == "pending_logistics":
before_status = order.order_status
self.order_repository.update_order_status(session, order, "in_transit")
self._notify_status_change(session, order, before_status, "in_transit", current_user.get("user_id") if current_user else None)
audit_service.write_log(
session,
{
@ -585,7 +593,7 @@ class LogisticsService:
) -> dict:
"""司机送达确认。
任务状态从 picked_up 变更为 delivered订单状态同步变更
任务状态从 picked_up 变更为 delivered订单状态变更pending_logistics
追加物流轨迹节点"司机已送达"同步欠款数据
Args:
@ -604,7 +612,7 @@ class LogisticsService:
task_id,
"picked_up",
"delivered",
"delivered",
"pending_logistics",
session,
current_user,
operate_payload,
@ -776,7 +784,7 @@ class LogisticsService:
"remark": operate_payload.get("remark") or f"司机任务状态变更为 {target_status}",
},
)
if target_status == "delivered":
if order_status == "pending_logistics":
arrears_service.sync_order_arrears(session, order)
# 触发物流状态更新事件
@ -1183,6 +1191,41 @@ class LogisticsService:
return "司机已送达"
return "物流状态更新"
def _notify_status_change(self, session: Session, order, before_status: str, after_status: str, operator_id: int | None) -> None:
"""订单状态变更时生成提醒通知。
向业务员发送状态变更通知
Args:
session: 数据库会话
order: 订单 ORM 对象
before_status: 变更前状态
after_status: 变更后状态
operator_id: 操作人 ID
"""
_STATUS_LABELS = {
"pending_logistics": "待绑定物流单号",
"in_transit": "运输中",
}
before_label = _STATUS_LABELS.get(before_status, before_status)
after_label = _STATUS_LABELS.get(after_status, after_status)
receiver = order.salesman_id or 0
if receiver > 0:
reminder_content = f"订单 {order.order_no} 状态由「{before_label}」变更为「{after_label}」。"
self.reminder_repository.create_reminder(
session,
{
"reminder_type": "order_status_change",
"receiver_id": receiver,
"title": "订单状态变更",
"content": reminder_content,
"biz_type": "sales_order",
"biz_id": order.id,
"source_platform": "backend",
},
)
def _build_trace_payload(self, task_id: int, node_desc: str, node_type: str, payload: dict | None) -> dict:
"""构建物流轨迹节点数据载荷。

View File

@ -529,7 +529,7 @@ class OrderService:
_DIRECT_CANCEL_STATUSES = {"draft", "pending_approve"}
_APPROVED_CANCEL_STATUSES = {"approved"}
_FULFILLMENT_CANCEL_STATUSES = {"pending_factory", "pending_driver", "accepted", "picked_up", "delivered", "production", "shipped"}
_FULFILLMENT_CANCEL_STATUSES = {"pending_factory", "pending_driver", "accepted", "picked_up", "pending_logistics", "in_transit", "shipped"}
if order.order_status in _DIRECT_CANCEL_STATUSES:
next_status = "canceled"
@ -624,9 +624,9 @@ class OrderService:
previous_status = order.order_status
if payload["approve_result"] == "pass":
# 自发订单(已填快递单号)审批通过后直接完成,无需工厂/司机环节
# 自发订单(已填快递单号)审批通过后直接进入运输中,无需工厂/司机环节
if order.tracking_number:
target_status = "completed"
target_status = "in_transit"
elif payload.get("factory_id") and payload.get("driver_id"):
# 有工厂+司机:审批通过并直接创建物流任务,跳到待司机接单
target_status = "pending_driver"
@ -1054,6 +1054,12 @@ class OrderService:
old_tracking = order.tracking_number or ""
order.tracking_number = new_tracking
# 如果订单处于待绑定物流单号状态,绑定快递单号后自动变为运输中
if order.order_status == "pending_logistics":
before_status = order.order_status
self.order_repository.update_order_status(session, order, "in_transit")
self._notify_status_change(session, order, before_status, "in_transit", current_user.get("user_id") if current_user else None)
audit_service.write_log(
session,
{
@ -1112,8 +1118,8 @@ class OrderService:
"pending_driver",
"accepted",
"picked_up",
"delivered",
"production",
"pending_logistics",
"in_transit",
"shipped",
"completed",
"pending_settle",
@ -1359,11 +1365,12 @@ class OrderService:
"pending_driver": "待司机接单",
"accepted": "已接单",
"picked_up": "已揽货",
"pending_logistics": "待绑定物流单号",
"in_transit": "运输中",
"delivered": "已送达",
"canceled": "已取消",
"cancel_pending": "取消审批中",
"cancel_fulfillment_pending": "履约取消审批中",
"production": "生产中",
"shipped": "已发货",
"completed": "已完成",
"pending_settle": "待结算",
@ -1386,7 +1393,7 @@ class OrderService:
before_label = self._STATUS_LABELS.get(before_status, before_status)
after_label = self._STATUS_LABELS.get(after_status, after_status)
_NOTIFY_MANAGER_STATUSES = {"pending_approve", "cancel_pending", "cancel_fulfillment_pending"}
_NOTIFY_SALESMAN_STATUSES = {"approved", "rejected", "canceled", "delivered", "completed", "settled", "pending_factory", "pending_driver", "accepted", "picked_up", "production", "shipped"}
_NOTIFY_SALESMAN_STATUSES = {"approved", "rejected", "canceled", "pending_logistics", "in_transit", "completed", "settled", "pending_factory", "pending_driver", "accepted", "picked_up", "shipped"}
receiver = order.salesman_id or 0
if receiver > 0 and after_status in _NOTIFY_SALESMAN_STATUSES | _NOTIFY_MANAGER_STATUSES:
@ -1567,16 +1574,16 @@ class OrderService:
# 需求 6.1 定义的合法状态流转路径
_VALID_STATUS_TRANSITIONS = {
"draft": {"pending_approve", "canceled"},
"pending_approve": {"approved", "rejected", "canceled", "pending_factory", "completed"},
"pending_approve": {"approved", "rejected", "canceled", "pending_factory", "in_transit"},
"rejected": {"draft", "pending_approve"},
"approved": {"pending_factory", "cancel_pending", "canceled"},
"pending_factory": {"pending_driver", "production", "cancel_fulfillment_pending", "cancel_pending"},
"pending_factory": {"pending_driver", "cancel_fulfillment_pending", "cancel_pending"},
"pending_driver": {"accepted", "cancel_fulfillment_pending"},
"accepted": {"picked_up", "cancel_fulfillment_pending"},
"picked_up": {"delivered", "cancel_fulfillment_pending"},
"delivered": {"completed", "cancel_fulfillment_pending"},
"production": {"shipped", "cancel_fulfillment_pending"},
"shipped": {"delivered", "completed", "cancel_fulfillment_pending"},
"picked_up": {"pending_logistics", "cancel_fulfillment_pending"},
"pending_logistics": {"in_transit", "cancel_fulfillment_pending"},
"in_transit": {"shipped", "cancel_fulfillment_pending"},
"shipped": {"completed", "cancel_fulfillment_pending"},
"completed": {"pending_settle", "settled"},
"pending_settle": {"settled"},
"settled": set(),

View File

@ -18,7 +18,8 @@ function getStatusBadge(status) {
approved: "badge-green", rejected: "badge-red",
pending_factory: "badge-blue", pending_driver: "badge-purple",
accepted: "badge-blue", picked_up: "badge-teal",
delivered: "badge-green", completed: "badge-green",
pending_logistics: "badge-orange", in_transit: "badge-blue",
completed: "badge-green",
pending_settle: "badge-orange", settled: "badge-purple",
canceled: "badge-red",
};

View File

@ -280,19 +280,19 @@
<!-- 已揽货:确认送达 -->
<block wx:elif="{{orderInfo.order_status === 'picked_up'}}">
<button class="action-btn cancel-btn" loading="{{actionLoading}}" bindtap="handleApplyCancel">申请取消</button>
<button class="action-btn pass-btn" type="primary" loading="{{actionLoading}}" bindtap="handleSimpleTransition" data-target="delivered">确认送达</button>
<button class="action-btn pass-btn" type="primary" loading="{{actionLoading}}" bindtap="handleSimpleTransition" data-target="pending_logistics">确认送达</button>
</block>
<!-- 已送达:标记生产 -->
<block wx:elif="{{orderInfo.order_status === 'delivered'}}">
<!-- 待绑定物流单号:绑定物流单号 -->
<block wx:elif="{{orderInfo.order_status === 'pending_logistics'}}">
<button class="action-btn cancel-btn" loading="{{actionLoading}}" bindtap="handleApplyCancel">申请取消</button>
<button class="action-btn pass-btn" type="primary" loading="{{actionLoading}}" bindtap="handleSimpleTransition" data-target="production">标记生产</button>
<button class="action-btn pass-btn" type="primary" loading="{{actionLoading}}" bindtap="handleSimpleTransition" data-target="in_transit">绑定物流单号</button>
</block>
<!-- 生产中:标记发货 -->
<block wx:elif="{{orderInfo.order_status === 'production'}}">
<!-- 运输中:确认收货 -->
<block wx:elif="{{orderInfo.order_status === 'in_transit'}}">
<button class="action-btn cancel-btn" loading="{{actionLoading}}" bindtap="handleApplyCancel">申请取消</button>
<button class="action-btn pass-btn" type="primary" loading="{{actionLoading}}" bindtap="handleSimpleTransition" data-target="shipped">标记发货</button>
<button class="action-btn pass-btn" type="primary" loading="{{actionLoading}}" bindtap="handleSimpleTransition" data-target="shipped">确认收货</button>
</block>
<!-- 已发货:标记完成 -->

View File

@ -9,6 +9,8 @@ var statusTabs = [
{ key: "approved", label: "已审批" },
{ key: "rejected", label: "已驳回" },
{ key: "pending_factory", label: "待发厂" },
{ key: "pending_logistics", label: "待绑定物流" },
{ key: "in_transit", label: "运输中" },
{ key: "pending_settle", label: "待结算" },
{ key: "canceled", label: "已取消" },
];
@ -17,7 +19,8 @@ var badgeColorMap = {
pending_approve: "badge-orange", cancel_pending: "badge-orange",
approved: "badge-green", rejected: "badge-red",
pending_factory: "badge-blue", pending_driver: "badge-blue",
delivered: "badge-green", canceled: "badge-red",
pending_logistics: "badge-orange", in_transit: "badge-blue",
canceled: "badge-red",
pending_settle: "badge-orange", settled: "badge-green",
};

View File

@ -26,8 +26,8 @@ function mapOrderStatus(status) {
pending_driver: "待司机接单",
accepted: "已接单",
picked_up: "已揽货",
delivered: "已送达",
production: "生产中",
pending_logistics: "待绑定物流单号",
in_transit: "运输中",
shipped: "已发货",
completed: "已完成",
pending_settle: "待结算",
@ -60,8 +60,8 @@ function getOrderStatusBadge(status) {
pending_driver: "badge-blue",
accepted: "badge-blue",
picked_up: "badge-blue",
delivered: "badge-green",
production: "badge-blue",
pending_logistics: "badge-orange",
in_transit: "badge-blue",
shipped: "badge-blue",
completed: "badge-green",
pending_settle: "badge-orange",

View File

@ -126,8 +126,8 @@ const statusTextMap = {
draft: '草稿', pending_approve: '待审批', approved: '已审批', rejected: '已驳回',
cancel_pending: '待取消审批', cancel_fulfillment_pending: '履约取消审批中',
canceled: '已取消', pending_factory: '待工厂处理', pending_driver: '待司机接单',
accepted: '已接单', picked_up: '已揽货', delivered: '已送达',
production: '生产中', shipped: '已发货', completed: '已完成',
accepted: '已接单', picked_up: '已揽货', pending_logistics: '待绑定物流单号',
in_transit: '运输中', shipped: '已发货', completed: '已完成',
pending_settle: '待结算', settled: '已结算',
};

View File

@ -55,6 +55,8 @@
<option value="cancel_fulfillment_pending">履约取消审批</option>
<option value="pending_factory">待工厂处理</option>
<option value="pending_driver">待司机接单</option>
<option value="pending_logistics">待绑定物流单号</option>
<option value="in_transit">运输中</option>
<option value="pending_settle">待结算</option>
<option value="canceled">已取消</option>
</select>
@ -192,13 +194,13 @@
@click="handleDetailStatusChange('picked_up')">确认揽货
</button>
<button v-if="detail.order.rawStatus === 'picked_up'" class="primary-btn" :disabled="detailActionBusy"
@click="handleDetailStatusChange('delivered')">确认送达
@click="handleDetailStatusChange('pending_logistics')">确认送达
</button>
<button v-if="detail.order.rawStatus === 'delivered'" class="primary-btn" :disabled="detailActionBusy"
@click="handleDetailStatusChange('production')">标记生产
<button v-if="detail.order.rawStatus === 'pending_logistics'" class="primary-btn" :disabled="detailActionBusy"
@click="handleDetailStatusChange('in_transit')">绑定物流单号
</button>
<button v-if="detail.order.rawStatus === 'production'" class="primary-btn" :disabled="detailActionBusy"
@click="handleDetailStatusChange('shipped')">标记发
<button v-if="detail.order.rawStatus === 'in_transit'" class="primary-btn" :disabled="detailActionBusy"
@click="handleDetailStatusChange('shipped')">确认收
</button>
<button v-if="detail.order.rawStatus === 'shipped'" class="primary-btn" :disabled="detailActionBusy"
@click="handleDetailStatusChange('completed')">标记完成
@ -1021,8 +1023,8 @@ const statusTextMap = {
draft: '草稿', pending_approve: '待审批', approved: '已审批', rejected: '已驳回',
cancel_pending: '待取消审批', cancel_fulfillment_pending: '履约取消审批中',
canceled: '已取消', pending_factory: '待工厂处理', pending_driver: '待司机接单',
accepted: '已接单', picked_up: '已揽货', delivered: '已送达',
production: '生产中', shipped: '已发货', completed: '已完成',
accepted: '已接单', picked_up: '已揽货', pending_logistics: '待绑定物流单号',
in_transit: '运输中', shipped: '已发货', completed: '已完成',
pending_settle: '待结算', settled: '已结算',
};
@ -2136,12 +2138,12 @@ td {
background: #14b8a6;
}
.status-delivered {
background: #22c55e;
.status-pending_logistics {
background: #f97316;
}
.status-production {
background: #eab308;
.status-in_transit {
background: #3b82f6;
}
.status-shipped {
@ -2482,14 +2484,14 @@ textarea {
color: #0f766e;
}
.badge-delivered {
background: #f0fdf4;
color: #15803d;
.badge-pending_logistics {
background: #fff7ed;
color: #c2410c;
}
.badge-production {
background: #fefce8;
color: #a16207;
.badge-in_transit {
background: #eff6ff;
color: #1d4ed8;
}
.badge-shipped {

View File

@ -73,8 +73,8 @@ function mapOrderStatus(status) {
pending_driver: "待司机接单",
accepted: "已接单",
picked_up: "已揽货",
delivered: "已送达",
production: "生产中",
pending_logistics: "待绑定物流单号",
in_transit: "运输中",
shipped: "已发货",
completed: "已完成",
pending_settle: "待结算",

View File

@ -174,7 +174,7 @@ const greeting = computed(() => {
* @returns {string} 对应的中文状态名称
*/
function mapStatus(s) {
return { draft: "草稿", pending_approve: "待审批", approved: "已审批", rejected: "已驳回", canceled: "已取消", pending_factory: "待工厂处理" }[s] || s || "-";
return { draft: "草稿", pending_approve: "待审批", approved: "已审批", rejected: "已驳回", canceled: "已取消", pending_factory: "待工厂处理", pending_logistics: "待绑定物流单号", in_transit: "运输中" }[s] || s || "-";
}
/**

View File

@ -279,7 +279,7 @@ function canSubmit(status) {
}
function canCancel(status) {
return ["draft", "pending_approve", "approved", "rejected", "cancel_pending", "pending_factory"].includes(status);
return ["draft", "pending_approve", "approved", "rejected", "cancel_pending", "pending_factory", "pending_driver", "accepted", "picked_up", "pending_logistics", "in_transit", "shipped"].includes(status);
}
function canEdit(status) {
@ -561,6 +561,16 @@ onUnmounted(() => {
color: #3730a3;
}
.status-tag.pending_logistics {
background: #fff7ed;
color: #c2410c;
}
.status-tag.in_transit {
background: #eff6ff;
color: #1d4ed8;
}
.action-group {
display: flex;
gap: 10px;

View File

@ -245,6 +245,8 @@ const filterFields = [
{ value: "cancel_fulfillment_pending", label: "履约取消审批中" },
{ value: "canceled", label: "已取消" },
{ value: "pending_factory", label: "待工厂处理" },
{ value: "pending_logistics", label: "待绑定物流单号" },
{ value: "in_transit", label: "运输中" },
],
},
{ key: "customer_name", label: "客户姓名", type: "text", placeholder: "请输入客户姓名" },
@ -259,7 +261,7 @@ function canSubmit(status) {
}
function canCancel(status) {
return ["draft", "pending_approve", "approved", "pending_factory", "pending_driver", "accepted", "picked_up", "delivered", "production", "shipped"].includes(status);
return ["draft", "pending_approve", "approved", "pending_factory", "pending_driver", "accepted", "picked_up", "pending_logistics", "in_transit", "shipped"].includes(status);
}
function statusClass(status) {