「权限 × 状态 × 审计」联合约束机制
This commit is contained in:
@@ -20,14 +20,18 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { computed, ref } from "vue";
|
||||
import { changeFinanceStatus } from "../api/finance";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import PermissionAction from "./PermissionAction.vue";
|
||||
import { financeMachine, getAvailableActions } from "../state-machine";
|
||||
import type { ActionConfig } from "../state-machine";
|
||||
import { evaluateAction } from "../guards/actionGuard";
|
||||
import { logAudit } from "../audit";
|
||||
|
||||
const props = defineProps<{ item: any }>();
|
||||
const emit = defineEmits(["success"]);
|
||||
|
||||
const study = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
const loading = ref(false);
|
||||
const permissionMap: Record<string, string> = {
|
||||
submit: "finance.create",
|
||||
@@ -46,20 +50,69 @@ const buttonType = (action: ActionConfig) => {
|
||||
};
|
||||
|
||||
const change = async (status: string, reject_reason?: string) => {
|
||||
if (!study.currentStudy) return;
|
||||
if (!study.currentStudy) return false;
|
||||
loading.value = true;
|
||||
try {
|
||||
await changeFinanceStatus(study.currentStudy.id, props.item.id, { status, reject_reason });
|
||||
ElMessage.success("状态已更新");
|
||||
emit("success");
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "更新失败");
|
||||
const msg = e?.response?.data?.detail || e?.response?.data?.message || "更新失败";
|
||||
ElMessage.error(msg);
|
||||
if (e?.response?.status === 403) {
|
||||
logAudit("UNAUTHORIZED_ACTION_ATTEMPT", {
|
||||
targetId: props.item.id,
|
||||
targetName: props.item.title || props.item.id,
|
||||
reason: msg,
|
||||
severity: "violation",
|
||||
});
|
||||
} else {
|
||||
logAudit("FINANCE_STATUS_CHANGED", {
|
||||
targetId: props.item.id,
|
||||
targetName: props.item.title || props.item.id,
|
||||
reason: msg,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onAction = async (action: ActionConfig) => {
|
||||
if (!props.item) return;
|
||||
const prevStatus = props.item.status;
|
||||
if (prevStatus === action.to) {
|
||||
ElMessage.warning("当前状态不允许该操作");
|
||||
logAudit("INVALID_STATE_TRANSITION_ATTEMPT", {
|
||||
targetId: props.item.id,
|
||||
targetName: props.item.title || props.item.id,
|
||||
before: { status: prevStatus },
|
||||
after: { status: action.to },
|
||||
severity: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const decision = evaluateAction({
|
||||
actorRole: auth.user?.role || null,
|
||||
requiredPermission: permissionMap[action.key],
|
||||
stateMachine: financeMachine,
|
||||
currentState: props.item.status,
|
||||
actionKey: action.key,
|
||||
target: { id: props.item.id },
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
ElMessage.warning(decision.reason || "当前不可执行该操作");
|
||||
logAudit(decision.auditType, {
|
||||
targetId: props.item.id,
|
||||
targetName: props.item.title || props.item.id,
|
||||
reason: decision.reason,
|
||||
severity: decision.severity,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action.confirm) {
|
||||
const ok = await ElMessageBox.confirm(action.confirmText || "确认执行该操作?", "提示", {
|
||||
type: action.danger ? "warning" : "info",
|
||||
@@ -72,10 +125,24 @@ const onAction = async (action: ActionConfig) => {
|
||||
cancelButtonText: "取消",
|
||||
}).catch(() => null);
|
||||
if (!reason || !reason.value) return;
|
||||
await change(action.to, reason.value);
|
||||
const ok = await change(action.to, reason.value);
|
||||
logAudit("FINANCE_STATUS_CHANGED", {
|
||||
targetId: props.item.id,
|
||||
targetName: props.item.title || props.item.id,
|
||||
before: { status: prevStatus },
|
||||
after: { status: action.to },
|
||||
severity: ok ? "normal" : "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await change(action.to);
|
||||
const ok = await change(action.to);
|
||||
logAudit("FINANCE_STATUS_CHANGED", {
|
||||
targetId: props.item.id,
|
||||
targetName: props.item.title || props.item.id,
|
||||
before: { status: prevStatus },
|
||||
after: { status: action.to },
|
||||
severity: ok ? "normal" : "warning",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/study/finance">费用</el-menu-item>
|
||||
<el-menu-item index="/study/faq">知识库</el-menu-item>
|
||||
<el-menu-item v-if="isAdmin || isPm" index="/study/audit-logs">
|
||||
审计日志
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="isAdmin || isPm" index="/study/audit-logs">审计日志</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
Reference in New Issue
Block a user