Merge branch 'claude/update-tracking'
# Conflicts: # frontend/web-sales/src/views/OrderDetailPage.vue
This commit is contained in:
commit
6788e1f5b4
@ -16,7 +16,7 @@ from sqlalchemy.orm import Session
|
||||
from backend.app.api.deps import get_logistics_service, require_permissions, require_roles
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.schemas.common import success_payload
|
||||
from backend.app.schemas.logistics import CreateLogisticsTaskRequest, CreateLogisticsTraceRequest, DriverTaskOperateRequest
|
||||
from backend.app.schemas.logistics import CreateLogisticsTaskRequest, CreateLogisticsTraceRequest, DriverTaskOperateRequest, UpdateTrackingRequest
|
||||
from backend.app.services.logistics_service import LogisticsService
|
||||
|
||||
router = APIRouter(tags=["logistics"])
|
||||
@ -113,6 +113,32 @@ def get_logistics_task(
|
||||
return success_payload(logistics_service.get_task(task_id, session, current_user))
|
||||
|
||||
|
||||
@router.put("/api/logistics/tasks/{task_id}/tracking")
|
||||
def update_tracking_number(
|
||||
task_id: int, # 物流任务ID
|
||||
payload: UpdateTrackingRequest, # 修改物流单号请求体
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
current_user: dict = Depends(require_roles("salesman", "manager", "admin")),
|
||||
) -> dict:
|
||||
"""
|
||||
修改物流单号
|
||||
|
||||
业务员或管理员修改已有物流任务的快递单号和快递公司。
|
||||
业务员仅可修改自己订单关联的物流任务。
|
||||
|
||||
请求参数:
|
||||
task_id: 路径参数,物流任务ID
|
||||
payload: UpdateTrackingRequest 请求体
|
||||
|
||||
返回值:
|
||||
更新后的物流任务信息
|
||||
|
||||
权限要求:salesman、manager、admin 角色
|
||||
"""
|
||||
return success_payload(logistics_service.update_tracking(task_id, payload.model_dump(), session, current_user))
|
||||
|
||||
|
||||
@router.get("/api/driver/tasks")
|
||||
def list_driver_tasks(
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例
|
||||
|
||||
@ -113,3 +113,16 @@ class CreateLogisticsTraceRequest(BaseModel):
|
||||
|
||||
source_platform: str
|
||||
"""来源平台"""
|
||||
|
||||
|
||||
class UpdateTrackingRequest(BaseModel):
|
||||
"""修改物流单号请求体。被 PUT /api/logistics/tasks/{id}/tracking 路由使用。"""
|
||||
|
||||
tracking_number: str
|
||||
"""新的物流追踪单号"""
|
||||
|
||||
express_company: str | None = None
|
||||
"""快递公司"""
|
||||
|
||||
remark: str | None = None
|
||||
"""修改原因/备注"""
|
||||
|
||||
@ -285,6 +285,70 @@ class LogisticsService:
|
||||
"""
|
||||
return self.get_task(task_id, session=session, current_user=current_user)
|
||||
|
||||
def update_tracking(
|
||||
self,
|
||||
task_id: int,
|
||||
payload: dict,
|
||||
session: Session | None = None,
|
||||
current_user: dict | None = None,
|
||||
) -> dict:
|
||||
"""修改物流单号。
|
||||
|
||||
业务员/管理员修改已有物流任务的快递单号和快递公司。
|
||||
业务员只能修改自己订单关联的任务。
|
||||
|
||||
Args:
|
||||
task_id: 物流任务 ID
|
||||
payload: 包含 tracking_number、express_company、remark 的字典
|
||||
session: 数据库会话
|
||||
current_user: 当前登录用户信息
|
||||
|
||||
Returns:
|
||||
更新后的任务信息字典
|
||||
|
||||
被调用路由: logistics.py - PUT /logistics/tasks/{task_id}/tracking
|
||||
"""
|
||||
if session is None:
|
||||
raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500)
|
||||
|
||||
task = self.logistics_repository.get_task(session, task_id)
|
||||
if task is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="物流任务不存在", status_code=404)
|
||||
|
||||
# 权限校验:salesman 只能改自己订单的物流单号
|
||||
if current_user and current_user.get("role_code") == "salesman":
|
||||
order = self.order_repository.get_order(session, task.order_id)
|
||||
if order and order.salesman_id != current_user.get("user_id"):
|
||||
raise AppException(code=ErrorCode.FORBIDDEN, message="无权修改他人订单的物流单号", status_code=403)
|
||||
|
||||
new_tracking = (payload.get("tracking_number") or "").strip()
|
||||
if not new_tracking:
|
||||
raise AppException(code=ErrorCode.PARAM_ERROR, message="物流单号不能为空", status_code=400)
|
||||
|
||||
old_tracking = task.tracking_number or ""
|
||||
old_express = task.express_company or ""
|
||||
task.tracking_number = new_tracking
|
||||
task.express_company = (payload.get("express_company") or "").strip() or None
|
||||
|
||||
audit_service.write_log(
|
||||
session,
|
||||
{
|
||||
"operate_type": "tracking_update",
|
||||
"biz_type": "logistics_task",
|
||||
"biz_id": task.id,
|
||||
"before_value": {"tracking_number": old_tracking, "express_company": old_express},
|
||||
"after_value": {"tracking_number": new_tracking, "express_company": task.express_company},
|
||||
"remark": payload.get("remark") or f"修改物流单号:{old_tracking} → {new_tracking}",
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"order_id": task.order_id,
|
||||
"tracking_number": task.tracking_number,
|
||||
"express_company": task.express_company or "",
|
||||
}
|
||||
|
||||
def accept_task(
|
||||
self,
|
||||
task_id: int,
|
||||
|
||||
@ -74,6 +74,17 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && trackingInfo" class="tracking-card">
|
||||
<div class="tracking-header">
|
||||
<h3>物流信息</h3>
|
||||
<button class="link-btn" @click="openTrackingEdit">修改单号</button>
|
||||
</div>
|
||||
<div class="tracking-fields">
|
||||
<span>快递公司:<strong>{{ trackingInfo.express_company || '-' }}</strong></span>
|
||||
<span>快递单号:<strong>{{ trackingInfo.tracking_number || '-' }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="!loading" class="detail-panel">
|
||||
<div class="panel-header">
|
||||
<h3>订单明细</h3>
|
||||
@ -149,6 +160,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showTrackingEdit" class="modal-mask" @click.self="showTrackingEdit = false">
|
||||
<div class="modal-box">
|
||||
<h3>修改物流单号</h3>
|
||||
<label class="reason-label">
|
||||
<span>快递公司</span>
|
||||
<input v-model.trim="trackingForm.express_company" type="text" placeholder="如:顺丰、圆通" />
|
||||
</label>
|
||||
<label class="reason-label">
|
||||
<span>快递单号</span>
|
||||
<input v-model.trim="trackingForm.tracking_number" type="text" placeholder="请输入新快递单号" />
|
||||
</label>
|
||||
<label class="reason-label">
|
||||
<span>修改原因</span>
|
||||
<input v-model.trim="trackingForm.remark" type="text" placeholder="选填" />
|
||||
</label>
|
||||
<div class="modal-actions">
|
||||
<button class="cancel-btn" @click="showTrackingEdit = false">取消</button>
|
||||
<button class="confirm-btn" :disabled="!trackingForm.tracking_number || trackingSaving" @click="doUpdateTracking">{{ trackingSaving ? '保存中...' : '确认修改' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -157,8 +190,11 @@ import { computed, onMounted, ref } from "vue";
|
||||
import { RouterLink, useRoute, useRouter } from "vue-router";
|
||||
|
||||
import ConfirmModal from "../components/ConfirmModal.vue";
|
||||
import { apiBaseUrl } from "../config";
|
||||
import { cancelOrder, fetchOrderDetail, submitOrder } from "../mockApi";
|
||||
|
||||
const SALES_TOKEN_KEY = "sales_token";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(true);
|
||||
@ -175,6 +211,11 @@ const message = ref("");
|
||||
const messageType = ref("success");
|
||||
const orderMeta = ref({ customer: "-", logistics: "-", factory: "-" });
|
||||
const isModal = computed(() => route.query.modal === "1");
|
||||
const trackingInfo = ref(null);
|
||||
const showTrackingEdit = ref(false);
|
||||
const trackingSaving = ref(false);
|
||||
const trackingForm = reactive({ tracking_number: "", express_company: "", remark: "" });
|
||||
const currentTaskId = ref(null);
|
||||
|
||||
function canSubmit(status) {
|
||||
return ["draft", "rejected"].includes(status);
|
||||
@ -208,6 +249,14 @@ async function loadDetail() {
|
||||
logistics: data.blocks?.[3]?.content || "-",
|
||||
factory: data.blocks?.[3]?.content || "-",
|
||||
};
|
||||
const task = data.logistics_info?.task;
|
||||
if (task && task.task_id) {
|
||||
trackingInfo.value = { tracking_number: task.tracking_number || "", express_company: task.express_company || "" };
|
||||
currentTaskId.value = task.task_id;
|
||||
} else {
|
||||
trackingInfo.value = null;
|
||||
currentTaskId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
@ -274,6 +323,37 @@ async function doCancel() {
|
||||
}
|
||||
}
|
||||
|
||||
function openTrackingEdit() {
|
||||
trackingForm.tracking_number = trackingInfo.value?.tracking_number || "";
|
||||
trackingForm.express_company = trackingInfo.value?.express_company || "";
|
||||
trackingForm.remark = "";
|
||||
showTrackingEdit.value = true;
|
||||
}
|
||||
|
||||
async function doUpdateTracking() {
|
||||
if (!trackingForm.tracking_number || !currentTaskId.value) return;
|
||||
trackingSaving.value = true;
|
||||
try {
|
||||
const token = localStorage.getItem(SALES_TOKEN_KEY) || "";
|
||||
const res = await fetch(`${apiBaseUrl}/api/logistics/tasks/${currentTaskId.value}/tracking`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||
body: JSON.stringify({ tracking_number: trackingForm.tracking_number, express_company: trackingForm.express_company, remark: trackingForm.remark }),
|
||||
});
|
||||
const result = await res.json().catch(() => ({}));
|
||||
if (!res.ok || result.code !== 0) throw new Error(result.message || "修改失败");
|
||||
showTrackingEdit.value = false;
|
||||
await loadDetail();
|
||||
message.value = "物流单号已更新";
|
||||
messageType.value = "success";
|
||||
} catch (error) {
|
||||
message.value = error.message || "修改物流单号失败";
|
||||
messageType.value = "error";
|
||||
} finally {
|
||||
trackingSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
router.push("/orders");
|
||||
}
|
||||
@ -658,4 +738,11 @@ p {
|
||||
.confirm-btn { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: #fff; border-color: transparent; }
|
||||
.confirm-btn.danger { background: linear-gradient(135deg, #dc2626, #b91c1c); }
|
||||
.confirm-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.tracking-card { border: 1px solid #dbe3ef; border-radius: 18px; padding: 16px; background: #f0fdf4; }
|
||||
.tracking-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.tracking-header h3 { margin: 0; font-size: 16px; }
|
||||
.tracking-fields { display: flex; gap: 24px; color: #475569; }
|
||||
.tracking-fields strong { color: #1d4ed8; }
|
||||
.link-btn { background: none; border: none; color: #2563eb; cursor: pointer; font-size: 13px; text-decoration: underline; }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user