权限: receive/reject校验assignee_id匹配 + 前端myActiveTask只显示本人任务 + 403处理

This commit is contained in:
2026-08-05 16:21:27 +08:00
parent 62422e7558
commit 71e8eeb7e8
3 changed files with 51 additions and 16 deletions

View File

@ -238,12 +238,16 @@ async def receive_task(
"""
task = await _get_task_or_404(db, task_id)
# 权限校验:只有负责人本人可接收
if operator_id and task.assignee_id and operator_id != task.assignee_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"您无权操作此任务,当前任务负责人为 {task.assignee_id}",
)
# 校验:只有 PENDING 状态可接收
if task.status != TASK_STATUS_PENDING:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"只有待接收(PENDING)状态的任务可接收,当前状态: {task.status}",
)
now = get_beijing_time()
task.status = TASK_STATUS_WIP
@ -284,6 +288,13 @@ async def reject_task(
"""
task = await _get_task_or_404(db, task_id)
# 权限校验:只有负责人本人可驳回
if operator_id and task.assignee_id and operator_id != task.assignee_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"您无权操作此任务,当前任务负责人为 {task.assignee_id}",
)
# 校验:不能重复驳回已完成/已驳回的任务
if task.status in (TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED):
raise HTTPException(

View File

@ -90,21 +90,21 @@
<!-- ================================================================ -->
<!-- 底部操作栏钉死不随滚动 -->
<!-- ================================================================ -->
<view v-if="activeTask" class="footer-actions">
<button v-if="activeTask.status === 'WIP'" class="footer-btn footer-transfer"
@tap="handleTaskAction({ task: activeTask, type: 'transfer' })">
<view v-if="myActiveTask" class="footer-actions">
<button v-if="myActiveTask.status === 'WIP'" class="footer-btn footer-transfer"
@tap="handleTaskAction({ task: myActiveTask, type: 'transfer' })">
🔄 完工转交
</button>
<button v-if="activeTask.status === 'WIP'" class="footer-btn footer-record"
@tap="handleTaskAction({ task: activeTask, type: 'record' })">
<button v-if="myActiveTask.status === 'WIP'" class="footer-btn footer-record"
@tap="handleTaskAction({ task: myActiveTask, type: 'record' })">
📝 记录/拍照
</button>
<button v-if="activeTask.status === 'PENDING'" class="footer-btn footer-receive"
@tap="handleTaskAction({ task: activeTask, type: 'receive' })">
<button v-if="myActiveTask.status === 'PENDING'" class="footer-btn footer-receive"
@tap="handleTaskAction({ task: myActiveTask, type: 'receive' })">
接收任务
</button>
<button v-if="activeTask.status === 'PENDING'" class="footer-btn footer-reject"
@tap="handleTaskAction({ task: activeTask, type: 'reject' })">
<button v-if="myActiveTask.status === 'PENDING'" class="footer-btn footer-reject"
@tap="handleTaskAction({ task: myActiveTask, type: 'reject' })">
驳回任务
</button>
</view>
@ -379,7 +379,7 @@ export default {
});
},
activeTask() {
// 查找树中第一个 WIP 或 PENDING 任务作为底部 Footer 的操作目标
// 查找树中第一个 WIP 或 PENDING 任务(不限归属)
const findActive = (tasks) => {
if (!tasks) return null;
for (const t of tasks) {
@ -393,6 +393,24 @@ export default {
};
return this.product ? findActive(this.product.task_tree) : null;
},
myActiveTask() {
// 只返回当前登录人自己的 WIP/PENDING 任务
const findMine = (tasks) => {
if (!tasks) return null;
for (const t of tasks) {
const isMine = t.status === 'WIP' || t.status === 'PENDING';
const matchId = t.assignee_id == this.currentUserId;
const matchName = t.assignee_id == this.currentUsername;
if (isMine && (matchId || matchName)) return t;
if (t.child_tasks && t.child_tasks.length) {
const found = findMine(t.child_tasks);
if (found) return found;
}
}
return null;
};
return this.product ? findMine(this.product.task_tree) : null;
},
},
onLoad(options) {
this.loadUsers();
@ -516,7 +534,8 @@ export default {
// 场景B: 立即接收
if (this.firstForm.autoReceive) {
try {
await post(`/tasks/${task.id}/receive`);
const opId = this.currentUsername || this.currentUserId;
await post(`/tasks/${task.id}/receive?operator_id=${encodeURIComponent(opId)}`);
} catch { /* receive 失败不影响流程 */ }
}
uni.showToast({
@ -696,7 +715,8 @@ export default {
this.actionLoading = true;
try {
const remark = this.receiveRemark.trim() || undefined;
await post(`/tasks/${this.actionPopup.task.id}/receive`, { remark });
const opId = this.currentUsername || this.currentUserId;
await post(`/tasks/${this.actionPopup.task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark });
uni.showToast({ title: "已接收", icon: "success" });
this.closeActionPopup();
this.doQuery(this.product.serial_number);
@ -705,7 +725,8 @@ export default {
async doReject() {
this.actionLoading = true;
try {
await post(`/tasks/${this.actionPopup.task.id}/reject`, { reason: this.rejectReason.trim() });
const opId = this.currentUsername || this.currentUserId;
await post(`/tasks/${this.actionPopup.task.id}/reject?operator_id=${encodeURIComponent(opId)}`, { reason: this.rejectReason.trim() });
uni.showToast({ title: "已驳回,返工任务已创建", icon: "success" });
this.closeActionPopup();
this.doQuery(this.product.serial_number);

View File

@ -30,6 +30,9 @@ export default function request(options) {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" });
setTimeout(() => uni.reLaunch({ url: "/pages/login/login" }), 1000);
reject(res);
} else if (code === 403) {
uni.showToast({ title: res.data?.detail || "无权操作", icon: "none", duration: 3000 });
reject(res);
} else if (code === 400) {
uni.showToast({ title: res.data?.detail || "请求参数有误", icon: "none", duration: 2500 });
reject(res);