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.api.deps import get_logistics_service, require_permissions, require_roles
|
||||||
from backend.app.db import get_db_session
|
from backend.app.db import get_db_session
|
||||||
from backend.app.schemas.common import success_payload
|
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
|
from backend.app.services.logistics_service import LogisticsService
|
||||||
|
|
||||||
router = APIRouter(tags=["logistics"])
|
router = APIRouter(tags=["logistics"])
|
||||||
@ -113,6 +113,32 @@ def get_logistics_task(
|
|||||||
return success_payload(logistics_service.get_task(task_id, session, current_user))
|
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")
|
@router.get("/api/driver/tasks")
|
||||||
def list_driver_tasks(
|
def list_driver_tasks(
|
||||||
logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例
|
logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例
|
||||||
|
|||||||
@ -113,3 +113,16 @@ class CreateLogisticsTraceRequest(BaseModel):
|
|||||||
|
|
||||||
source_platform: str
|
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)
|
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(
|
def accept_task(
|
||||||
self,
|
self,
|
||||||
task_id: int,
|
task_id: int,
|
||||||
|
|||||||
@ -74,6 +74,17 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</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">
|
<section v-if="!loading" class="detail-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>订单明细</h3>
|
<h3>订单明细</h3>
|
||||||
@ -149,6 +160,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -157,8 +190,11 @@ import { computed, onMounted, ref } from "vue";
|
|||||||
import { RouterLink, useRoute, useRouter } from "vue-router";
|
import { RouterLink, useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
import ConfirmModal from "../components/ConfirmModal.vue";
|
import ConfirmModal from "../components/ConfirmModal.vue";
|
||||||
|
import { apiBaseUrl } from "../config";
|
||||||
import { cancelOrder, fetchOrderDetail, submitOrder } from "../mockApi";
|
import { cancelOrder, fetchOrderDetail, submitOrder } from "../mockApi";
|
||||||
|
|
||||||
|
const SALES_TOKEN_KEY = "sales_token";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
@ -175,6 +211,11 @@ const message = ref("");
|
|||||||
const messageType = ref("success");
|
const messageType = ref("success");
|
||||||
const orderMeta = ref({ customer: "-", logistics: "-", factory: "-" });
|
const orderMeta = ref({ customer: "-", logistics: "-", factory: "-" });
|
||||||
const isModal = computed(() => route.query.modal === "1");
|
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) {
|
function canSubmit(status) {
|
||||||
return ["draft", "rejected"].includes(status);
|
return ["draft", "rejected"].includes(status);
|
||||||
@ -208,6 +249,14 @@ async function loadDetail() {
|
|||||||
logistics: data.blocks?.[3]?.content || "-",
|
logistics: data.blocks?.[3]?.content || "-",
|
||||||
factory: 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() {
|
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() {
|
function handleClose() {
|
||||||
router.push("/orders");
|
router.push("/orders");
|
||||||
}
|
}
|
||||||
@ -658,4 +738,11 @@ p {
|
|||||||
.confirm-btn { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: #fff; border-color: transparent; }
|
.confirm-btn { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: #fff; border-color: transparent; }
|
||||||
.confirm-btn.danger { background: linear-gradient(135deg, #dc2626, #b91c1c); }
|
.confirm-btn.danger { background: linear-gradient(135deg, #dc2626, #b91c1c); }
|
||||||
.confirm-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
.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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user