feat: 合同金额权限控制 + 内置角色权限自动同步
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m18s
Build and Deploy Web / build-and-deploy (push) Successful in 53s

- 新增 project:view_contract 权限,仅超级管理员可查看合同金额
- 项目详情、仪表盘、结算页、创建项目表单均受权限保护
- 启动时自动同步内置角色权限,新增权限无需手动更新数据库

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-02-14 12:10:02 +08:00
parent db56ea1f99
commit 2b990f06fb
6 changed files with 23 additions and 6 deletions

View File

@ -12,6 +12,7 @@ from models import (
Project, ProjectMilestone, DEFAULT_MILESTONES Project, ProjectMilestone, DEFAULT_MILESTONES
) )
from auth import hash_password from auth import hash_password
from sqlalchemy.orm.attributes import flag_modified
import os import os
import logging import logging
@ -108,7 +109,7 @@ def init_roles_and_admin():
from database import SessionLocal from database import SessionLocal
db = SessionLocal() db = SessionLocal()
try: try:
# 初始化内置角色 # 初始化 / 同步内置角色权限
for role_name, role_def in BUILTIN_ROLES.items(): for role_name, role_def in BUILTIN_ROLES.items():
existing = db.query(Role).filter(Role.name == role_name).first() existing = db.query(Role).filter(Role.name == role_name).first()
if not existing: if not existing:
@ -120,6 +121,15 @@ def init_roles_and_admin():
) )
db.add(role) db.add(role)
print(f"[OK] created role: {role_name}") print(f"[OK] created role: {role_name}")
elif existing.is_system:
# 同步内置角色:补充代码中新增的权限
current = set(existing.permissions or [])
defined = set(role_def["permissions"])
missing = defined - current
if missing:
existing.permissions = list(current | missing)
flag_modified(existing, "permissions")
print(f"[SYNC] added permissions to {role_name}: {missing}")
db.commit() db.commit()
# 迁移旧成本权限 → 细分权限 # 迁移旧成本权限 → 细分权限

View File

@ -20,6 +20,7 @@ ALL_PERMISSIONS = [
("project:edit", "编辑项目", "项目管理"), ("project:edit", "编辑项目", "项目管理"),
("project:delete", "删除项目", "项目管理"), ("project:delete", "删除项目", "项目管理"),
("project:complete", "确认完成项目", "项目管理"), ("project:complete", "确认完成项目", "项目管理"),
("project:view_contract", "查看合同金额", "项目管理"),
# 内容提交 # 内容提交
("submission:view", "查看提交记录", "内容提交"), ("submission:view", "查看提交记录", "内容提交"),
("submission:create", "新增提交", "内容提交"), ("submission:create", "新增提交", "内容提交"),

View File

@ -161,7 +161,7 @@
<div class="card-body"> <div class="card-body">
<el-table :data="data.settled_projects" size="small"> <el-table :data="data.settled_projects" size="small">
<el-table-column prop="project_name" label="项目" /> <el-table-column prop="project_name" label="项目" />
<el-table-column label="合同金额" align="right" width="120"> <el-table-column v-if="authStore.hasPermission('project:view_contract')" label="合同金额" align="right" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.contract_amount">¥{{ formatNum(row.contract_amount) }}</span> <span v-if="row.contract_amount">¥{{ formatNum(row.contract_amount) }}</span>
<span v-else class="text-muted"></span> <span v-else class="text-muted"></span>
@ -187,8 +187,11 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue' import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { dashboardApi } from '../api' import { dashboardApi } from '../api'
import { useAuthStore } from '../stores/auth'
import * as echarts from 'echarts' import * as echarts from 'echarts'
const authStore = useAuthStore()
const loading = ref(false) const loading = ref(false)
const data = ref({}) const data = ref({})
const typeTagMap = { '客户正式项目': 'success', '客户测试项目': 'warning', '内部原创项目': '', '内部测试项目': 'info' } const typeTagMap = { '客户正式项目': 'success', '客户测试项目': 'warning', '内部原创项目': '', '内部测试项目': 'info' }

View File

@ -19,7 +19,7 @@
<!-- 项目信息 --> <!-- 项目信息 -->
<div class="card info-card"> <div class="card info-card">
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item" v-if="authStore.hasPermission('project:view_contract')">
<span class="info-label">合同金额</span> <span class="info-label">合同金额</span>
<span class="info-value price">{{ project.contract_amount ? '¥' + project.contract_amount.toLocaleString() : '未设置' }}</span> <span class="info-value price">{{ project.contract_amount ? '¥' + project.contract_amount.toLocaleString() : '未设置' }}</span>
</div> </div>
@ -355,7 +355,7 @@
<el-form-item label="预估完成日期"> <el-form-item label="预估完成日期">
<el-date-picker v-model="editForm.estimated_completion_date" value-format="YYYY-MM-DD" placeholder="选择预计交付日期" style="width:100%" /> <el-date-picker v-model="editForm.estimated_completion_date" value-format="YYYY-MM-DD" placeholder="选择预计交付日期" style="width:100%" />
</el-form-item> </el-form-item>
<el-form-item v-if="editForm.project_type === '客户正式项目'" label="合同金额"> <el-form-item v-if="editForm.project_type === '客户正式项目' && authStore.hasPermission('project:view_contract')" label="合同金额">
<el-input-number v-model="editForm.contract_amount" :min="0" :step="10000" :controls="false" placeholder="甲方合同总金额(元)" style="width:100%" /> <el-input-number v-model="editForm.contract_amount" :min="0" :step="10000" :controls="false" placeholder="甲方合同总金额(元)" style="width:100%" />
</el-form-item> </el-form-item>
</el-form> </el-form>

View File

@ -90,7 +90,7 @@
<el-form-item label="预估完成日期"> <el-form-item label="预估完成日期">
<el-date-picker v-model="form.estimated_completion_date" value-format="YYYY-MM-DD" placeholder="选择预计交付日期" style="width:100%" /> <el-date-picker v-model="form.estimated_completion_date" value-format="YYYY-MM-DD" placeholder="选择预计交付日期" style="width:100%" />
</el-form-item> </el-form-item>
<el-form-item v-if="form.project_type === '客户正式项目'" label="合同金额"> <el-form-item v-if="form.project_type === '客户正式项目' && authStore.hasPermission('project:view_contract')" label="合同金额">
<el-input-number v-model="form.contract_amount" :min="0" :step="10000" :controls="false" placeholder="甲方合同总金额(元)" style="width:100%" /> <el-input-number v-model="form.contract_amount" :min="0" :step="10000" :controls="false" placeholder="甲方合同总金额(元)" style="width:100%" />
</el-form-item> </el-form-item>

View File

@ -39,7 +39,7 @@
</el-row> </el-row>
<!-- 盈亏仅客户正式项目 --> <!-- 盈亏仅客户正式项目 -->
<el-card v-if="data.contract_amount != null" class="section-card"> <el-card v-if="data.contract_amount != null && authStore.hasPermission('project:view_contract')" class="section-card">
<template #header><span class="section-title">项目盈亏</span></template> <template #header><span class="section-title">项目盈亏</span></template>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="8"> <el-col :span="8">
@ -102,6 +102,9 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { projectApi } from '../api' import { projectApi } from '../api'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const route = useRoute() const route = useRoute()
const loading = ref(false) const loading = ref(false)