「权限 × 状态 × 审计」联合约束机制

This commit is contained in:
Cheng Zhou
2025-12-17 21:48:29 +08:00
parent 13efb0a3a9
commit 4202ed7922
36 changed files with 714 additions and 85 deletions
@@ -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>
+1 -3
View File
@@ -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>