权限: 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) 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 状态可接收 # 校验:只有 PENDING 状态可接收
if task.status != TASK_STATUS_PENDING: if task.status != TASK_STATUS_PENDING:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"只有待接收(PENDING)状态的任务可接收,当前状态: {task.status}",
)
now = get_beijing_time() now = get_beijing_time()
task.status = TASK_STATUS_WIP task.status = TASK_STATUS_WIP
@ -284,6 +288,13 @@ async def reject_task(
""" """
task = await _get_task_or_404(db, task_id) 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): if task.status in (TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED):
raise HTTPException( raise HTTPException(

View File

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

View File

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