feat(borrow): 转交发起收紧为「仅当前持有人本人」(责任链隔离)
背景
----
此前【转交】只要持有 borrow_transfer 权限就可见可调,与「当前持有人」无关 ——
任何库管都能把别人保管的资产转给第三方,责任链形同虚设。业务方确认改为
**只有该物品的当前持有人本人可以发起**。
改动(前后端同改,缺一不可)
----
· service.transfer_borrow 新增 caller_user_id,强校验其 == 该明细
current_holder_id;传 None 一律拒绝,不做「系统内部调用」的隐式放行。
· get_records 为每条明细附加 can_transfer(当前持有人 == 我)—— 前端
localStorage 里只有 username 没有 user_id,故与 is_mine 一样由后端判定。
· 前端明细行【转交】改判 can_transfer;主行【转交】改为「该单下存在由我持有
的未还物品」时才出现;弹窗候选也过滤为「由我持有」,不是我的不列进来
(后端会拒,列出来只会误导)。
★ 连带调整:移除 route 上的 permission_required('borrow_transfer')
责任链规则既然是「持有人本人」,而持有人是普通员工、通常不持有库管权限,
再加一道库管权限,实际能发起的人变成「持有人 ∩ 库管」,绝大多数持有人
反而发不了 —— 功能形同虚设。这与 accept/reject 同级:员工处置自己名下资产。
真正的边界是 service 层的 caller_user_id 强校验,不是界面遮挡。
⚠ 由此 borrow_transfer 权限码已无任何代码引用(sys_element 中的定义与
4 个角色的授权仍在,属无害冗余)。若后续需要「管理员代办」入口,
可在此基础上加豁免;若确定不需要,该权限码可择期下线。
验证(13 项断言全通过)
----
· 非持有人发起被拒;未传调用者被拒;持有人转给自己被拒
· 持有人本人发起成功,from_user_id 正确记为持有人
· can_transfer:持有人 True / 接收人 False;接收转移后新持有人变 True
· 接收环节不受影响;库存零副作用、数据零残留
This commit is contained in:
@ -152,9 +152,12 @@
|
||||
<el-button type="success" link size="small" @click="acceptOne(c)">接收</el-button>
|
||||
<el-button type="danger" link size="small" @click="rejectOne(c)">拒绝</el-button>
|
||||
</template>
|
||||
<!-- ★ 只有该物品的**当前持有人本人**能发起转交(can_transfer
|
||||
由后端判定:前端 localStorage 里没有 user_id)。
|
||||
后端 transfer_borrow 有同样的强校验 —— 这里只是降噪,
|
||||
真正的边界在服务端。 -->
|
||||
<el-button
|
||||
v-else-if="(c.pending_quantity || 0) > 0 && !c.pending_transfer"
|
||||
v-permission="'borrow_transfer'"
|
||||
v-else-if="(c.pending_quantity || 0) > 0 && !c.pending_transfer && c.can_transfer"
|
||||
type="primary" link size="small"
|
||||
@click="openTransferDialog(props.row, c)"
|
||||
>转交</el-button>
|
||||
@ -292,11 +295,10 @@
|
||||
@click="openScrapDialog(row)"
|
||||
>申请报废</el-button>
|
||||
|
||||
<!-- 主行入口:进入弹窗后勾选具体物品(默认全选可转交项)。
|
||||
精准的单件转交在展开后的明细行操作列里,两者共用同一个弹窗。 -->
|
||||
<!-- 主行入口:仅当该单下有**由我持有**的未还物品时才出现
|
||||
(can_transfer 由后端按「当前持有人 == 我」判定)。 -->
|
||||
<el-button
|
||||
v-if="isUnreturned(row)"
|
||||
v-permission="'borrow_transfer'"
|
||||
v-if="canTransferAny(row)"
|
||||
type="primary" link size="small"
|
||||
@click="openTransferDialog(row)"
|
||||
>转交</el-button>
|
||||
@ -782,6 +784,11 @@ const transferTotalQty = computed(() =>
|
||||
transferSelected.value.reduce((s: number, c: any) => s + (Number(c.pending_quantity) || 0), 0)
|
||||
)
|
||||
|
||||
// 该单下是否有「由我持有、可发起转交」的未还物品 —— 主行【转交】按钮的显示条件。
|
||||
// can_transfer 由后端按「当前持有人 == 我」判定(前端 localStorage 无 user_id)。
|
||||
const canTransferAny = (row: any): boolean =>
|
||||
(row.children || []).some((c: any) => (c.pending_quantity || 0) > 0 && c.can_transfer)
|
||||
|
||||
// 该单下所有「待我接收」的转交 —— 主行聚合用。
|
||||
// (明细行只看自己那一条,见模板里的 c.pending_transfer)
|
||||
const myPendings = (row: any): any[] =>
|
||||
@ -810,9 +817,12 @@ const loadTransferUsers = async () => {
|
||||
* 故这里用勾选而非「整单」。
|
||||
*/
|
||||
const openTransferDialog = async (row: any, detail?: any) => {
|
||||
const candidates = (row.children || []).filter((c: any) => (c.pending_quantity || 0) > 0)
|
||||
// ★ 只列出「由我持有」的未还物品:不是我的,后端会拒,列在弹窗里只会误导。
|
||||
const candidates = (row.children || []).filter(
|
||||
(c: any) => (c.pending_quantity || 0) > 0 && c.can_transfer
|
||||
)
|
||||
if (!candidates.length) {
|
||||
ElMessage.warning('该单号下没有未归还的明细,无法转交')
|
||||
ElMessage.warning('该单号下没有由您持有的未归还物品,无法转交')
|
||||
return
|
||||
}
|
||||
transferBorrowNo.value = row.borrow_no
|
||||
|
||||
Reference in New Issue
Block a user