Compare commits

..

1 Commits

Author SHA1 Message Date
hiderfong 3ae151404b fix: optimize compliance scan performance and improve error handling
- Refactor scan_compliance to eliminate N+1 queries using joinedload and batch loading
- Add try-except wrapper in compliance scan API endpoint
- Improve frontend axios error interceptor to display detail/message/timeout errors
- Update CORS config and nginx for domain deployment
2026-04-25 20:49:38 +08:00
8 changed files with 54 additions and 136 deletions
+26 -33
View File
@@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, Query, HTTPException, status from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
@@ -89,6 +89,7 @@ def delete_project(
): ):
p = project_service.get_project(db, project_id) p = project_service.get_project(db, project_id)
if not p: if not p:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
# Only admin or project creator can delete # Only admin or project creator can delete
if not _is_admin(current_user) and p.created_by != current_user.id: if not _is_admin(current_user) and p.created_by != current_user.id:
@@ -109,40 +110,32 @@ def project_auto_classify(
project = project_service.get_project(db, project_id) project = project_service.get_project(db, project_id)
if not project: if not project:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
try: if background:
if background: # Check if already running
# Check if already running if project.celery_task_id:
if project.celery_task_id: existing = AsyncResult(project.celery_task_id)
from app.tasks.worker import celery_app if existing.state in ("PENDING", "PROGRESS", "STARTED"):
existing = AsyncResult(project.celery_task_id, app=celery_app) return ResponseModel(data={"task_id": project.celery_task_id, "status": existing.state})
if existing.state in ("PENDING", "PROGRESS", "STARTED"):
return ResponseModel(data={"task_id": project.celery_task_id, "status": existing.state})
task = auto_classify_task.delay(project_id) task = auto_classify_task.delay(project_id)
project.celery_task_id = task.id project.celery_task_id = task.id
project.status = "scanning" project.status = "scanning"
db.commit() db.commit()
return ResponseModel(data={"task_id": task.id, "status": task.state}) return ResponseModel(data={"task_id": task.id, "status": task.state})
else:
from app.services.classification_engine import run_auto_classification
project.status = "scanning"
db.commit()
result = run_auto_classification(db, project_id)
if result.get("success"):
project.status = "assigning"
else: else:
from app.services.classification_engine import run_auto_classification project.status = "created"
project.status = "scanning" db.commit()
db.commit() return ResponseModel(data=result)
result = run_auto_classification(db, project_id)
if result.get("success"):
project.status = "assigning"
else:
project.status = "created"
db.commit()
return ResponseModel(data=result)
except Exception as e:
import logging
logging.exception("Auto classify failed")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"自动分类执行失败: {str(e)}"
)
@router.get("/{project_id}/auto-classify-status") @router.get("/{project_id}/auto-classify-status")
@@ -156,6 +149,7 @@ def project_auto_classify_status(
project = project_service.get_project(db, project_id) project = project_service.get_project(db, project_id)
if not project: if not project:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
task_id = project.celery_task_id task_id = project.celery_task_id
@@ -164,8 +158,7 @@ def project_auto_classify_status(
progress = json.loads(project.scan_progress) if project.scan_progress else None progress = json.loads(project.scan_progress) if project.scan_progress else None
return ResponseModel(data={"status": project.status, "progress": progress}) return ResponseModel(data={"status": project.status, "progress": progress})
from app.tasks.worker import celery_app result = AsyncResult(task_id)
result = AsyncResult(task_id, app=celery_app)
progress = None progress = None
if result.state == "PROGRESS" and result.info: if result.state == "PROGRESS" and result.info:
progress = result.info progress = result.info
+6 -28
View File
@@ -4,7 +4,7 @@ from fastapi import HTTPException, status
from app.models.user import User, Role, Dept, UserRole from app.models.user import User, Role, Dept, UserRole
from app.schemas.user import UserCreate, UserUpdate from app.schemas.user import UserCreate, UserUpdate
from app.core.security import get_password_hash, verify_password from app.core.security import get_password_hash
def get_user_by_id(db: Session, user_id: int) -> Optional[User]: def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
@@ -105,10 +105,9 @@ def create_initial_data(db: Session):
db.commit() db.commit()
# Create or sync the configured bootstrap superuser. # Create superuser
from app.core.config import settings from app.core.config import settings
superuser = get_user_by_username(db, settings.FIRST_SUPERUSER_USERNAME) if not get_user_by_username(db, settings.FIRST_SUPERUSER_USERNAME):
if not superuser:
superuser = User( superuser = User(
username=settings.FIRST_SUPERUSER_USERNAME, username=settings.FIRST_SUPERUSER_USERNAME,
email=settings.FIRST_SUPERUSER_EMAIL, email=settings.FIRST_SUPERUSER_EMAIL,
@@ -122,28 +121,7 @@ def create_initial_data(db: Session):
db.commit() db.commit()
db.refresh(superuser) db.refresh(superuser)
else: superadmin_role = db.query(Role).filter(Role.code == "superadmin").first()
changed = False if superadmin_role:
if not verify_password(settings.FIRST_SUPERUSER_PASSWORD, superuser.hashed_password): db.add(UserRole(user_id=superuser.id, role_id=superadmin_role.id))
superuser.hashed_password = get_password_hash(settings.FIRST_SUPERUSER_PASSWORD)
changed = True
if superuser.email != settings.FIRST_SUPERUSER_EMAIL:
superuser.email = settings.FIRST_SUPERUSER_EMAIL
changed = True
if not superuser.is_active:
superuser.is_active = True
changed = True
if not superuser.is_superuser:
superuser.is_superuser = True
changed = True
if superuser.dept_id is None:
superuser.dept_id = 1
changed = True
if changed:
db.commit() db.commit()
db.refresh(superuser)
superadmin_role = db.query(Role).filter(Role.code == "superadmin").first()
if superadmin_role and superadmin_role not in superuser.roles:
db.add(UserRole(user_id=superuser.id, role_id=superadmin_role.id))
db.commit()
+1 -1
View File
@@ -35,7 +35,7 @@ export function deleteProject(id: number) {
} }
export function autoClassifyProject(id: number, background: boolean = true) { export function autoClassifyProject(id: number, background: boolean = true) {
return request.post(`/projects/${id}/auto-classify`, undefined, { params: { background } }) return request.post(`/projects/${id}/auto-classify`, null, { params: { background } })
} }
export function getAutoClassifyStatus(id: number) { export function getAutoClassifyStatus(id: number) {
+12 -40
View File
@@ -1,38 +1,15 @@
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios' import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
type HandledError = Error & {
handled?: boolean
status?: number
}
const request = axios.create({ const request = axios.create({
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api/v1', baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api/v1',
timeout: 30000, timeout: 30000,
}) })
function isAuthEndpoint(url?: string) {
return !!url && ['/auth/login', '/auth/refresh'].some((path) => url.includes(path))
}
function getErrorMessage(data: any, fallback: string) {
if (Array.isArray(data?.detail)) {
return data.detail.map((d: any) => d.msg || JSON.stringify(d)).join(', ')
}
return data?.detail || data?.message || fallback
}
function makeHandledError(message: string, status?: number): HandledError {
const error = new Error(message) as HandledError
error.handled = true
error.status = status
return error
}
request.interceptors.request.use( request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('dp_token') const token = localStorage.getItem('dp_token')
if (token && config.headers && !isAuthEndpoint(config.url)) { if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`
} }
return config return config
@@ -46,31 +23,26 @@ request.interceptors.response.use(
(response) => { (response) => {
const res = response.data const res = response.data
if (res.code !== 200) { if (res.code !== 200) {
const message = res.message || '请求失败' ElMessage.error(res.message || '请求失败')
ElMessage.error(message) return Promise.reject(new Error(res.message))
return Promise.reject(makeHandledError(message))
} }
return res return res
}, },
(error: AxiosError) => { (error: AxiosError) => {
const status = error.response?.status const status = error.response?.status
const data = error.response?.data as any
const message = getErrorMessage(data, error.message || '网络错误')
if (status === 401) { if (status === 401) {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('dp_token') localStorage.removeItem('dp_token')
localStorage.removeItem('dp_refresh') localStorage.removeItem('dp_refresh')
window.location.href = '/login'
if (isAuthEndpoint(error.config?.url)) { } else {
return Promise.reject(new Error(message || '用户名或密码错误')) const data = error.response?.data as any
} const detail = Array.isArray(data?.detail)
? data.detail.map((d: any) => d.msg || JSON.stringify(d)).join(', ')
const expiredMessage = '登录已过期,请重新登录' : data?.detail
ElMessage.error(expiredMessage) ElMessage.error(detail || data?.message || error.message || '网络错误')
return Promise.reject(makeHandledError(expiredMessage, status))
} }
return Promise.reject(error)
ElMessage.error(message)
return Promise.reject(makeHandledError(message, status))
} }
) )
+2 -16
View File
@@ -129,7 +129,7 @@ const router = createRouter({
routes, routes,
}) })
router.beforeEach(async (to, from, next) => { router.beforeEach((to, from, next) => {
const userStore = useUserStore() const userStore = useUserStore()
// Public routes (login) // Public routes (login)
@@ -143,27 +143,13 @@ router.beforeEach(async (to, from, next) => {
next('/login') next('/login')
return return
} }
if (!userStore.userInfo) {
try {
const userInfo = await userStore.fetchUserInfo()
if (!userInfo) {
userStore.logout()
next('/login')
return
}
} catch {
next('/login')
return
}
}
// Check role permissions // Check role permissions
const allowedRoles = to.meta.roles as string[] | undefined const allowedRoles = to.meta.roles as string[] | undefined
if (allowedRoles && allowedRoles.length > 0) { if (allowedRoles && allowedRoles.length > 0) {
const hasPermission = userStore.hasAnyRole(allowedRoles) const hasPermission = userStore.hasAnyRole(allowedRoles)
if (!hasPermission) { if (!hasPermission) {
next(to.path === '/dashboard' ? '/login' : '/dashboard') next('/dashboard')
return return
} }
} }
+4 -12
View File
@@ -28,7 +28,6 @@ export const useUserStore = defineStore('user', () => {
} }
async function login(username: string, password: string) { async function login(username: string, password: string) {
logout()
const res = await apiLogin(username, password) const res = await apiLogin(username, password)
token.value = res.access_token token.value = res.access_token
localStorage.setItem('dp_token', res.access_token) localStorage.setItem('dp_token', res.access_token)
@@ -38,20 +37,13 @@ export const useUserStore = defineStore('user', () => {
} }
async function fetchUserInfo() { async function fetchUserInfo() {
if (!token.value) return null if (!token.value) return
try { try {
const res = await getMe() const res = await getMe()
userInfo.value = res userInfo.value = res
return res } catch (e) {
} catch (e: any) { logout()
// Don't logout on fetch error; token may still be valid throw e
console.error('Failed to fetch user info:', e?.message || e)
// Only logout on 401
if (e?.response?.status === 401 || e?.status === 401) {
logout()
throw e
}
return null
} }
} }
+2 -4
View File
@@ -81,11 +81,9 @@ async function handleLogin() {
try { try {
await userStore.login(form.username, form.password) await userStore.login(form.username, form.password)
ElMessage.success('登录成功') ElMessage.success('登录成功')
await router.push('/') router.push('/')
} catch (e: any) { } catch (e: any) {
if (!e?.handled) { ElMessage.error(e?.message || '登录失败')
ElMessage.error(e?.message || '登录失败')
}
} finally { } finally {
loading.value = false loading.value = false
} }
+1 -2
View File
@@ -220,8 +220,7 @@ async function handleAutoClassify(p: ProjectItem) {
} }
fetchData() fetchData()
} catch (e: any) { } catch (e: any) {
const msg = e?.response?.data?.detail || e?.response?.data?.message || e?.message || '自动分类失败' ElMessage.error(e?.message || '自动分类失败')
ElMessage.error(msg)
} }
} }