Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba04f3b35 | ||
|
|
b8d2b3ac0d | ||
|
|
e48975f9d5 |
@@ -0,0 +1,31 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
[*.{json,md,yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Matches multiple files with brace expansion notation
|
||||||
|
[*.scss]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# The above files are for code, but for config files we want no indentation
|
||||||
|
[{package.json,.eslintrc.cjs,.prettierrc}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 开发环境配置
|
||||||
|
VITE_API_BASE_URL=http://localhost:8000/api/v1
|
||||||
|
VITE_APP_TITLE=资产管理系统
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# 应用配置
|
|
||||||
APP_NAME=资产管理系统
|
|
||||||
APP_VERSION=1.0.0
|
|
||||||
APP_ENVIRONMENT=development
|
|
||||||
DEBUG=True
|
|
||||||
API_V1_PREFIX=/api/v1
|
|
||||||
|
|
||||||
# 服务器配置
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=8000
|
|
||||||
|
|
||||||
# 数据库配置
|
|
||||||
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/asset_management
|
|
||||||
DATABASE_ECHO=False
|
|
||||||
|
|
||||||
# Redis配置
|
|
||||||
REDIS_URL=redis://localhost:6379/0
|
|
||||||
REDIS_MAX_CONNECTIONS=50
|
|
||||||
|
|
||||||
# JWT配置
|
|
||||||
SECRET_KEY=your-secret-key-change-in-production
|
|
||||||
ALGORITHM=HS256
|
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=15
|
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
||||||
|
|
||||||
# CORS配置
|
|
||||||
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000","http://127.0.0.1:5173"]
|
|
||||||
CORS_ALLOW_CREDENTIALS=True
|
|
||||||
CORS_ALLOW_METHODS=["*"]
|
|
||||||
CORS_ALLOW_HEADERS=["*"]
|
|
||||||
|
|
||||||
# 文件上传配置
|
|
||||||
UPLOAD_DIR=uploads
|
|
||||||
MAX_UPLOAD_SIZE=10485760
|
|
||||||
ALLOWED_EXTENSIONS=["png","jpg","jpeg","gif","pdf","xlsx","xls"]
|
|
||||||
|
|
||||||
# 验证码配置
|
|
||||||
CAPTCHA_EXPIRE_SECONDS=300
|
|
||||||
CAPTCHA_LENGTH=4
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_FILE=logs/app.log
|
|
||||||
LOG_ROTATION=500 MB
|
|
||||||
LOG_RETENTION=10 days
|
|
||||||
|
|
||||||
# 分页配置
|
|
||||||
DEFAULT_PAGE_SIZE=20
|
|
||||||
MAX_PAGE_SIZE=100
|
|
||||||
|
|
||||||
# 二维码配置
|
|
||||||
QR_CODE_DIR=uploads/qrcodes
|
|
||||||
QR_CODE_SIZE=300
|
|
||||||
QR_CODE_BORDER=2
|
|
||||||
+3
-35
@@ -1,35 +1,3 @@
|
|||||||
APP_NAME=资产管理系统
|
# 生产环境配置
|
||||||
APP_VERSION=1.0.0
|
VITE_API_BASE_URL=https://zc.workyai.cn/api/v1
|
||||||
APP_ENVIRONMENT=production
|
VITE_APP_TITLE=资产管理系统
|
||||||
DEBUG=False
|
|
||||||
HOST=0.0.0.0
|
|
||||||
PORT=8001
|
|
||||||
API_V1_PREFIX=/api/v1
|
|
||||||
|
|
||||||
# 数据库配置(从服务器获取)
|
|
||||||
DATABASE_URL=postgresql+asyncpg://asset_user:PASSWORD@118.145.218.2:5433/asset_management
|
|
||||||
DATABASE_ECHO=False
|
|
||||||
|
|
||||||
# Redis配置
|
|
||||||
REDIS_URL=redis://:PASSWORD@118.145.218.2:6380/0
|
|
||||||
REDIS_MAX_CONNECTIONS=50
|
|
||||||
|
|
||||||
# JWT配置
|
|
||||||
SECRET_KEY=请生成强密钥
|
|
||||||
ALGORITHM=HS256
|
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=15
|
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
||||||
|
|
||||||
# CORS配置
|
|
||||||
CORS_ORIGINS=["https://zc.workyai.cn"]
|
|
||||||
CORS_ALLOW_CREDENTIALS=True
|
|
||||||
CORS_ALLOW_METHODS=["GET","POST","PUT","DELETE","PATCH"]
|
|
||||||
CORS_ALLOW_HEADERS=["*"]
|
|
||||||
|
|
||||||
# 文件上传配置
|
|
||||||
UPLOAD_DIR=uploads
|
|
||||||
MAX_UPLOAD_SIZE=104857600
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_FILE=logs/app.log
|
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"Component": true,
|
||||||
|
"ComponentPublicInstance": true,
|
||||||
|
"ComputedRef": true,
|
||||||
|
"DirectiveBinding": true,
|
||||||
|
"EffectScope": true,
|
||||||
|
"ExtractDefaultPropTypes": true,
|
||||||
|
"ExtractPropTypes": true,
|
||||||
|
"ExtractPublicPropTypes": true,
|
||||||
|
"InjectionKey": true,
|
||||||
|
"MaybeRef": true,
|
||||||
|
"MaybeRefOrGetter": true,
|
||||||
|
"PropType": true,
|
||||||
|
"Ref": true,
|
||||||
|
"VNode": true,
|
||||||
|
"WritableComputedRef": true,
|
||||||
|
"acceptHMRUpdate": true,
|
||||||
|
"asyncComputed": true,
|
||||||
|
"autoResetRef": true,
|
||||||
|
"computed": true,
|
||||||
|
"computedAsync": true,
|
||||||
|
"computedEager": true,
|
||||||
|
"computedInject": true,
|
||||||
|
"computedWithControl": true,
|
||||||
|
"controlledComputed": true,
|
||||||
|
"controlledRef": true,
|
||||||
|
"createApp": true,
|
||||||
|
"createEventHook": true,
|
||||||
|
"createGlobalState": true,
|
||||||
|
"createInjectionState": true,
|
||||||
|
"createPinia": true,
|
||||||
|
"createReactiveFn": true,
|
||||||
|
"createReusableTemplate": true,
|
||||||
|
"createSharedComposable": true,
|
||||||
|
"createTemplatePromise": true,
|
||||||
|
"createUnrefFn": true,
|
||||||
|
"customRef": true,
|
||||||
|
"debouncedRef": true,
|
||||||
|
"debouncedWatch": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"defineStore": true,
|
||||||
|
"eagerComputed": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"extendRef": true,
|
||||||
|
"getActivePinia": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"h": true,
|
||||||
|
"ignorableWatch": true,
|
||||||
|
"inject": true,
|
||||||
|
"injectLocal": true,
|
||||||
|
"isDefined": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"makeDestructurable": true,
|
||||||
|
"mapActions": true,
|
||||||
|
"mapGetters": true,
|
||||||
|
"mapState": true,
|
||||||
|
"mapStores": true,
|
||||||
|
"mapWritableState": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeRouteLeave": true,
|
||||||
|
"onBeforeRouteUpdate": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onClickOutside": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onKeyStroke": true,
|
||||||
|
"onLongPress": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onStartTyping": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"onWatcherCleanup": true,
|
||||||
|
"pausableWatch": true,
|
||||||
|
"provide": true,
|
||||||
|
"provideLocal": true,
|
||||||
|
"reactify": true,
|
||||||
|
"reactifyObject": true,
|
||||||
|
"reactive": true,
|
||||||
|
"reactiveComputed": true,
|
||||||
|
"reactiveOmit": true,
|
||||||
|
"reactivePick": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"refAutoReset": true,
|
||||||
|
"refDebounced": true,
|
||||||
|
"refDefault": true,
|
||||||
|
"refThrottled": true,
|
||||||
|
"refWithControl": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"resolveRef": true,
|
||||||
|
"resolveUnref": true,
|
||||||
|
"setActivePinia": true,
|
||||||
|
"setMapStoreSuffix": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"storeToRefs": true,
|
||||||
|
"syncRef": true,
|
||||||
|
"syncRefs": true,
|
||||||
|
"templateRef": true,
|
||||||
|
"throttledRef": true,
|
||||||
|
"throttledWatch": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toReactive": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"toValue": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"tryOnBeforeMount": true,
|
||||||
|
"tryOnBeforeUnmount": true,
|
||||||
|
"tryOnMounted": true,
|
||||||
|
"tryOnScopeDispose": true,
|
||||||
|
"tryOnUnmounted": true,
|
||||||
|
"unref": true,
|
||||||
|
"unrefElement": true,
|
||||||
|
"until": true,
|
||||||
|
"useActiveElement": true,
|
||||||
|
"useAnimate": true,
|
||||||
|
"useArrayDifference": true,
|
||||||
|
"useArrayEvery": true,
|
||||||
|
"useArrayFilter": true,
|
||||||
|
"useArrayFind": true,
|
||||||
|
"useArrayFindIndex": true,
|
||||||
|
"useArrayFindLast": true,
|
||||||
|
"useArrayIncludes": true,
|
||||||
|
"useArrayJoin": true,
|
||||||
|
"useArrayMap": true,
|
||||||
|
"useArrayReduce": true,
|
||||||
|
"useArraySome": true,
|
||||||
|
"useArrayUnique": true,
|
||||||
|
"useAsyncQueue": true,
|
||||||
|
"useAsyncState": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useBase64": true,
|
||||||
|
"useBattery": true,
|
||||||
|
"useBluetooth": true,
|
||||||
|
"useBreakpoints": true,
|
||||||
|
"useBroadcastChannel": true,
|
||||||
|
"useBrowserLocation": true,
|
||||||
|
"useCached": true,
|
||||||
|
"useClipboard": true,
|
||||||
|
"useClipboardItems": true,
|
||||||
|
"useCloned": true,
|
||||||
|
"useColorMode": true,
|
||||||
|
"useConfirmDialog": true,
|
||||||
|
"useCounter": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVar": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useCurrentElement": true,
|
||||||
|
"useCycleList": true,
|
||||||
|
"useDark": true,
|
||||||
|
"useDateFormat": true,
|
||||||
|
"useDebounce": true,
|
||||||
|
"useDebounceFn": true,
|
||||||
|
"useDebouncedRefHistory": true,
|
||||||
|
"useDeviceMotion": true,
|
||||||
|
"useDeviceOrientation": true,
|
||||||
|
"useDevicePixelRatio": true,
|
||||||
|
"useDevicesList": true,
|
||||||
|
"useDisplayMedia": true,
|
||||||
|
"useDocumentVisibility": true,
|
||||||
|
"useDraggable": true,
|
||||||
|
"useDropZone": true,
|
||||||
|
"useElementBounding": true,
|
||||||
|
"useElementByPoint": true,
|
||||||
|
"useElementHover": true,
|
||||||
|
"useElementSize": true,
|
||||||
|
"useElementVisibility": true,
|
||||||
|
"useEventBus": true,
|
||||||
|
"useEventListener": true,
|
||||||
|
"useEventSource": true,
|
||||||
|
"useEyeDropper": true,
|
||||||
|
"useFavicon": true,
|
||||||
|
"useFetch": true,
|
||||||
|
"useFileDialog": true,
|
||||||
|
"useFileSystemAccess": true,
|
||||||
|
"useFocus": true,
|
||||||
|
"useFocusWithin": true,
|
||||||
|
"useFps": true,
|
||||||
|
"useFullscreen": true,
|
||||||
|
"useGamepad": true,
|
||||||
|
"useGeolocation": true,
|
||||||
|
"useId": true,
|
||||||
|
"useIdle": true,
|
||||||
|
"useImage": true,
|
||||||
|
"useInfiniteScroll": true,
|
||||||
|
"useIntersectionObserver": true,
|
||||||
|
"useInterval": true,
|
||||||
|
"useIntervalFn": true,
|
||||||
|
"useKeyModifier": true,
|
||||||
|
"useLastChanged": true,
|
||||||
|
"useLink": true,
|
||||||
|
"useLocalStorage": true,
|
||||||
|
"useMagicKeys": true,
|
||||||
|
"useManualRefHistory": true,
|
||||||
|
"useMediaControls": true,
|
||||||
|
"useMediaQuery": true,
|
||||||
|
"useMemoize": true,
|
||||||
|
"useMemory": true,
|
||||||
|
"useModel": true,
|
||||||
|
"useMounted": true,
|
||||||
|
"useMouse": true,
|
||||||
|
"useMouseInElement": true,
|
||||||
|
"useMousePressed": true,
|
||||||
|
"useMutationObserver": true,
|
||||||
|
"useNavigatorLanguage": true,
|
||||||
|
"useNetwork": true,
|
||||||
|
"useNow": true,
|
||||||
|
"useObjectUrl": true,
|
||||||
|
"useOffsetPagination": true,
|
||||||
|
"useOnline": true,
|
||||||
|
"usePageLeave": true,
|
||||||
|
"useParallax": true,
|
||||||
|
"useParentElement": true,
|
||||||
|
"usePerformanceObserver": true,
|
||||||
|
"usePermission": true,
|
||||||
|
"usePointer": true,
|
||||||
|
"usePointerLock": true,
|
||||||
|
"usePointerSwipe": true,
|
||||||
|
"usePreferredColorScheme": true,
|
||||||
|
"usePreferredContrast": true,
|
||||||
|
"usePreferredDark": true,
|
||||||
|
"usePreferredLanguages": true,
|
||||||
|
"usePreferredReducedMotion": true,
|
||||||
|
"usePrevious": true,
|
||||||
|
"useRafFn": true,
|
||||||
|
"useRefHistory": true,
|
||||||
|
"useResizeObserver": true,
|
||||||
|
"useRoute": true,
|
||||||
|
"useRouter": true,
|
||||||
|
"useScreenOrientation": true,
|
||||||
|
"useScreenSafeArea": true,
|
||||||
|
"useScriptTag": true,
|
||||||
|
"useScroll": true,
|
||||||
|
"useScrollLock": true,
|
||||||
|
"useSessionStorage": true,
|
||||||
|
"useShare": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"useSorted": true,
|
||||||
|
"useSpeechRecognition": true,
|
||||||
|
"useSpeechSynthesis": true,
|
||||||
|
"useStepper": true,
|
||||||
|
"useStorage": true,
|
||||||
|
"useStorageAsync": true,
|
||||||
|
"useStyleTag": true,
|
||||||
|
"useSupported": true,
|
||||||
|
"useSwipe": true,
|
||||||
|
"useTemplateRef": true,
|
||||||
|
"useTemplateRefsList": true,
|
||||||
|
"useTextDirection": true,
|
||||||
|
"useTextSelection": true,
|
||||||
|
"useTextareaAutosize": true,
|
||||||
|
"useThrottle": true,
|
||||||
|
"useThrottleFn": true,
|
||||||
|
"useThrottledRefHistory": true,
|
||||||
|
"useTimeAgo": true,
|
||||||
|
"useTimeout": true,
|
||||||
|
"useTimeoutFn": true,
|
||||||
|
"useTimeoutPoll": true,
|
||||||
|
"useTimestamp": true,
|
||||||
|
"useTitle": true,
|
||||||
|
"useToNumber": true,
|
||||||
|
"useToString": true,
|
||||||
|
"useToggle": true,
|
||||||
|
"useTransition": true,
|
||||||
|
"useUrlSearchParams": true,
|
||||||
|
"useUserMedia": true,
|
||||||
|
"useVModel": true,
|
||||||
|
"useVModels": true,
|
||||||
|
"useVibrate": true,
|
||||||
|
"useVirtualList": true,
|
||||||
|
"useWakeLock": true,
|
||||||
|
"useWebNotification": true,
|
||||||
|
"useWebSocket": true,
|
||||||
|
"useWebWorker": true,
|
||||||
|
"useWebWorkerFn": true,
|
||||||
|
"useWindowFocus": true,
|
||||||
|
"useWindowScroll": true,
|
||||||
|
"useWindowSize": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchArray": true,
|
||||||
|
"watchAtMost": true,
|
||||||
|
"watchDebounced": true,
|
||||||
|
"watchDeep": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchIgnorable": true,
|
||||||
|
"watchImmediate": true,
|
||||||
|
"watchOnce": true,
|
||||||
|
"watchPausable": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true,
|
||||||
|
"watchThrottled": true,
|
||||||
|
"watchTriggerable": true,
|
||||||
|
"watchWithFilter": true,
|
||||||
|
"whenever": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:vue/vue3-recommended'
|
||||||
|
],
|
||||||
|
parser: 'vue-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint', 'vue'],
|
||||||
|
rules: {
|
||||||
|
// Vue 规则
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/no-v-html': 'off',
|
||||||
|
|
||||||
|
// TypeScript 规则
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
}],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
|
||||||
|
// 通用规则
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'no-var': 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-102
@@ -1,107 +1,38 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
# Logs
|
||||||
__pycache__/
|
logs
|
||||||
*.py[cod]
|
*.log
|
||||||
*$py.class
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
# C extensions
|
node_modules
|
||||||
*.so
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
# Distribution / packaging
|
# Editor directories and files
|
||||||
.Python
|
.vscode/*
|
||||||
build/
|
!.vscode/extensions.json
|
||||||
develop-eggs/
|
.idea
|
||||||
dist/
|
.DS_Store
|
||||||
downloads/
|
*.suo
|
||||||
eggs/
|
*.ntvs*
|
||||||
.eggs/
|
*.njsproj
|
||||||
lib/
|
*.sln
|
||||||
lib64/
|
*.sw?
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
# Environment variables
|
||||||
*.manifest
|
|
||||||
.spec
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
tests/.pytest_cache/
|
|
||||||
tests/*.png
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.production
|
.env.development.local
|
||||||
.venv
|
.env.test.local
|
||||||
env/
|
.env.production.local
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Database
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
# Uploads
|
|
||||||
uploads/*
|
|
||||||
!uploads/.gitkeep
|
|
||||||
|
|
||||||
# Alembic
|
|
||||||
alembic/versions/*.py
|
|
||||||
!alembic/versions/__init__.py
|
|
||||||
|
|
||||||
# MyPy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# Jupyter
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# VSCode
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
test_*.py
|
coverage
|
||||||
*_test.py
|
.nyc_output/
|
||||||
tests/
|
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
*.md
|
*.md
|
||||||
@@ -110,6 +41,7 @@ PHASE*.md
|
|||||||
DELIVERY*.md
|
DELIVERY*.md
|
||||||
SUMMARY*.md
|
SUMMARY*.md
|
||||||
!README.md
|
!README.md
|
||||||
|
!src/components/charts/README.md
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
@@ -117,8 +49,7 @@ SUMMARY*.md
|
|||||||
*.bak
|
*.bak
|
||||||
*.backup
|
*.backup
|
||||||
|
|
||||||
# Docker
|
# Lock files
|
||||||
.dockerignore
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
# Test reports
|
pnpm-lock.yaml
|
||||||
test_reports/
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
图表组件库文件清单
|
||||||
|
===================
|
||||||
|
|
||||||
|
核心组件
|
||||||
|
--------
|
||||||
|
src/components/charts/BaseChart.vue
|
||||||
|
src/components/charts/PieChart.vue
|
||||||
|
src/components/charts/BarChart.vue
|
||||||
|
src/components/charts/LineChart.vue
|
||||||
|
src/components/charts/GaugeChart.vue
|
||||||
|
src/components/charts/FunnelChart.vue
|
||||||
|
|
||||||
|
业务图表组件
|
||||||
|
------------
|
||||||
|
src/components/charts/business/AssetStatusChart.vue
|
||||||
|
src/components/charts/business/AssetDistributionChart.vue
|
||||||
|
src/components/charts/business/AssetValueTrendChart.vue
|
||||||
|
src/components/charts/business/AssetUtilizationChart.vue
|
||||||
|
|
||||||
|
统计卡片组件
|
||||||
|
------------
|
||||||
|
src/components/statistics/StatCard.vue
|
||||||
|
src/components/statistics/StatCardGroup.vue
|
||||||
|
|
||||||
|
Composables
|
||||||
|
-----------
|
||||||
|
src/composables/useECharts.ts
|
||||||
|
src/composables/useChartData.ts
|
||||||
|
|
||||||
|
工具函数
|
||||||
|
--------
|
||||||
|
src/utils/echarts.ts
|
||||||
|
src/utils/echarts/performance.ts
|
||||||
|
|
||||||
|
类型定义
|
||||||
|
--------
|
||||||
|
src/types/charts.ts
|
||||||
|
src/components/charts/charts.d.ts
|
||||||
|
|
||||||
|
组件导出
|
||||||
|
--------
|
||||||
|
src/components/charts/index.ts
|
||||||
|
src/components/charts/README.md
|
||||||
|
src/components/statistics/index.ts
|
||||||
|
|
||||||
|
文档
|
||||||
|
----
|
||||||
|
CHARTS_README.md - 完整使用文档
|
||||||
|
CHARTS_DELIVERY.md - 交付文档
|
||||||
|
CHARTS_QUICKSTART.md - 快速开始指南
|
||||||
|
CHARTS_SUMMARY.md - 项目总结
|
||||||
|
CHARTS_FILES.txt - 本文件
|
||||||
|
|
||||||
|
示例和测试
|
||||||
|
----------
|
||||||
|
src/views/examples/ChartsExample.vue
|
||||||
|
tests/unit/components/PieChart.test.ts
|
||||||
|
tests/unit/composables/useECharts.test.ts
|
||||||
|
|
||||||
|
路由配置
|
||||||
|
--------
|
||||||
|
src/router/index.ts (已添加示例页面路由)
|
||||||
|
|
||||||
|
总计:25 个文件
|
||||||
+7
-21
@@ -1,26 +1,12 @@
|
|||||||
FROM python:3.10-slim
|
FROM nginx:alpine
|
||||||
|
|
||||||
WORKDIR /app
|
# 复制构建产物
|
||||||
|
COPY dist /usr/share/nginx/html
|
||||||
|
|
||||||
# 安装系统依赖
|
# 复制Nginx配置
|
||||||
RUN apt-get update && apt-get install -y \
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
gcc \
|
|
||||||
postgresql-client \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 复制依赖文件
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# 复制应用代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 创建必要的目录
|
|
||||||
RUN mkdir -p logs uploads uploads/qrcodes
|
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8001
|
EXPOSE 3000
|
||||||
|
|
||||||
# 启动命令
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
.PHONY: help install run test clean format lint db-migrate db-upgrade db-downgrade
|
|
||||||
|
|
||||||
# 默认目标
|
|
||||||
help:
|
|
||||||
@echo "可用命令:"
|
|
||||||
@echo " make install - 安装依赖"
|
|
||||||
@echo " make run - 启动开发服务器"
|
|
||||||
@echo " make test - 运行测试"
|
|
||||||
@echo " make clean - 清理缓存和临时文件"
|
|
||||||
@echo " make format - 格式化代码"
|
|
||||||
@echo " make lint - 代码检查"
|
|
||||||
@echo " make db-migrate - 创建数据库迁移"
|
|
||||||
@echo " make db-upgrade - 执行数据库迁移"
|
|
||||||
@echo " make db-downgrade - 回滚数据库迁移"
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
install:
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
run:
|
|
||||||
python run.py
|
|
||||||
|
|
||||||
# 运行测试
|
|
||||||
test:
|
|
||||||
pytest tests/ -v --cov=app --cov-report=html
|
|
||||||
|
|
||||||
# 清理缓存和临时文件
|
|
||||||
clean:
|
|
||||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
|
||||||
find . -type f -name "*.pyc" -delete
|
|
||||||
find . -type f -name "*.pyo" -delete
|
|
||||||
find . -type f -name "*.pyd" -delete
|
|
||||||
rm -rf .pytest_cache
|
|
||||||
rm -rf htmlcov
|
|
||||||
rm -rf .mypy_cache
|
|
||||||
rm -rf .coverage
|
|
||||||
|
|
||||||
# 格式化代码
|
|
||||||
format:
|
|
||||||
black app/ tests/
|
|
||||||
isort app/ tests/
|
|
||||||
|
|
||||||
# 代码检查
|
|
||||||
lint:
|
|
||||||
flake8 app/ tests/
|
|
||||||
mypy app/
|
|
||||||
|
|
||||||
# 创建数据库迁移
|
|
||||||
db-migrate:
|
|
||||||
@read -p "请输入迁移描述: " desc; \
|
|
||||||
alembic revision --autogenerate -m "$$desc"
|
|
||||||
|
|
||||||
# 执行数据库迁移
|
|
||||||
db-upgrade:
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
# 回滚数据库迁移
|
|
||||||
db-downgrade:
|
|
||||||
alembic downgrade -1
|
|
||||||
@@ -1,284 +1,217 @@
|
|||||||
# 资产管理系统 - 后端API
|
# 资产管理系统前端
|
||||||
|
|
||||||
基于 FastAPI + SQLAlchemy + PostgreSQL 的企业级资产管理系统后端API
|
> 基于 Vue 3 + TypeScript + Element Plus 构建的现代化资产管理系统前端应用
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **框架**: FastAPI 0.104+
|
- **框架**: Vue 3.4+ (Composition API + `<script setup>`)
|
||||||
- **ORM**: SQLAlchemy 2.0+ (异步模式)
|
- **语言**: TypeScript 5.0+
|
||||||
- **数据库**: PostgreSQL 14+
|
- **构建工具**: Vite 5
|
||||||
- **缓存**: Redis
|
- **UI组件库**: Element Plus
|
||||||
- **认证**: JWT (python-jose)
|
- **状态管理**: Pinia
|
||||||
- **密码加密**: bcrypt
|
- **路由**: Vue Router 4
|
||||||
- **数据验证**: Pydantic v2
|
- **HTTP客户端**: Axios
|
||||||
- **数据库迁移**: Alembic
|
- **表单验证**: VeeValidate + Yup
|
||||||
- **测试**: pytest
|
- **图表**: ECharts
|
||||||
- **ASGI服务器**: Uvicorn
|
- **工具库**: dayjs, lodash-es, qrcode
|
||||||
|
- **组合式函数**: @vueuse/core
|
||||||
|
- **代码规范**: ESLint + Prettier
|
||||||
|
- **测试**: Vitest
|
||||||
|
|
||||||
## 项目结构
|
## 功能特性
|
||||||
|
|
||||||
|
### Phase 1: 项目基础搭建 ✅
|
||||||
|
- [x] Vite + Vue 3项目初始化
|
||||||
|
- [x] TypeScript配置
|
||||||
|
- [x] ESLint + Prettier配置
|
||||||
|
- [x] 路由配置(Vue Router 4)
|
||||||
|
- [x] 状态管理(Pinia)
|
||||||
|
- [x] Axios请求封装
|
||||||
|
- [x] UI主题配置(青灰配色 #475569)
|
||||||
|
|
||||||
|
### Phase 2: 认证与布局 ✅
|
||||||
|
- [x] 登录页面(Login.vue)
|
||||||
|
- [x] 主应用布局(MainLayout.vue)
|
||||||
|
- [x] 侧边菜单组件
|
||||||
|
- [x] 顶部导航栏
|
||||||
|
- [x] 认证状态管理
|
||||||
|
- [x] 路由守卫
|
||||||
|
|
||||||
|
### Phase 3: 后台管理模块(开发中)
|
||||||
|
- [ ] 用户管理页面
|
||||||
|
- [ ] 角色权限页面
|
||||||
|
- [ ] 设备类型管理页面
|
||||||
|
- [ ] 动态表单设计器
|
||||||
|
- [ ] 机构网点管理页面
|
||||||
|
|
||||||
|
### Phase 4: 资产管理模块(部分完成)
|
||||||
|
- [x] 资产列表页面
|
||||||
|
- [x] 资产详情对话框
|
||||||
|
- [x] 资产入库页面
|
||||||
|
- [x] 资产编辑对话框
|
||||||
|
- [ ] 批量导入组件
|
||||||
|
- [ ] 批量导出组件
|
||||||
|
- [ ] 扫码查询页面
|
||||||
|
- [ ] 二维码对话框
|
||||||
|
- [ ] 资产状态追踪组件
|
||||||
|
|
||||||
|
### Phase 5: 资产分配模块(待开发)
|
||||||
|
- [ ] 分配单列表页面
|
||||||
|
- [ ] 创建分配单对话框
|
||||||
|
- [ ] 审批对话框
|
||||||
|
- [ ] 资产选择器
|
||||||
|
|
||||||
|
### Phase 6: 维修与统计(待开发)
|
||||||
|
- [ ] 维修管理页面
|
||||||
|
- [ ] 统计报表页面
|
||||||
|
- [ ] ECharts图表集成
|
||||||
|
|
||||||
|
### Phase 7: 系统管理(待开发)
|
||||||
|
- [ ] 系统配置页面
|
||||||
|
- [ ] 操作日志页面
|
||||||
|
- [ ] 消息通知组件
|
||||||
|
- [ ] 个人中心页面
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
asset_management_backend/
|
src/
|
||||||
├── app/
|
├── api/ # API接口
|
||||||
│ ├── api/ # API路由
|
│ ├── request.ts # Axios封装
|
||||||
│ │ └── v1/
|
│ ├── auth.ts # 认证接口
|
||||||
│ │ ├── auth.py # 认证相关API
|
│ ├── assets.ts # 资产接口
|
||||||
│ │ └── __init__.py
|
│ ├── users.ts # 用户接口
|
||||||
│ ├── core/ # 核心模块
|
│ └── index.ts # 统一导出
|
||||||
│ │ ├── config.py # 配置管理
|
├── assets/ # 静态资源
|
||||||
│ │ ├── security.py # 安全相关
|
│ └── styles/ # 全局样式
|
||||||
│ │ ├── deps.py # 依赖注入
|
│ ├── variables.scss # 主题变量
|
||||||
│ │ ├── exceptions.py # 自定义异常
|
│ └── index.scss # 全局样式
|
||||||
│ │ └── response.py # 统一响应
|
├── components/ # 通用组件
|
||||||
│ ├── crud/ # 数据库CRUD操作
|
├── composables/ # 组合式函数
|
||||||
│ │ ├── user.py # 用户CRUD
|
│ ├── usePagination.ts # 分页
|
||||||
│ │ └── ...
|
│ └── useTable.ts # 表格
|
||||||
│ ├── db/ # 数据库相关
|
├── layouts/ # 布局组件
|
||||||
│ │ ├── base.py # 模型基类
|
│ └── MainLayout.vue # 主布局
|
||||||
│ │ └── session.py # 会话管理
|
├── router/ # 路由配置
|
||||||
│ ├── models/ # SQLAlchemy模型
|
│ └── index.ts
|
||||||
│ │ ├── user.py # 用户模型
|
├── stores/ # Pinia状态管理
|
||||||
│ │ └── ...
|
│ ├── modules/
|
||||||
│ ├── schemas/ # Pydantic Schema
|
│ │ ├── user.ts # 用户状态
|
||||||
│ │ ├── user.py # 用户Schema
|
│ │ └── app.ts # 应用状态
|
||||||
│ │ └── ...
|
│ └── index.ts
|
||||||
│ ├── services/ # 业务逻辑层
|
├── types/ # TypeScript类型定义
|
||||||
│ │ ├── auth_service.py # 认证服务
|
│ └── index.ts
|
||||||
│ │ └── ...
|
├── utils/ # 工具函数
|
||||||
│ └── utils/ # 工具函数
|
│ ├── auth.ts # 认证工具
|
||||||
│ └── ...
|
│ ├── format.ts # 格式化工具
|
||||||
├── alembic/ # 数据库迁移
|
│ ├── validate.ts # 验证工具
|
||||||
│ └── versions/
|
│ └── constants.ts # 常量定义
|
||||||
├── tests/ # 测试
|
├── views/ # 页面组件
|
||||||
├── logs/ # 日志文件
|
│ ├── auth/ # 认证相关
|
||||||
├── uploads/ # 上传文件
|
│ │ └── Login.vue
|
||||||
├── .env.example # 环境变量示例
|
│ ├── admin/ # 后台管理
|
||||||
├── requirements.txt # 依赖包
|
│ │ ├── UserManagement.vue
|
||||||
├── run.py # 开发服务器启动脚本
|
│ │ ├── RoleManagement.vue
|
||||||
└── README.md # 项目说明
|
│ │ ├── DeviceTypeManagement.vue
|
||||||
|
│ │ └── OrganizationManagement.vue
|
||||||
|
│ ├── assets/ # 资产管理
|
||||||
|
│ │ ├── AssetList.vue
|
||||||
|
│ │ ├── AssetCreate.vue
|
||||||
|
│ │ ├── AssetAllocation.vue
|
||||||
|
│ │ ├── AssetScan.vue
|
||||||
|
│ │ ├── MaintenanceManagement.vue
|
||||||
|
│ │ ├── StatisticsDashboard.vue
|
||||||
|
│ │ └── components/ # 资产相关组件
|
||||||
|
│ │ ├── AssetDetailDialog.vue
|
||||||
|
│ │ ├── AssetEditDialog.vue
|
||||||
|
│ │ └── QrcodeDialog.vue
|
||||||
|
│ └── error/ # 错误页面
|
||||||
|
│ └── 404.vue
|
||||||
|
├── App.vue # 根组件
|
||||||
|
└── main.ts # 应用入口
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 开发指南
|
||||||
|
|
||||||
### 1. 环境准备
|
### 安装依赖
|
||||||
|
|
||||||
确保已安装:
|
|
||||||
- Python 3.10+
|
|
||||||
- PostgreSQL 14+
|
|
||||||
- Redis
|
|
||||||
|
|
||||||
### 2. 安装依赖
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建虚拟环境
|
npm install
|
||||||
python -m venv venv
|
|
||||||
|
|
||||||
# 激活虚拟环境
|
|
||||||
# Windows:
|
|
||||||
venv\Scripts\activate
|
|
||||||
# Linux/Mac:
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# 安装依赖包
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置环境变量
|
### 启动开发服务器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 复制环境变量示例文件
|
npm run dev
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# 编辑 .env 文件,配置数据库等信息
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 初始化数据库
|
访问 `http://localhost:3000`
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建数据库
|
npm run build
|
||||||
createdb asset_management
|
|
||||||
|
|
||||||
# 运行数据库迁移
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
# 或在开发环境直接初始化(会自动创建表)
|
|
||||||
# 修改 app/main.py 中的 lifespan 函数,取消注释 init_db()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 启动服务
|
### 代码检查
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 开发模式(支持热重载)
|
npm run lint
|
||||||
python run.py
|
|
||||||
|
|
||||||
# 或使用 uvicorn
|
|
||||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 访问API文档
|
### 代码格式化
|
||||||
|
|
||||||
启动成功后,访问以下地址:
|
```bash
|
||||||
|
npm run format
|
||||||
|
```
|
||||||
|
|
||||||
- Swagger UI: http://localhost:8000/docs
|
### 运行测试
|
||||||
- ReDoc: http://localhost:8000/redoc
|
|
||||||
- OpenAPI JSON: http://localhost:8000/openapi.json
|
|
||||||
|
|
||||||
## API端点
|
```bash
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
### 认证模块
|
## 环境配置
|
||||||
|
|
||||||
- `POST /api/v1/auth/login` - 用户登录
|
### 开发环境 (.env.development)
|
||||||
- `POST /api/v1/auth/refresh` - 刷新令牌
|
```
|
||||||
- `POST /api/v1/auth/logout` - 用户登出
|
VITE_API_BASE_URL=http://localhost:8000/api/v1
|
||||||
- `PUT /api/v1/auth/change-password` - 修改密码
|
VITE_APP_TITLE=资产管理系统
|
||||||
- `GET /api/v1/auth/captcha` - 获取验证码
|
```
|
||||||
|
|
||||||
### 用户管理
|
### 生产环境 (.env.production)
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=https://api.yourcompany.com/api/v1
|
||||||
|
VITE_APP_TITLE=资产管理系统
|
||||||
|
```
|
||||||
|
|
||||||
- `GET /api/v1/users` - 用户列表
|
## 主题定制
|
||||||
- `POST /api/v1/users` - 创建用户
|
|
||||||
- `GET /api/v1/users/{user_id}` - 获取用户详情
|
|
||||||
- `PUT /api/v1/users/{user_id}` - 更新用户
|
|
||||||
- `DELETE /api/v1/users/{user_id}` - 删除用户
|
|
||||||
- `POST /api/v1/users/{user_id}/reset-password` - 重置密码
|
|
||||||
- `GET /api/v1/users/me` - 获取当前用户信息
|
|
||||||
|
|
||||||
### 角色权限
|
主题色采用青灰配色方案 (#475569),在 `src/assets/styles/variables.scss` 中可以自定义主题变量。
|
||||||
|
|
||||||
- `GET /api/v1/roles` - 角色列表
|
## API接口规范
|
||||||
- `POST /api/v1/roles` - 创建角色
|
|
||||||
- `GET /api/v1/roles/{role_id}` - 获取角色详情
|
|
||||||
- `PUT /api/v1/roles/{role_id}` - 更新角色
|
|
||||||
- `DELETE /api/v1/roles/{role_id}` - 删除角色
|
|
||||||
- `GET /api/v1/permissions/tree` - 权限树列表
|
|
||||||
|
|
||||||
更多API请查看Swagger文档。
|
详见 `complete_api_reference.md` 文档。
|
||||||
|
|
||||||
## 开发规范
|
## 开发规范
|
||||||
|
|
||||||
请参考 `development_standards_guide.md` 文件。
|
详见 `development_standards_guide.md` 文档。
|
||||||
|
|
||||||
### 代码风格
|
## 页面原型
|
||||||
|
|
||||||
- 遵循 PEP 8 规范
|
详见 `frontend_page_prototypes.md` 文档。
|
||||||
- 使用 Black 进行代码格式化
|
|
||||||
- 使用 isort 管理导入
|
|
||||||
- 使用 flake8 进行代码检查
|
|
||||||
- 使用 mypy 进行类型检查
|
|
||||||
|
|
||||||
### 提交规范
|
## 浏览器支持
|
||||||
|
|
||||||
遵循 Conventional Commits 规范:
|
- Chrome >= 90
|
||||||
|
- Firefox >= 88
|
||||||
|
- Safari >= 14
|
||||||
|
- Edge >= 90
|
||||||
|
|
||||||
```
|
## 作者
|
||||||
feat: 新功能
|
|
||||||
fix: Bug修复
|
|
||||||
docs: 文档更新
|
|
||||||
style: 代码格式
|
|
||||||
refactor: 重构
|
|
||||||
perf: 性能优化
|
|
||||||
test: 测试
|
|
||||||
chore: 构建/工具
|
|
||||||
```
|
|
||||||
|
|
||||||
示例:
|
老王
|
||||||
```bash
|
|
||||||
git commit -m "feat(auth): 实现用户登录功能"
|
|
||||||
git commit -m "fix(asset): 修复资产状态转换问题"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 运行所有测试
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# 运行特定测试文件
|
|
||||||
pytest tests/api/test_auth.py
|
|
||||||
|
|
||||||
# 生成覆盖率报告
|
|
||||||
pytest --cov=app --cov-report=html
|
|
||||||
|
|
||||||
# 查看覆盖率报告
|
|
||||||
open htmlcov/index.html
|
|
||||||
```
|
|
||||||
|
|
||||||
## 数据库迁移
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建新的迁移
|
|
||||||
alembic revision --autogenerate -m "描述信息"
|
|
||||||
|
|
||||||
# 执行迁移
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
# 回滚迁移
|
|
||||||
alembic downgrade -1
|
|
||||||
|
|
||||||
# 查看迁移历史
|
|
||||||
alembic history
|
|
||||||
|
|
||||||
# 查看当前版本
|
|
||||||
alembic current
|
|
||||||
```
|
|
||||||
|
|
||||||
## 生产部署
|
|
||||||
|
|
||||||
### 使用 Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 构建镜像
|
|
||||||
docker build -t asset-management-backend .
|
|
||||||
|
|
||||||
# 运行容器
|
|
||||||
docker run -d \
|
|
||||||
--name asset-backend \
|
|
||||||
-p 8000:8000 \
|
|
||||||
--env-file .env \
|
|
||||||
asset-management-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用 Gunicorn + Uvicorn
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install gunicorn
|
|
||||||
|
|
||||||
gunicorn app.main:app \
|
|
||||||
--workers 4 \
|
|
||||||
--worker-class uvicorn.workers.UvicornWorker \
|
|
||||||
--bind 0.0.0.0:8000 \
|
|
||||||
--access-logfile - \
|
|
||||||
--error-logfile -
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### 数据库连接失败
|
|
||||||
|
|
||||||
检查 `DATABASE_URL` 是否正确配置,确保PostgreSQL服务正在运行。
|
|
||||||
|
|
||||||
### Redis连接失败
|
|
||||||
|
|
||||||
检查 `REDIS_URL` 是否正确配置,确保Redis服务正在运行。
|
|
||||||
|
|
||||||
### Token验证失败
|
|
||||||
|
|
||||||
确保 `SECRET_KEY` 配置正确,并检查Token是否过期。
|
|
||||||
|
|
||||||
## 贡献指南
|
|
||||||
|
|
||||||
1. Fork 本仓库
|
|
||||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`)
|
|
||||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
|
||||||
5. 创建 Pull Request
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
本项目采用 MIT 许可证。
|
MIT
|
||||||
|
|
||||||
## 联系方式
|
|
||||||
|
|
||||||
- 项目负责人: 老王
|
|
||||||
- 创建时间: 2025-01-24
|
|
||||||
- 版本: v1.0.0
|
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
# Alembic配置文件
|
|
||||||
|
|
||||||
[alembic]
|
|
||||||
# 迁移脚本目录
|
|
||||||
script_location = alembic
|
|
||||||
|
|
||||||
# 迁移版本存储表
|
|
||||||
version_table = alembic_version
|
|
||||||
|
|
||||||
# 时区设置
|
|
||||||
timezone = Asia/Shanghai
|
|
||||||
|
|
||||||
# 数据库连接URL(从环境变量读取)
|
|
||||||
# sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
||||||
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/asset_management
|
|
||||||
|
|
||||||
[post_write_hooks]
|
|
||||||
|
|
||||||
[loggers]
|
|
||||||
keys = root,sqlalchemy,alembic
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = console
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = WARN
|
|
||||||
handlers = console
|
|
||||||
qualname =
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
level = WARN
|
|
||||||
handlers =
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
|
|
||||||
[logger_alembic]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = alembic
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
level = NOTSET
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
||||||
datefmt = %H:%M:%S
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
"""
|
|
||||||
Alembic环境配置
|
|
||||||
"""
|
|
||||||
from logging.config import fileConfig
|
|
||||||
from sqlalchemy import engine_from_config, pool
|
|
||||||
from alembic import context
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 添加项目根目录到Python路径
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
|
||||||
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.db.base import Base
|
|
||||||
from app.models import user # 导入所有模型
|
|
||||||
|
|
||||||
# Alembic配置对象
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# 设置数据库URL
|
|
||||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL.replace("+asyncpg", ""))
|
|
||||||
|
|
||||||
# 解析日志配置
|
|
||||||
if config.config_file_name is not None:
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
|
|
||||||
# 模型的元数据
|
|
||||||
target_metadata = Base.metadata
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
|
||||||
"""离线模式运行迁移"""
|
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
|
||||||
context.configure(
|
|
||||||
url=url,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
literal_binds=True,
|
|
||||||
dialect_opts={"paramstyle": "named"},
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
|
||||||
"""在线模式运行迁移"""
|
|
||||||
connectable = engine_from_config(
|
|
||||||
config.get_section(config.config_ini_section),
|
|
||||||
prefix="sqlalchemy.",
|
|
||||||
poolclass=pool.NullPool,
|
|
||||||
)
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
|
||||||
context.configure(
|
|
||||||
connection=connection,
|
|
||||||
target_metadata=target_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""${message}
|
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
|
||||||
Revises: ${down_revision | comma,n}
|
|
||||||
Create Date: ${create_date}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
${imports if imports else ""}
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = ${repr(up_revision)}
|
|
||||||
down_revision = ${repr(down_revision)}
|
|
||||||
branch_labels = ${repr(branch_labels)}
|
|
||||||
depends_on = ${repr(depends_on)}
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
${upgrades if upgrades else "pass"}
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
${downgrades if downgrades else "pass"}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
"""
|
|
||||||
应用模块初始化
|
|
||||||
"""
|
|
||||||
__all__ = []
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
"""
|
|
||||||
API模块初始化
|
|
||||||
"""
|
|
||||||
__all__ = []
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
"""
|
|
||||||
API V1模块初始化
|
|
||||||
"""
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from app.api.v1 import (
|
|
||||||
auth, device_types, organizations, assets, brands_suppliers,
|
|
||||||
allocations, maintenance, files, transfers, recoveries,
|
|
||||||
statistics, system_config, operation_logs, notifications
|
|
||||||
)
|
|
||||||
|
|
||||||
api_router = APIRouter()
|
|
||||||
|
|
||||||
# 注册路由模块
|
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
|
|
||||||
api_router.include_router(device_types.router, prefix="/device-types", tags=["设备类型管理"])
|
|
||||||
api_router.include_router(organizations.router, prefix="/organizations", tags=["机构网点管理"])
|
|
||||||
api_router.include_router(assets.router, prefix="/assets", tags=["资产管理"])
|
|
||||||
api_router.include_router(brands_suppliers.router, prefix="/brands-suppliers", tags=["品牌和供应商管理"])
|
|
||||||
api_router.include_router(allocations.router, prefix="/allocation-orders", tags=["资产分配管理"])
|
|
||||||
api_router.include_router(maintenance.router, prefix="/maintenance-records", tags=["维修管理"])
|
|
||||||
api_router.include_router(files.router, prefix="/files", tags=["文件管理"])
|
|
||||||
api_router.include_router(transfers.router, prefix="/transfers", tags=["资产调拨管理"])
|
|
||||||
api_router.include_router(recoveries.router, prefix="/recoveries", tags=["资产回收管理"])
|
|
||||||
api_router.include_router(statistics.router, prefix="/statistics", tags=["统计分析"])
|
|
||||||
api_router.include_router(system_config.router, prefix="/system-config", tags=["系统配置管理"])
|
|
||||||
api_router.include_router(operation_logs.router, prefix="/operation-logs", tags=["操作日志管理"])
|
|
||||||
api_router.include_router(notifications.router, prefix="/notifications", tags=["消息通知管理"])
|
|
||||||
|
|
||||||
__all__ = ["api_router"]
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
"""
|
|
||||||
资产分配管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.allocation import (
|
|
||||||
AllocationOrderCreate,
|
|
||||||
AllocationOrderUpdate,
|
|
||||||
AllocationOrderApproval,
|
|
||||||
AllocationOrderWithRelations,
|
|
||||||
AllocationItemResponse,
|
|
||||||
AllocationOrderQueryParams,
|
|
||||||
AllocationOrderStatistics
|
|
||||||
)
|
|
||||||
from app.services.allocation_service import allocation_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list)
|
|
||||||
def get_allocation_orders(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
order_type: Optional[str] = Query(None, description="单据类型"),
|
|
||||||
approval_status: Optional[str] = Query(None, description="审批状态"),
|
|
||||||
execute_status: Optional[str] = Query(None, description="执行状态"),
|
|
||||||
applicant_id: Optional[int] = Query(None, description="申请人ID"),
|
|
||||||
target_organization_id: Optional[int] = Query(None, description="目标网点ID"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取分配单列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **order_type**: 单据类型(allocation/transfer/recovery/maintenance/scrap)
|
|
||||||
- **approval_status**: 审批状态(pending/approved/rejected/cancelled)
|
|
||||||
- **execute_status**: 执行状态(pending/executing/completed/cancelled)
|
|
||||||
- **applicant_id**: 申请人ID
|
|
||||||
- **target_organization_id**: 目标网点ID
|
|
||||||
- **keyword**: 搜索关键词(单号/标题)
|
|
||||||
"""
|
|
||||||
items, total = allocation_service.get_orders(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
order_type=order_type,
|
|
||||||
approval_status=approval_status,
|
|
||||||
execute_status=execute_status,
|
|
||||||
applicant_id=applicant_id,
|
|
||||||
target_organization_id=target_organization_id,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=AllocationOrderStatistics)
|
|
||||||
def get_allocation_statistics(
|
|
||||||
applicant_id: Optional[int] = Query(None, description="申请人ID"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取分配单统计信息
|
|
||||||
|
|
||||||
- **applicant_id**: 申请人ID(可选)
|
|
||||||
|
|
||||||
返回分配单总数、待审批数、已审批数等统计信息
|
|
||||||
"""
|
|
||||||
return allocation_service.get_statistics(db, applicant_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}", response_model=dict)
|
|
||||||
def get_allocation_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取分配单详情
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
|
|
||||||
返回分配单详情及其关联信息(包含明细列表)
|
|
||||||
"""
|
|
||||||
return allocation_service.get_order(db, order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}/items", response_model=list)
|
|
||||||
def get_allocation_order_items(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取分配单明细列表
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
|
|
||||||
返回该分配单的所有资产明细
|
|
||||||
"""
|
|
||||||
return allocation_service.get_order_items(db, order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=dict, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_allocation_order(
|
|
||||||
obj_in: AllocationOrderCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建分配单
|
|
||||||
|
|
||||||
- **order_type**: 单据类型
|
|
||||||
- allocation: 资产分配(从仓库分配给网点)
|
|
||||||
- transfer: 资产调拨(网点间调拨)
|
|
||||||
- recovery: 资产回收(从使用中回收)
|
|
||||||
- maintenance: 维修分配
|
|
||||||
- scrap: 报废分配
|
|
||||||
- **title**: 标题
|
|
||||||
- **source_organization_id**: 调出网点ID(可选,调拨时必填)
|
|
||||||
- **target_organization_id**: 调入网点ID
|
|
||||||
- **asset_ids**: 资产ID列表
|
|
||||||
- **expect_execute_date**: 预计执行日期
|
|
||||||
- **remark**: 备注
|
|
||||||
"""
|
|
||||||
return allocation_service.create_order(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
applicant_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{order_id}", response_model=dict)
|
|
||||||
def update_allocation_order(
|
|
||||||
order_id: int,
|
|
||||||
obj_in: AllocationOrderUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新分配单
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
- **title**: 标题
|
|
||||||
- **expect_execute_date**: 预计执行日期
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
只有待审批状态的分配单可以更新
|
|
||||||
"""
|
|
||||||
return allocation_service.update_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/approve", response_model=dict)
|
|
||||||
def approve_allocation_order(
|
|
||||||
order_id: int,
|
|
||||||
approval_in: AllocationOrderApproval,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
审批分配单
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
- **approval_status**: 审批状态(approved/rejected)
|
|
||||||
- **approval_remark**: 审批备注
|
|
||||||
|
|
||||||
审批通过后会自动执行资产分配逻辑
|
|
||||||
"""
|
|
||||||
return allocation_service.approve_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
approval_in=approval_in,
|
|
||||||
approver_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/execute", response_model=dict)
|
|
||||||
def execute_allocation_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
执行分配单
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
|
|
||||||
手动执行已审批通过的分配单
|
|
||||||
"""
|
|
||||||
return allocation_service.execute_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
executor_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/cancel", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def cancel_allocation_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
取消分配单
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
|
|
||||||
取消分配单(已完成的无法取消)
|
|
||||||
"""
|
|
||||||
allocation_service.cancel_order(db, order_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_allocation_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除分配单
|
|
||||||
|
|
||||||
- **order_id**: 分配单ID
|
|
||||||
|
|
||||||
只能删除草稿、已拒绝或已取消的分配单
|
|
||||||
"""
|
|
||||||
allocation_service.delete_order(db, order_id)
|
|
||||||
return None
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
"""
|
|
||||||
资产管理API路由
|
|
||||||
"""
|
|
||||||
from typing import List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.asset import (
|
|
||||||
AssetCreate,
|
|
||||||
AssetUpdate,
|
|
||||||
AssetResponse,
|
|
||||||
AssetWithRelations,
|
|
||||||
AssetStatusHistoryResponse,
|
|
||||||
AssetStatusTransition,
|
|
||||||
AssetQueryParams
|
|
||||||
)
|
|
||||||
from app.services.asset_service import asset_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[AssetResponse])
|
|
||||||
def get_assets(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
device_type_id: Optional[int] = Query(None, description="设备类型ID"),
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID"),
|
|
||||||
status: Optional[str] = Query(None, description="状态"),
|
|
||||||
purchase_date_start: Optional[str] = Query(None, description="采购日期开始(YYYY-MM-DD)"),
|
|
||||||
purchase_date_end: Optional[str] = Query(None, description="采购日期结束(YYYY-MM-DD)"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取资产列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **keyword**: 搜索关键词(编码/名称/型号/序列号)
|
|
||||||
- **device_type_id**: 设备类型ID筛选
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
- **status**: 状态筛选
|
|
||||||
- **purchase_date_start**: 采购日期开始
|
|
||||||
- **purchase_date_end**: 采购日期结束
|
|
||||||
"""
|
|
||||||
items, total = asset_service.get_assets(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
keyword=keyword,
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
organization_id=organization_id,
|
|
||||||
status=status,
|
|
||||||
purchase_date_start=purchase_date_start,
|
|
||||||
purchase_date_end=purchase_date_end
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics")
|
|
||||||
def get_asset_statistics(
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取资产统计信息
|
|
||||||
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回资产总数、总价值、状态分布等统计信息
|
|
||||||
"""
|
|
||||||
return asset_service.get_statistics(db, organization_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{asset_id}", response_model=AssetWithRelations)
|
|
||||||
def get_asset(
|
|
||||||
asset_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取资产详情
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
|
|
||||||
返回资产详情及其关联信息
|
|
||||||
"""
|
|
||||||
return asset_service.get_asset(db, asset_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scan/{asset_code}", response_model=AssetWithRelations)
|
|
||||||
def scan_asset(
|
|
||||||
asset_code: str,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
扫码查询资产
|
|
||||||
|
|
||||||
- **asset_code**: 资产编码
|
|
||||||
|
|
||||||
通过扫描二维码查询资产详情
|
|
||||||
"""
|
|
||||||
return asset_service.scan_asset_by_code(db, asset_code)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_asset(
|
|
||||||
obj_in: AssetCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建资产
|
|
||||||
|
|
||||||
- **asset_name**: 资产名称
|
|
||||||
- **device_type_id**: 设备类型ID
|
|
||||||
- **brand_id**: 品牌ID(可选)
|
|
||||||
- **model**: 规格型号
|
|
||||||
- **serial_number**: 序列号
|
|
||||||
- **supplier_id**: 供应商ID
|
|
||||||
- **purchase_date**: 采购日期
|
|
||||||
- **purchase_price**: 采购价格
|
|
||||||
- **warranty_period**: 保修期(月)
|
|
||||||
- **organization_id**: 所属网点ID
|
|
||||||
- **location**: 存放位置
|
|
||||||
- **dynamic_attributes**: 动态字段值
|
|
||||||
- **remark**: 备注
|
|
||||||
"""
|
|
||||||
return asset_service.create_asset(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
creator_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{asset_id}", response_model=AssetResponse)
|
|
||||||
def update_asset(
|
|
||||||
asset_id: int,
|
|
||||||
obj_in: AssetUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新资产
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
- **asset_name**: 资产名称
|
|
||||||
- **brand_id**: 品牌ID
|
|
||||||
- **model**: 规格型号
|
|
||||||
- **serial_number**: 序列号
|
|
||||||
- **supplier_id**: 供应商ID
|
|
||||||
- **purchase_date**: 采购日期
|
|
||||||
- **purchase_price**: 采购价格
|
|
||||||
- **warranty_period**: 保修期
|
|
||||||
- **organization_id**: 所属网点ID
|
|
||||||
- **location**: 存放位置
|
|
||||||
- **dynamic_attributes**: 动态字段值
|
|
||||||
- **remark**: 备注
|
|
||||||
"""
|
|
||||||
return asset_service.update_asset(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_asset(
|
|
||||||
asset_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除资产
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
|
|
||||||
软删除资产
|
|
||||||
"""
|
|
||||||
asset_service.delete_asset(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_id,
|
|
||||||
deleter_id=current_user.id
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 状态管理 =====
|
|
||||||
|
|
||||||
@router.post("/{asset_id}/status", response_model=AssetResponse)
|
|
||||||
def change_asset_status(
|
|
||||||
asset_id: int,
|
|
||||||
status_transition: AssetStatusTransition,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
变更资产状态
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
- **new_status**: 目标状态
|
|
||||||
- **remark**: 备注
|
|
||||||
- **extra_data**: 额外数据
|
|
||||||
|
|
||||||
状态说明:
|
|
||||||
- pending: 待入库
|
|
||||||
- in_stock: 库存中
|
|
||||||
- in_use: 使用中
|
|
||||||
- transferring: 调拨中
|
|
||||||
- maintenance: 维修中
|
|
||||||
- pending_scrap: 待报废
|
|
||||||
- scrapped: 已报废
|
|
||||||
- lost: 已丢失
|
|
||||||
"""
|
|
||||||
return asset_service.change_asset_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_id,
|
|
||||||
status_transition=status_transition,
|
|
||||||
operator_id=current_user.id,
|
|
||||||
operator_name=current_user.real_name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{asset_id}/history", response_model=List[AssetStatusHistoryResponse])
|
|
||||||
def get_asset_status_history(
|
|
||||||
asset_id: int,
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(50, ge=1, le=100, description="返回条数"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取资产状态历史
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数
|
|
||||||
|
|
||||||
返回资产的所有状态变更记录
|
|
||||||
"""
|
|
||||||
return asset_service.get_asset_status_history(db, asset_id, skip, limit)
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
"""
|
|
||||||
认证相关API路由
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Header
|
|
||||||
from fastapi.security import HTTPBearer
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.user import (
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
RefreshTokenRequest,
|
|
||||||
RefreshTokenResponse,
|
|
||||||
ChangePasswordRequest,
|
|
||||||
)
|
|
||||||
from app.services.auth_service import auth_service
|
|
||||||
from app.models.user import User
|
|
||||||
from app.core.response import success_response
|
|
||||||
from app.core.config import settings
|
|
||||||
from jose import jwt
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
security = HTTPBearer()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=LoginResponse)
|
|
||||||
async def login(
|
|
||||||
credentials: LoginRequest,
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
用户登录
|
|
||||||
|
|
||||||
- **username**: 用户名
|
|
||||||
- **password**: 密码
|
|
||||||
- **captcha**: 验证码
|
|
||||||
- **captcha_key**: 验证码UUID
|
|
||||||
"""
|
|
||||||
logger.info(f"登录请求 - 用户名: {credentials.username}, "
|
|
||||||
f"验证码: {credentials.captcha}, 验证码Key: {credentials.captcha_key}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = await auth_service.login(
|
|
||||||
db=db,
|
|
||||||
username=credentials.username,
|
|
||||||
password=credentials.password,
|
|
||||||
captcha=credentials.captcha,
|
|
||||||
captcha_key=credentials.captcha_key
|
|
||||||
)
|
|
||||||
logger.info(f"登录成功 - 用户名: {credentials.username}")
|
|
||||||
return success_response(data=result)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"登录失败 - 用户名: {credentials.username}, 错误: {str(e)}", exc_info=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refresh", response_model=RefreshTokenResponse)
|
|
||||||
async def refresh_token(
|
|
||||||
token_request: RefreshTokenRequest,
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
刷新访问令牌
|
|
||||||
|
|
||||||
- **refresh_token**: 刷新令牌
|
|
||||||
"""
|
|
||||||
result = await auth_service.refresh_token(
|
|
||||||
db=db,
|
|
||||||
refresh_token=token_request.refresh_token
|
|
||||||
)
|
|
||||||
return success_response(data=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout")
|
|
||||||
async def logout(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
authorization: str = Header(...),
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
用户登出
|
|
||||||
"""
|
|
||||||
from app.utils.redis_client import redis_client
|
|
||||||
|
|
||||||
# 提取Token
|
|
||||||
token = authorization.replace("Bearer ", "")
|
|
||||||
|
|
||||||
# 获取Token剩余有效期
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
|
||||||
exp = payload.get("exp")
|
|
||||||
|
|
||||||
if exp:
|
|
||||||
# 计算剩余秒数
|
|
||||||
remaining_time = int(exp) - int(datetime.utcnow().timestamp())
|
|
||||||
|
|
||||||
if remaining_time > 0:
|
|
||||||
# 将Token加入黑名单
|
|
||||||
await redis_client.setex(
|
|
||||||
f"blacklist:{token}",
|
|
||||||
remaining_time,
|
|
||||||
"1"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Token黑名单添加失败: {str(e)}")
|
|
||||||
|
|
||||||
return success_response(message="登出成功")
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/change-password")
|
|
||||||
async def change_password(
|
|
||||||
password_data: ChangePasswordRequest,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
修改密码
|
|
||||||
|
|
||||||
- **old_password**: 旧密码
|
|
||||||
- **new_password**: 新密码
|
|
||||||
- **confirm_password**: 确认密码
|
|
||||||
"""
|
|
||||||
await auth_service.change_password(
|
|
||||||
db=db,
|
|
||||||
user=current_user,
|
|
||||||
old_password=password_data.old_password,
|
|
||||||
new_password=password_data.new_password
|
|
||||||
)
|
|
||||||
return success_response(message="密码修改成功")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/captcha")
|
|
||||||
async def get_captcha():
|
|
||||||
"""
|
|
||||||
获取验证码
|
|
||||||
|
|
||||||
返回验证码图片和captcha_key
|
|
||||||
"""
|
|
||||||
captcha_data = await auth_service._generate_captcha()
|
|
||||||
|
|
||||||
return success_response(data={
|
|
||||||
"captcha_key": captcha_data["captcha_key"],
|
|
||||||
"captcha_image": captcha_data["captcha_base64"]
|
|
||||||
})
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"""
|
|
||||||
品牌和供应商API路由
|
|
||||||
"""
|
|
||||||
from typing import List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.brand_supplier import (
|
|
||||||
BrandCreate,
|
|
||||||
BrandUpdate,
|
|
||||||
BrandResponse,
|
|
||||||
SupplierCreate,
|
|
||||||
SupplierUpdate,
|
|
||||||
SupplierResponse
|
|
||||||
)
|
|
||||||
from app.services.brand_supplier_service import brand_service, supplier_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 品牌管理 =====
|
|
||||||
|
|
||||||
@router.get("/brands", response_model=List[BrandResponse])
|
|
||||||
def get_brands(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
status: Optional[str] = Query(None, description="状态筛选"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""获取品牌列表"""
|
|
||||||
items, total = brand_service.get_brands(db, skip, limit, status, keyword)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/brands/{brand_id}", response_model=BrandResponse)
|
|
||||||
def get_brand(
|
|
||||||
brand_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""获取品牌详情"""
|
|
||||||
return brand_service.get_brand(db, brand_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/brands", response_model=BrandResponse, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_brand(
|
|
||||||
obj_in: BrandCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""创建品牌"""
|
|
||||||
return brand_service.create_brand(db, obj_in, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/brands/{brand_id}", response_model=BrandResponse)
|
|
||||||
def update_brand(
|
|
||||||
brand_id: int,
|
|
||||||
obj_in: BrandUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""更新品牌"""
|
|
||||||
return brand_service.update_brand(db, brand_id, obj_in, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/brands/{brand_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_brand(
|
|
||||||
brand_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""删除品牌"""
|
|
||||||
brand_service.delete_brand(db, brand_id, current_user.id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 供应商管理 =====
|
|
||||||
|
|
||||||
@router.get("/suppliers", response_model=List[SupplierResponse])
|
|
||||||
def get_suppliers(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
status: Optional[str] = Query(None, description="状态筛选"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""获取供应商列表"""
|
|
||||||
items, total = supplier_service.get_suppliers(db, skip, limit, status, keyword)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/suppliers/{supplier_id}", response_model=SupplierResponse)
|
|
||||||
def get_supplier(
|
|
||||||
supplier_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""获取供应商详情"""
|
|
||||||
return supplier_service.get_supplier(db, supplier_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/suppliers", response_model=SupplierResponse, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_supplier(
|
|
||||||
obj_in: SupplierCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""创建供应商"""
|
|
||||||
return supplier_service.create_supplier(db, obj_in, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/suppliers/{supplier_id}", response_model=SupplierResponse)
|
|
||||||
def update_supplier(
|
|
||||||
supplier_id: int,
|
|
||||||
obj_in: SupplierUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""更新供应商"""
|
|
||||||
return supplier_service.update_supplier(db, supplier_id, obj_in, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/suppliers/{supplier_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_supplier(
|
|
||||||
supplier_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""删除供应商"""
|
|
||||||
supplier_service.delete_supplier(db, supplier_id, current_user.id)
|
|
||||||
return None
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
"""
|
|
||||||
设备类型API路由
|
|
||||||
"""
|
|
||||||
from typing import List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.device_type import (
|
|
||||||
DeviceTypeCreate,
|
|
||||||
DeviceTypeUpdate,
|
|
||||||
DeviceTypeResponse,
|
|
||||||
DeviceTypeWithFields,
|
|
||||||
DeviceTypeFieldCreate,
|
|
||||||
DeviceTypeFieldUpdate,
|
|
||||||
DeviceTypeFieldResponse
|
|
||||||
)
|
|
||||||
from app.services.device_type_service import device_type_service
|
|
||||||
from app.utils.redis_client import redis_client
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
# 异步缓存包装器
|
|
||||||
@redis_client.cached_async("device_types:list", expire=1800) # 缓存30分钟
|
|
||||||
async def _cached_get_device_types(
|
|
||||||
skip: int,
|
|
||||||
limit: int,
|
|
||||||
category: Optional[str],
|
|
||||||
status: Optional[str],
|
|
||||||
keyword: Optional[str],
|
|
||||||
db: Session
|
|
||||||
):
|
|
||||||
"""获取设备类型列表的缓存包装器"""
|
|
||||||
items, total = device_type_service.get_device_types(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
category=category,
|
|
||||||
status=status,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@redis_client.cached_async("device_types:categories", expire=1800) # 缓存30分钟
|
|
||||||
async def _cached_get_device_type_categories(db: Session):
|
|
||||||
"""获取所有设备分类的缓存包装器"""
|
|
||||||
return device_type_service.get_all_categories(db)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[DeviceTypeResponse])
|
|
||||||
async def get_device_types(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
category: Optional[str] = Query(None, description="设备分类"),
|
|
||||||
status: Optional[str] = Query(None, description="状态"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取设备类型列表(已启用缓存,30分钟)
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **category**: 设备分类筛选
|
|
||||||
- **status**: 状态筛选(active/inactive)
|
|
||||||
- **keyword**: 搜索关键词(代码或名称)
|
|
||||||
"""
|
|
||||||
return await _cached_get_device_types(
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
category=category,
|
|
||||||
status=status,
|
|
||||||
keyword=keyword,
|
|
||||||
db=db
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/categories", response_model=List[str])
|
|
||||||
async def get_device_type_categories(
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取所有设备分类(已启用缓存,30分钟)
|
|
||||||
|
|
||||||
返回所有使用中的设备分类列表
|
|
||||||
"""
|
|
||||||
return await _cached_get_device_type_categories(db)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{device_type_id}", response_model=DeviceTypeWithFields)
|
|
||||||
def get_device_type(
|
|
||||||
device_type_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取设备类型详情
|
|
||||||
|
|
||||||
- **device_type_id**: 设备类型ID
|
|
||||||
|
|
||||||
返回设备类型详情及其字段列表
|
|
||||||
"""
|
|
||||||
return device_type_service.get_device_type(db, device_type_id, include_fields=True)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=DeviceTypeResponse, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_device_type(
|
|
||||||
obj_in: DeviceTypeCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建设备类型
|
|
||||||
|
|
||||||
- **type_code**: 设备类型代码(唯一)
|
|
||||||
- **type_name**: 设备类型名称
|
|
||||||
- **category**: 设备分类
|
|
||||||
- **description**: 描述
|
|
||||||
- **icon**: 图标名称
|
|
||||||
- **sort_order**: 排序
|
|
||||||
"""
|
|
||||||
return device_type_service.create_device_type(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
creator_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{device_type_id}", response_model=DeviceTypeResponse)
|
|
||||||
def update_device_type(
|
|
||||||
device_type_id: int,
|
|
||||||
obj_in: DeviceTypeUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新设备类型
|
|
||||||
|
|
||||||
- **device_type_id**: 设备类型ID
|
|
||||||
- **type_name**: 设备类型名称
|
|
||||||
- **category**: 设备分类
|
|
||||||
- **description**: 描述
|
|
||||||
- **icon**: 图标名称
|
|
||||||
- **status**: 状态
|
|
||||||
- **sort_order**: 排序
|
|
||||||
"""
|
|
||||||
return device_type_service.update_device_type(
|
|
||||||
db=db,
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{device_type_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_device_type(
|
|
||||||
device_type_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除设备类型
|
|
||||||
|
|
||||||
- **device_type_id**: 设备类型ID
|
|
||||||
|
|
||||||
软删除设备类型及其所有字段
|
|
||||||
"""
|
|
||||||
device_type_service.delete_device_type(
|
|
||||||
db=db,
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
deleter_id=current_user.id
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 字段管理 =====
|
|
||||||
|
|
||||||
@router.get("/{device_type_id}/fields", response_model=List[DeviceTypeFieldResponse])
|
|
||||||
def get_device_type_fields(
|
|
||||||
device_type_id: int,
|
|
||||||
status: Optional[str] = Query(None, description="状态筛选"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取设备类型的字段列表
|
|
||||||
|
|
||||||
- **device_type_id**: 设备类型ID
|
|
||||||
- **status**: 状态筛选(active/inactive)
|
|
||||||
|
|
||||||
返回指定设备类型的所有字段定义
|
|
||||||
"""
|
|
||||||
return device_type_service.get_device_type_fields(db, device_type_id, status)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{device_type_id}/fields", response_model=DeviceTypeFieldResponse, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_device_type_field(
|
|
||||||
device_type_id: int,
|
|
||||||
obj_in: DeviceTypeFieldCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建设备类型字段
|
|
||||||
|
|
||||||
- **device_type_id**: 设备类型ID
|
|
||||||
- **field_code**: 字段代码(在同一设备类型下唯一)
|
|
||||||
- **field_name**: 字段名称
|
|
||||||
- **field_type**: 字段类型(text/number/date/select/multiselect/boolean/textarea)
|
|
||||||
- **is_required**: 是否必填
|
|
||||||
- **default_value**: 默认值
|
|
||||||
- **options**: 选项列表(用于select/multiselect类型)
|
|
||||||
- **validation_rules**: 验证规则
|
|
||||||
- **placeholder**: 占位符
|
|
||||||
- **help_text**: 帮助文本
|
|
||||||
- **sort_order**: 排序
|
|
||||||
"""
|
|
||||||
return device_type_service.create_device_type_field(
|
|
||||||
db=db,
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
creator_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/fields/{field_id}", response_model=DeviceTypeFieldResponse)
|
|
||||||
def update_device_type_field(
|
|
||||||
field_id: int,
|
|
||||||
obj_in: DeviceTypeFieldUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新设备类型字段
|
|
||||||
|
|
||||||
- **field_id**: 字段ID
|
|
||||||
- **field_name**: 字段名称
|
|
||||||
- **field_type**: 字段类型
|
|
||||||
- **is_required**: 是否必填
|
|
||||||
- **default_value**: 默认值
|
|
||||||
- **options**: 选项列表
|
|
||||||
- **validation_rules**: 验证规则
|
|
||||||
- **placeholder**: 占位符
|
|
||||||
- **help_text**: 帮助文本
|
|
||||||
- **status**: 状态
|
|
||||||
- **sort_order**: 排序
|
|
||||||
"""
|
|
||||||
return device_type_service.update_device_type_field(
|
|
||||||
db=db,
|
|
||||||
field_id=field_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/fields/{field_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_device_type_field(
|
|
||||||
field_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除设备类型字段
|
|
||||||
|
|
||||||
- **field_id**: 字段ID
|
|
||||||
|
|
||||||
软删除字段
|
|
||||||
"""
|
|
||||||
device_type_service.delete_device_type_field(
|
|
||||||
db=db,
|
|
||||||
field_id=field_id,
|
|
||||||
deleter_id=current_user.id
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
"""
|
|
||||||
文件管理API路由
|
|
||||||
"""
|
|
||||||
from typing import List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, Form
|
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.file_management import (
|
|
||||||
UploadedFileCreate,
|
|
||||||
UploadedFileUpdate,
|
|
||||||
UploadedFileResponse,
|
|
||||||
UploadedFileWithUrl,
|
|
||||||
FileUploadResponse,
|
|
||||||
FileShareCreate,
|
|
||||||
FileShareResponse,
|
|
||||||
FileBatchDelete,
|
|
||||||
FileStatistics,
|
|
||||||
ChunkUploadInit,
|
|
||||||
ChunkUploadInfo,
|
|
||||||
ChunkUploadComplete
|
|
||||||
)
|
|
||||||
from app.crud.file_management import uploaded_file
|
|
||||||
from app.services.file_service import file_service, chunk_upload_manager
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload", response_model=FileUploadResponse)
|
|
||||||
async def upload_file(
|
|
||||||
file: UploadFile = File(..., description="上传的文件"),
|
|
||||||
remark: Optional[str] = Form(None, description="备注"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
上传文件
|
|
||||||
|
|
||||||
- **file**: 上传的文件
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
支持的文件类型:
|
|
||||||
- 图片: JPEG, PNG, GIF, BMP, WebP, SVG
|
|
||||||
- 文档: PDF, Word, Excel, PowerPoint, TXT, CSV
|
|
||||||
- 压缩包: ZIP, RAR, 7Z
|
|
||||||
|
|
||||||
文件大小限制:
|
|
||||||
- 图片: 最大10MB
|
|
||||||
- 其他: 最大100MB
|
|
||||||
"""
|
|
||||||
# 上传文件
|
|
||||||
file_obj = await file_service.upload_file(
|
|
||||||
db=db,
|
|
||||||
file=file,
|
|
||||||
uploader_id=current_user.id,
|
|
||||||
remark=remark
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成访问URL
|
|
||||||
base_url = "http://localhost:8000" # TODO: 从配置读取
|
|
||||||
download_url = f"{base_url}/api/v1/files/{file_obj.id}/download"
|
|
||||||
preview_url = None
|
|
||||||
if file_obj.file_type and file_obj.file_type.startswith('image/'):
|
|
||||||
preview_url = f"{base_url}/api/v1/files/{file_obj.id}/preview"
|
|
||||||
|
|
||||||
return FileUploadResponse(
|
|
||||||
id=file_obj.id,
|
|
||||||
file_name=file_obj.file_name,
|
|
||||||
original_name=file_obj.original_name,
|
|
||||||
file_size=file_obj.file_size,
|
|
||||||
file_type=file_obj.file_type,
|
|
||||||
file_path=file_obj.file_path,
|
|
||||||
download_url=download_url,
|
|
||||||
preview_url=preview_url,
|
|
||||||
message="上传成功"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[UploadedFileResponse])
|
|
||||||
def get_files(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
file_type: Optional[str] = Query(None, description="文件类型"),
|
|
||||||
uploader_id: Optional[int] = Query(None, description="上传者ID"),
|
|
||||||
start_date: Optional[str] = Query(None, description="开始日期(YYYY-MM-DD)"),
|
|
||||||
end_date: Optional[str] = Query(None, description="结束日期(YYYY-MM-DD)"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取文件列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **keyword**: 搜索关键词(文件名)
|
|
||||||
- **file_type**: 文件类型筛选
|
|
||||||
- **uploader_id**: 上传者ID筛选
|
|
||||||
- **start_date**: 开始日期
|
|
||||||
- **end_date**: 结束日期
|
|
||||||
"""
|
|
||||||
items, total = uploaded_file.get_multi(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
keyword=keyword,
|
|
||||||
file_type=file_type,
|
|
||||||
uploader_id=uploader_id,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加上传者姓名
|
|
||||||
result = []
|
|
||||||
for item in items:
|
|
||||||
item_dict = UploadedFileResponse.from_orm(item).dict()
|
|
||||||
if item.uploader:
|
|
||||||
item_dict['uploader_name'] = item.uploader.real_name
|
|
||||||
result.append(UploadedFileResponse(**item_dict))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=FileStatistics)
|
|
||||||
def get_file_statistics(
|
|
||||||
uploader_id: Optional[int] = Query(None, description="上传者ID筛选"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取文件统计信息
|
|
||||||
|
|
||||||
- **uploader_id**: 上传者ID筛选
|
|
||||||
|
|
||||||
返回文件总数、总大小、类型分布等统计信息
|
|
||||||
"""
|
|
||||||
return file_service.get_statistics(db, uploader_id=uploader_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{file_id}", response_model=UploadedFileWithUrl)
|
|
||||||
def get_file(
|
|
||||||
file_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取文件详情
|
|
||||||
|
|
||||||
- **file_id**: 文件ID
|
|
||||||
|
|
||||||
返回文件详情及访问URL
|
|
||||||
"""
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成访问URL
|
|
||||||
base_url = "http://localhost:8000"
|
|
||||||
file_dict = UploadedFileWithUrl.from_orm(file_obj).dict()
|
|
||||||
file_dict['download_url'] = f"{base_url}/api/v1/files/{file_id}/download"
|
|
||||||
|
|
||||||
if file_obj.file_type and file_obj.file_type.startswith('image/'):
|
|
||||||
file_dict['preview_url'] = f"{base_url}/api/v1/files/{file_id}/preview"
|
|
||||||
|
|
||||||
if file_obj.share_code:
|
|
||||||
file_dict['share_url'] = f"{base_url}/api/v1/files/share/{file_obj.share_code}"
|
|
||||||
|
|
||||||
return UploadedFileWithUrl(**file_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{file_id}/download")
|
|
||||||
def download_file(
|
|
||||||
file_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
下载文件
|
|
||||||
|
|
||||||
- **file_id**: 文件ID
|
|
||||||
|
|
||||||
返回文件流
|
|
||||||
"""
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not file_service.file_exists(file_obj):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件已被删除或移动"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 增加下载次数
|
|
||||||
uploaded_file.increment_download_count(db, file_id=file_id)
|
|
||||||
|
|
||||||
# 返回文件
|
|
||||||
file_path = file_service.get_file_path(file_obj)
|
|
||||||
return FileResponse(
|
|
||||||
path=str(file_path),
|
|
||||||
filename=file_obj.original_name,
|
|
||||||
media_type=file_obj.file_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{file_id}/preview")
|
|
||||||
def preview_file(
|
|
||||||
file_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
预览文件
|
|
||||||
|
|
||||||
- **file_id**: 文件ID
|
|
||||||
|
|
||||||
支持图片直接预览,其他文件类型可能需要转换为预览格式
|
|
||||||
"""
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not file_service.file_exists(file_obj):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件已被删除或移动"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查文件类型是否支持预览
|
|
||||||
if not file_obj.file_type or not file_obj.file_type.startswith('image/'):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="该文件类型不支持在线预览"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 返回缩略图(如果存在)
|
|
||||||
if file_obj.thumbnail_path:
|
|
||||||
thumbnail_path = file_obj.thumbnail_path
|
|
||||||
if Path(thumbnail_path).exists():
|
|
||||||
return FileResponse(
|
|
||||||
path=thumbnail_path,
|
|
||||||
media_type="image/jpeg"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 返回原图
|
|
||||||
file_path = file_service.get_file_path(file_obj)
|
|
||||||
return FileResponse(
|
|
||||||
path=str(file_path),
|
|
||||||
media_type=file_obj.file_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{file_id}", response_model=UploadedFileResponse)
|
|
||||||
def update_file(
|
|
||||||
file_id: int,
|
|
||||||
obj_in: UploadedFileUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新文件信息
|
|
||||||
|
|
||||||
- **file_id**: 文件ID
|
|
||||||
- **remark**: 备注
|
|
||||||
"""
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查权限:只有上传者可以更新
|
|
||||||
if file_obj.uploader_id != current_user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="无权限修改此文件"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新文件
|
|
||||||
file_obj = uploaded_file.update(
|
|
||||||
db,
|
|
||||||
db_obj=file_obj,
|
|
||||||
obj_in=obj_in.dict(exclude_unset=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return UploadedFileResponse.from_orm(file_obj)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_file(
|
|
||||||
file_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除文件
|
|
||||||
|
|
||||||
- **file_id**: 文件ID
|
|
||||||
|
|
||||||
软删除文件记录和物理删除文件
|
|
||||||
"""
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查权限:只有上传者可以删除
|
|
||||||
if file_obj.uploader_id != current_user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="无权限删除此文件"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 软删除数据库记录
|
|
||||||
uploaded_file.delete(db, db_obj=file_obj, deleter_id=current_user.id)
|
|
||||||
|
|
||||||
# 从磁盘删除文件
|
|
||||||
file_service.delete_file_from_disk(file_obj)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/batch", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_files_batch(
|
|
||||||
obj_in: FileBatchDelete,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
批量删除文件
|
|
||||||
|
|
||||||
- **file_ids**: 文件ID列表
|
|
||||||
|
|
||||||
批量软删除文件记录和物理删除文件
|
|
||||||
"""
|
|
||||||
# 软删除数据库记录
|
|
||||||
count = uploaded_file.delete_batch(
|
|
||||||
db,
|
|
||||||
file_ids=obj_in.file_ids,
|
|
||||||
deleter_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 从磁盘删除文件
|
|
||||||
for file_id in obj_in.file_ids:
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if file_obj and file_obj.uploader_id == current_user.id:
|
|
||||||
file_service.delete_file_from_disk(file_obj)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{file_id}/share", response_model=FileShareResponse)
|
|
||||||
def create_share_link(
|
|
||||||
file_id: int,
|
|
||||||
share_in: FileShareCreate = FileShareCreate(),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
生成分享链接
|
|
||||||
|
|
||||||
- **file_id**: 文件ID
|
|
||||||
- **expire_days**: 有效期(天),默认7天,最大30天
|
|
||||||
|
|
||||||
生成用于文件分享的临时链接
|
|
||||||
"""
|
|
||||||
file_obj = uploaded_file.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查权限:只有上传者可以分享
|
|
||||||
if file_obj.uploader_id != current_user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="无权限分享此文件"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成分享链接
|
|
||||||
base_url = "http://localhost:8000"
|
|
||||||
return file_service.generate_share_link(
|
|
||||||
db,
|
|
||||||
file_id=file_id,
|
|
||||||
expire_days=share_in.expire_days,
|
|
||||||
base_url=base_url
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/share/{share_code}")
|
|
||||||
def access_shared_file(
|
|
||||||
share_code: str,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
访问分享的文件
|
|
||||||
|
|
||||||
- **share_code**: 分享码
|
|
||||||
|
|
||||||
通过分享码访问文件(无需登录)
|
|
||||||
"""
|
|
||||||
file_obj = file_service.get_shared_file(db, share_code)
|
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="分享链接不存在或已过期"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not file_service.file_exists(file_obj):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件已被删除或移动"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 增加下载次数
|
|
||||||
uploaded_file.increment_download_count(db, file_id=file_obj.id)
|
|
||||||
|
|
||||||
# 返回文件
|
|
||||||
file_path = file_service.get_file_path(file_obj)
|
|
||||||
return FileResponse(
|
|
||||||
path=str(file_path),
|
|
||||||
filename=file_obj.original_name,
|
|
||||||
media_type=file_obj.file_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 分片上传 =====
|
|
||||||
|
|
||||||
@router.post("/chunks/init")
|
|
||||||
def init_chunk_upload(
|
|
||||||
obj_in: ChunkUploadInit,
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
初始化分片上传
|
|
||||||
|
|
||||||
- **file_name**: 文件名
|
|
||||||
- **file_size**: 文件大小(字节)
|
|
||||||
- **file_type**: 文件类型
|
|
||||||
- **total_chunks**: 总分片数
|
|
||||||
- **file_hash**: 文件哈希(可选)
|
|
||||||
|
|
||||||
返回上传ID,用于后续上传分片
|
|
||||||
"""
|
|
||||||
upload_id = chunk_upload_manager.init_upload(
|
|
||||||
file_name=obj_in.file_name,
|
|
||||||
file_size=obj_in.file_size,
|
|
||||||
file_type=obj_in.file_type,
|
|
||||||
total_chunks=obj_in.total_chunks,
|
|
||||||
file_hash=obj_in.file_hash
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"upload_id": upload_id, "message": "初始化成功"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/chunks/upload")
|
|
||||||
async def upload_chunk(
|
|
||||||
upload_id: str,
|
|
||||||
chunk_index: int,
|
|
||||||
chunk: UploadFile = File(..., description="分片文件"),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
上传分片
|
|
||||||
|
|
||||||
- **upload_id**: 上传ID
|
|
||||||
- **chunk_index**: 分片索引(从0开始)
|
|
||||||
- **chunk**: 分片文件
|
|
||||||
"""
|
|
||||||
content = await chunk.read()
|
|
||||||
success = chunk_upload_manager.save_chunk(upload_id, chunk_index, content)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="上传会话不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": f"分片 {chunk_index} 上传成功"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/chunks/complete", response_model=FileUploadResponse)
|
|
||||||
def complete_chunk_upload(
|
|
||||||
obj_in: ChunkUploadComplete,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
完成分片上传
|
|
||||||
|
|
||||||
- **upload_id**: 上传ID
|
|
||||||
- **file_name**: 文件名
|
|
||||||
- **file_hash**: 文件哈希(可选)
|
|
||||||
|
|
||||||
合并所有分片并创建文件记录
|
|
||||||
"""
|
|
||||||
# 合并分片
|
|
||||||
try:
|
|
||||||
file_obj = chunk_upload_manager.merge_chunks(
|
|
||||||
db=db,
|
|
||||||
upload_id=obj_in.upload_id,
|
|
||||||
uploader_id=current_user.id,
|
|
||||||
file_service=file_service
|
|
||||||
)
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"合并分片失败: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成访问URL
|
|
||||||
base_url = "http://localhost:8000"
|
|
||||||
download_url = f"{base_url}/api/v1/files/{file_obj.id}/download"
|
|
||||||
preview_url = None
|
|
||||||
if file_obj.file_type and file_obj.file_type.startswith('image/'):
|
|
||||||
preview_url = f"{base_url}/api/v1/files/{file_obj.id}/preview"
|
|
||||||
|
|
||||||
return FileUploadResponse(
|
|
||||||
id=file_obj.id,
|
|
||||||
file_name=file_obj.file_name,
|
|
||||||
original_name=file_obj.original_name,
|
|
||||||
file_size=file_obj.file_size,
|
|
||||||
file_type=file_obj.file_type,
|
|
||||||
file_path=file_obj.file_path,
|
|
||||||
download_url=download_url,
|
|
||||||
preview_url=preview_url,
|
|
||||||
message="上传成功"
|
|
||||||
)
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
"""
|
|
||||||
维修管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.maintenance import (
|
|
||||||
MaintenanceRecordCreate,
|
|
||||||
MaintenanceRecordUpdate,
|
|
||||||
MaintenanceRecordStart,
|
|
||||||
MaintenanceRecordComplete,
|
|
||||||
MaintenanceRecordWithRelations,
|
|
||||||
MaintenanceRecordQueryParams,
|
|
||||||
MaintenanceStatistics
|
|
||||||
)
|
|
||||||
from app.services.maintenance_service import maintenance_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list)
|
|
||||||
def get_maintenance_records(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
asset_id: Optional[int] = Query(None, description="资产ID"),
|
|
||||||
status: Optional[str] = Query(None, description="状态"),
|
|
||||||
fault_type: Optional[str] = Query(None, description="故障类型"),
|
|
||||||
priority: Optional[str] = Query(None, description="优先级"),
|
|
||||||
maintenance_type: Optional[str] = Query(None, description="维修类型"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取维修记录列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **asset_id**: 资产ID筛选
|
|
||||||
- **status**: 状态筛选(pending/in_progress/completed/cancelled)
|
|
||||||
- **fault_type**: 故障类型筛选(hardware/software/network/other)
|
|
||||||
- **priority**: 优先级筛选(low/normal/high/urgent)
|
|
||||||
- **maintenance_type**: 维修类型筛选(self_repair/vendor_repair/warranty)
|
|
||||||
- **keyword**: 搜索关键词(单号/资产编码/故障描述)
|
|
||||||
"""
|
|
||||||
items, total = maintenance_service.get_records(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
asset_id=asset_id,
|
|
||||||
status=status,
|
|
||||||
fault_type=fault_type,
|
|
||||||
priority=priority,
|
|
||||||
maintenance_type=maintenance_type,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=MaintenanceStatistics)
|
|
||||||
def get_maintenance_statistics(
|
|
||||||
asset_id: Optional[int] = Query(None, description="资产ID"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取维修统计信息
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID(可选)
|
|
||||||
|
|
||||||
返回维修记录总数、待处理数、维修中数、已完成数等统计信息
|
|
||||||
"""
|
|
||||||
return maintenance_service.get_statistics(db, asset_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{record_id}", response_model=dict)
|
|
||||||
def get_maintenance_record(
|
|
||||||
record_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取维修记录详情
|
|
||||||
|
|
||||||
- **record_id**: 维修记录ID
|
|
||||||
|
|
||||||
返回维修记录详情及其关联信息
|
|
||||||
"""
|
|
||||||
return maintenance_service.get_record(db, record_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=dict, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_maintenance_record(
|
|
||||||
obj_in: MaintenanceRecordCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建维修记录
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
- **fault_description**: 故障描述
|
|
||||||
- **fault_type**: 故障类型(hardware/software/network/other)
|
|
||||||
- **priority**: 优先级(low/normal/high/urgent)
|
|
||||||
- **maintenance_type**: 维修类型(self_repair/vendor_repair/warranty)
|
|
||||||
- **vendor_id**: 维修供应商ID(外部维修时必填)
|
|
||||||
- **maintenance_cost**: 维修费用
|
|
||||||
- **maintenance_result**: 维修结果描述
|
|
||||||
- **replaced_parts**: 更换的配件
|
|
||||||
- **images**: 维修图片URL(多个逗号分隔)
|
|
||||||
- **remark**: 备注
|
|
||||||
"""
|
|
||||||
return maintenance_service.create_record(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
report_user_id=current_user.id,
|
|
||||||
creator_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{record_id}", response_model=dict)
|
|
||||||
def update_maintenance_record(
|
|
||||||
record_id: int,
|
|
||||||
obj_in: MaintenanceRecordUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新维修记录
|
|
||||||
|
|
||||||
- **record_id**: 维修记录ID
|
|
||||||
- **fault_description**: 故障描述
|
|
||||||
- **fault_type**: 故障类型
|
|
||||||
- **priority**: 优先级
|
|
||||||
- **maintenance_type**: 维修类型
|
|
||||||
- **vendor_id**: 维修供应商ID
|
|
||||||
- **maintenance_cost**: 维修费用
|
|
||||||
- **maintenance_result**: 维修结果描述
|
|
||||||
- **replaced_parts**: 更换的配件
|
|
||||||
- **images**: 维修图片URL
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
已完成的维修记录不能更新
|
|
||||||
"""
|
|
||||||
return maintenance_service.update_record(
|
|
||||||
db=db,
|
|
||||||
record_id=record_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{record_id}/start", response_model=dict)
|
|
||||||
def start_maintenance(
|
|
||||||
record_id: int,
|
|
||||||
start_in: MaintenanceRecordStart,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
开始维修
|
|
||||||
|
|
||||||
- **record_id**: 维修记录ID
|
|
||||||
- **maintenance_type**: 维修类型
|
|
||||||
- self_repair: 自行维修
|
|
||||||
- vendor_repair: 外部维修(需指定vendor_id)
|
|
||||||
- warranty: 保修维修
|
|
||||||
- **vendor_id**: 维修供应商ID(外部维修时必填)
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
只有待处理状态的维修记录可以开始维修
|
|
||||||
"""
|
|
||||||
return maintenance_service.start_maintenance(
|
|
||||||
db=db,
|
|
||||||
record_id=record_id,
|
|
||||||
start_in=start_in,
|
|
||||||
maintenance_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{record_id}/complete", response_model=dict)
|
|
||||||
def complete_maintenance(
|
|
||||||
record_id: int,
|
|
||||||
complete_in: MaintenanceRecordComplete,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
完成维修
|
|
||||||
|
|
||||||
- **record_id**: 维修记录ID
|
|
||||||
- **maintenance_result**: 维修结果描述
|
|
||||||
- **maintenance_cost**: 维修费用
|
|
||||||
- **replaced_parts**: 更换的配件
|
|
||||||
- **images**: 维修图片URL
|
|
||||||
- **asset_status**: 资产维修后状态(in_stock/in_use)
|
|
||||||
|
|
||||||
只有维修中的记录可以完成
|
|
||||||
"""
|
|
||||||
return maintenance_service.complete_maintenance(
|
|
||||||
db=db,
|
|
||||||
record_id=record_id,
|
|
||||||
complete_in=complete_in,
|
|
||||||
maintenance_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{record_id}/cancel", response_model=dict)
|
|
||||||
def cancel_maintenance(
|
|
||||||
record_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
取消维修
|
|
||||||
|
|
||||||
- **record_id**: 维修记录ID
|
|
||||||
|
|
||||||
已完成的维修记录不能取消
|
|
||||||
"""
|
|
||||||
return maintenance_service.cancel_maintenance(db, record_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{record_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_maintenance_record(
|
|
||||||
record_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除维修记录
|
|
||||||
|
|
||||||
- **record_id**: 维修记录ID
|
|
||||||
|
|
||||||
只能删除待处理或已取消的维修记录
|
|
||||||
"""
|
|
||||||
maintenance_service.delete_record(db, record_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/asset/{asset_id}", response_model=list)
|
|
||||||
def get_asset_maintenance_records(
|
|
||||||
asset_id: int,
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(50, ge=1, le=100, description="返回条数"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取资产的维修记录
|
|
||||||
|
|
||||||
- **asset_id**: 资产ID
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数
|
|
||||||
"""
|
|
||||||
return maintenance_service.get_asset_records(db, asset_id, skip, limit)
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
"""
|
|
||||||
消息通知管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import and_
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.notification import (
|
|
||||||
NotificationCreate,
|
|
||||||
NotificationUpdate,
|
|
||||||
NotificationResponse,
|
|
||||||
NotificationQueryParams,
|
|
||||||
NotificationBatchCreate,
|
|
||||||
NotificationBatchUpdate,
|
|
||||||
NotificationStatistics,
|
|
||||||
NotificationSendFromTemplate
|
|
||||||
)
|
|
||||||
from app.services.notification_service import notification_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=Dict[str, Any])
|
|
||||||
async def get_notifications(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
notification_type: Optional[str] = Query(None, description="通知类型"),
|
|
||||||
priority: Optional[str] = Query(None, description="优先级"),
|
|
||||||
is_read: Optional[bool] = Query(None, description="是否已读"),
|
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
|
||||||
keyword: Optional[str] = Query(None, description="关键词"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取消息通知列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **notification_type**: 通知类型筛选
|
|
||||||
- **priority**: 优先级筛选
|
|
||||||
- **is_read**: 是否已读筛选
|
|
||||||
- **start_time**: 开始时间筛选
|
|
||||||
- **end_time**: 结束时间筛选
|
|
||||||
- **keyword**: 关键词搜索
|
|
||||||
|
|
||||||
注意:普通用户只能查看自己的通知,管理员可以查看所有通知
|
|
||||||
"""
|
|
||||||
recipient_id = None if current_user.is_superuser else current_user.id
|
|
||||||
|
|
||||||
return await notification_service.get_notifications(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
recipient_id=recipient_id,
|
|
||||||
notification_type=notification_type,
|
|
||||||
priority=priority,
|
|
||||||
is_read=is_read,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/unread-count", response_model=Dict[str, Any])
|
|
||||||
async def get_unread_count(
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取当前用户未读通知数量
|
|
||||||
|
|
||||||
返回未读通知数量
|
|
||||||
"""
|
|
||||||
return await notification_service.get_unread_count(db, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=Dict[str, Any])
|
|
||||||
async def get_notification_statistics(
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取当前用户通知统计信息
|
|
||||||
|
|
||||||
返回通知总数、未读数、已读数、高优先级数、紧急通知数、类型分布等统计信息
|
|
||||||
"""
|
|
||||||
return await notification_service.get_statistics(db, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{notification_id}", response_model=Dict[str, Any])
|
|
||||||
async def get_notification(
|
|
||||||
notification_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取消息通知详情
|
|
||||||
|
|
||||||
- **notification_id**: 通知ID
|
|
||||||
|
|
||||||
注意:只能查看自己的通知,管理员可以查看所有通知
|
|
||||||
"""
|
|
||||||
notification = await notification_service.get_notification(db, notification_id)
|
|
||||||
if not notification:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="通知不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查权限
|
|
||||||
if not current_user.is_superuser and notification["recipient_id"] != current_user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="无权查看此通知"
|
|
||||||
)
|
|
||||||
|
|
||||||
return notification
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Dict[str, Any], status_code=status.HTTP_201_CREATED)
|
|
||||||
async def create_notification(
|
|
||||||
obj_in: NotificationCreate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建消息通知
|
|
||||||
|
|
||||||
- **recipient_id**: 接收人ID
|
|
||||||
- **title**: 通知标题
|
|
||||||
- **content**: 通知内容
|
|
||||||
- **notification_type**: 通知类型
|
|
||||||
- **priority**: 优先级(low/normal/high/urgent)
|
|
||||||
- **related_entity_type**: 关联实体类型
|
|
||||||
- **related_entity_id**: 关联实体ID
|
|
||||||
- **action_url**: 操作链接
|
|
||||||
- **extra_data**: 额外数据
|
|
||||||
- **send_email**: 是否发送邮件
|
|
||||||
- **send_sms**: 是否发送短信
|
|
||||||
- **expire_at**: 过期时间
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await notification_service.create_notification(db, obj_in=obj_in)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/batch", response_model=Dict[str, Any])
|
|
||||||
async def batch_create_notifications(
|
|
||||||
batch_in: NotificationBatchCreate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
批量创建消息通知
|
|
||||||
|
|
||||||
- **recipient_ids**: 接收人ID列表
|
|
||||||
- **title**: 通知标题
|
|
||||||
- **content**: 通知内容
|
|
||||||
- **notification_type**: 通知类型
|
|
||||||
- **priority**: 优先级
|
|
||||||
- **action_url**: 操作链接
|
|
||||||
- **extra_data**: 额外数据
|
|
||||||
"""
|
|
||||||
return await notification_service.batch_create_notifications(db, batch_in=batch_in)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/from-template", response_model=Dict[str, Any])
|
|
||||||
async def send_from_template(
|
|
||||||
template_in: NotificationSendFromTemplate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
从模板发送通知
|
|
||||||
|
|
||||||
- **template_code**: 模板编码
|
|
||||||
- **recipient_ids**: 接收人ID列表
|
|
||||||
- **variables**: 模板变量
|
|
||||||
- **related_entity_type**: 关联实体类型
|
|
||||||
- **related_entity_id**: 关联实体ID
|
|
||||||
- **action_url**: 操作链接
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await notification_service.send_from_template(db, template_in=template_in)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{notification_id}/read", response_model=Dict[str, Any])
|
|
||||||
async def mark_notification_as_read(
|
|
||||||
notification_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
标记通知为已读
|
|
||||||
|
|
||||||
- **notification_id**: 通知ID
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
notification = await notification_service.get_notification(db, notification_id)
|
|
||||||
if not notification:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="通知不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查权限
|
|
||||||
if not current_user.is_superuser and notification["recipient_id"] != current_user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="无权操作此通知"
|
|
||||||
)
|
|
||||||
|
|
||||||
return await notification_service.mark_as_read(db, notification_id)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/read-all", response_model=Dict[str, Any])
|
|
||||||
async def mark_all_as_read(
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
标记所有未读通知为已读
|
|
||||||
|
|
||||||
将当前用户的所有未读通知标记为已读
|
|
||||||
"""
|
|
||||||
return await notification_service.mark_all_as_read(db, current_user.id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{notification_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
async def delete_notification(
|
|
||||||
notification_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除消息通知
|
|
||||||
|
|
||||||
- **notification_id**: 通知ID
|
|
||||||
"""
|
|
||||||
notification = await notification_service.get_notification(db, notification_id)
|
|
||||||
if not notification:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="通知不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查权限
|
|
||||||
if not current_user.is_superuser and notification["recipient_id"] != current_user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="无权删除此通知"
|
|
||||||
)
|
|
||||||
|
|
||||||
await notification_service.delete_notification(db, notification_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/batch-delete", response_model=Dict[str, Any])
|
|
||||||
async def batch_delete_notifications(
|
|
||||||
notification_ids: List[int],
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
批量删除消息通知
|
|
||||||
|
|
||||||
- **notification_ids**: 通知ID列表
|
|
||||||
"""
|
|
||||||
# 检查权限
|
|
||||||
if not current_user.is_superuser:
|
|
||||||
# 普通用户只能删除自己的通知
|
|
||||||
notifications = await notification_service.get_notifications(
|
|
||||||
db,
|
|
||||||
skip=0,
|
|
||||||
limit=len(notification_ids) * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
valid_ids = [
|
|
||||||
n["id"] for n in notifications["items"]
|
|
||||||
if n["recipient_id"] == current_user.id and n["id"] in notification_ids
|
|
||||||
]
|
|
||||||
|
|
||||||
if not valid_ids:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="没有有效的通知ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
notification_ids = valid_ids
|
|
||||||
|
|
||||||
return await notification_service.batch_delete_notifications(db, notification_ids)
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
"""
|
|
||||||
操作日志管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.operation_log import (
|
|
||||||
OperationLogCreate,
|
|
||||||
OperationLogResponse,
|
|
||||||
OperationLogQueryParams,
|
|
||||||
OperationLogStatistics,
|
|
||||||
OperationLogExport
|
|
||||||
)
|
|
||||||
from app.services.operation_log_service import operation_log_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=Dict[str, Any])
|
|
||||||
async def get_operation_logs(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
operator_id: Optional[int] = Query(None, description="操作人ID"),
|
|
||||||
operator_name: Optional[str] = Query(None, description="操作人姓名"),
|
|
||||||
module: Optional[str] = Query(None, description="模块名称"),
|
|
||||||
operation_type: Optional[str] = Query(None, description="操作类型"),
|
|
||||||
result: Optional[str] = Query(None, description="操作结果"),
|
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
|
||||||
keyword: Optional[str] = Query(None, description="关键词"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取操作日志列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **operator_id**: 操作人ID筛选
|
|
||||||
- **operator_name**: 操作人姓名筛选
|
|
||||||
- **module**: 模块名称筛选
|
|
||||||
- **operation_type**: 操作类型筛选
|
|
||||||
- **result**: 操作结果筛选
|
|
||||||
- **start_time**: 开始时间筛选
|
|
||||||
- **end_time**: 结束时间筛选
|
|
||||||
- **keyword**: 关键词搜索
|
|
||||||
"""
|
|
||||||
return await operation_log_service.get_logs(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
operator_id=operator_id,
|
|
||||||
operator_name=operator_name,
|
|
||||||
module=module,
|
|
||||||
operation_type=operation_type,
|
|
||||||
result=result,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=Dict[str, Any])
|
|
||||||
async def get_operation_statistics(
|
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取操作日志统计信息
|
|
||||||
|
|
||||||
- **start_time**: 开始时间
|
|
||||||
- **end_time**: 结束时间
|
|
||||||
|
|
||||||
返回操作总数、成功数、失败数、今日操作数、模块分布、操作类型分布等统计信息
|
|
||||||
"""
|
|
||||||
return await operation_log_service.get_statistics(
|
|
||||||
db,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/top-operators", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_top_operators(
|
|
||||||
limit: int = Query(10, ge=1, le=50, description="返回条数"),
|
|
||||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
|
||||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取操作排行榜
|
|
||||||
|
|
||||||
- **limit**: 返回条数
|
|
||||||
- **start_time**: 开始时间
|
|
||||||
- **end_time**: 结束时间
|
|
||||||
|
|
||||||
返回操作次数最多的用户列表
|
|
||||||
"""
|
|
||||||
return await operation_log_service.get_operator_top(
|
|
||||||
db,
|
|
||||||
limit=limit,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{log_id}", response_model=Dict[str, Any])
|
|
||||||
async def get_operation_log(
|
|
||||||
log_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取操作日志详情
|
|
||||||
|
|
||||||
- **log_id**: 日志ID
|
|
||||||
"""
|
|
||||||
log = await operation_log_service.get_log(db, log_id)
|
|
||||||
if not log:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="操作日志不存在"
|
|
||||||
)
|
|
||||||
return log
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Dict[str, Any], status_code=status.HTTP_201_CREATED)
|
|
||||||
async def create_operation_log(
|
|
||||||
obj_in: OperationLogCreate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建操作日志(通常由系统自动记录)
|
|
||||||
|
|
||||||
- **operator_id**: 操作人ID
|
|
||||||
- **operator_name**: 操作人姓名
|
|
||||||
- **operator_ip**: 操作人IP
|
|
||||||
- **module**: 模块名称
|
|
||||||
- **operation_type**: 操作类型
|
|
||||||
- **method**: 请求方法
|
|
||||||
- **url**: 请求URL
|
|
||||||
- **params**: 请求参数
|
|
||||||
- **result**: 操作结果
|
|
||||||
- **error_msg**: 错误信息
|
|
||||||
- **duration**: 执行时长(毫秒)
|
|
||||||
- **user_agent**: 用户代理
|
|
||||||
- **extra_data**: 额外数据
|
|
||||||
"""
|
|
||||||
return await operation_log_service.create_log(db, obj_in=obj_in)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/export", response_model=List[Dict[str, Any]])
|
|
||||||
async def export_operation_logs(
|
|
||||||
export_config: OperationLogExport,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
导出操作日志
|
|
||||||
|
|
||||||
- **start_time**: 开始时间
|
|
||||||
- **end_time**: 结束时间
|
|
||||||
- **operator_id**: 操作人ID
|
|
||||||
- **module**: 模块名称
|
|
||||||
- **operation_type**: 操作类型
|
|
||||||
|
|
||||||
返回可导出的日志列表
|
|
||||||
"""
|
|
||||||
return await operation_log_service.export_logs(
|
|
||||||
db,
|
|
||||||
start_time=export_config.start_time,
|
|
||||||
end_time=export_config.end_time,
|
|
||||||
operator_id=export_config.operator_id,
|
|
||||||
module=export_config.module,
|
|
||||||
operation_type=export_config.operation_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/old-logs", response_model=Dict[str, Any])
|
|
||||||
async def delete_old_logs(
|
|
||||||
days: int = Query(90, ge=1, le=365, description="保留天数"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除旧操作日志
|
|
||||||
|
|
||||||
- **days**: 保留天数(默认90天)
|
|
||||||
|
|
||||||
删除指定天数之前的操作日志
|
|
||||||
"""
|
|
||||||
# 检查权限
|
|
||||||
if not current_user.is_superuser:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="只有超级管理员可以删除日志"
|
|
||||||
)
|
|
||||||
|
|
||||||
return await operation_log_service.delete_old_logs(db, days=days)
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
"""
|
|
||||||
机构网点API路由
|
|
||||||
"""
|
|
||||||
from typing import List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.organization import (
|
|
||||||
OrganizationCreate,
|
|
||||||
OrganizationUpdate,
|
|
||||||
OrganizationResponse,
|
|
||||||
OrganizationTreeNode,
|
|
||||||
OrganizationWithParent
|
|
||||||
)
|
|
||||||
from app.services.organization_service import organization_service
|
|
||||||
from app.utils.redis_client import redis_client
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
# 异步缓存包装器
|
|
||||||
@redis_client.cached_async("organizations:list", expire=1800) # 缓存30分钟
|
|
||||||
async def _cached_get_organizations(
|
|
||||||
skip: int,
|
|
||||||
limit: int,
|
|
||||||
org_type: Optional[str],
|
|
||||||
status: Optional[str],
|
|
||||||
keyword: Optional[str],
|
|
||||||
db: Session
|
|
||||||
):
|
|
||||||
"""获取机构列表的缓存包装器"""
|
|
||||||
items, total = organization_service.get_organizations(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
org_type=org_type,
|
|
||||||
status=status,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@redis_client.cached_async("organizations:tree", expire=1800) # 缓存30分钟
|
|
||||||
async def _cached_get_organization_tree(
|
|
||||||
status: Optional[str],
|
|
||||||
db: Session
|
|
||||||
):
|
|
||||||
"""获取机构树的缓存包装器"""
|
|
||||||
return organization_service.get_organization_tree(db, status)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[OrganizationResponse])
|
|
||||||
async def get_organizations(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
org_type: Optional[str] = Query(None, description="机构类型"),
|
|
||||||
status: Optional[str] = Query(None, description="状态"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取机构列表(已启用缓存,30分钟)
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **org_type**: 机构类型筛选(province/city/outlet)
|
|
||||||
- **status**: 状态筛选(active/inactive)
|
|
||||||
- **keyword**: 搜索关键词(代码或名称)
|
|
||||||
"""
|
|
||||||
return await _cached_get_organizations(
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
org_type=org_type,
|
|
||||||
status=status,
|
|
||||||
keyword=keyword,
|
|
||||||
db=db
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tree", response_model=List[OrganizationTreeNode])
|
|
||||||
async def get_organization_tree(
|
|
||||||
status: Optional[str] = Query(None, description="状态筛选"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取机构树(已启用缓存,30分钟)
|
|
||||||
|
|
||||||
- **status**: 状态筛选(active/inactive)
|
|
||||||
|
|
||||||
返回树形结构的机构列表
|
|
||||||
"""
|
|
||||||
return await _cached_get_organization_tree(status, db)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}", response_model=OrganizationWithParent)
|
|
||||||
def get_organization(
|
|
||||||
org_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取机构详情
|
|
||||||
|
|
||||||
- **org_id**: 机构ID
|
|
||||||
|
|
||||||
返回机构详情及其父机构信息
|
|
||||||
"""
|
|
||||||
org = organization_service.get_organization(db, org_id)
|
|
||||||
|
|
||||||
# 加载父机构信息
|
|
||||||
if org.parent_id:
|
|
||||||
from app.crud.organization import organization as organization_crud
|
|
||||||
parent = organization_crud.get(db, org.parent_id)
|
|
||||||
org.parent = parent
|
|
||||||
|
|
||||||
return org
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}/children", response_model=List[OrganizationResponse])
|
|
||||||
def get_organization_children(
|
|
||||||
org_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取直接子机构
|
|
||||||
|
|
||||||
- **org_id**: 父机构ID(0表示根节点)
|
|
||||||
|
|
||||||
返回指定机构的直接子机构列表
|
|
||||||
"""
|
|
||||||
return organization_service.get_organization_children(db, org_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}/all-children", response_model=List[OrganizationResponse])
|
|
||||||
def get_all_organization_children(
|
|
||||||
org_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
递归获取所有子机构
|
|
||||||
|
|
||||||
- **org_id**: 父机构ID
|
|
||||||
|
|
||||||
返回指定机构的所有子机构(包括子节点的子节点)
|
|
||||||
"""
|
|
||||||
return organization_service.get_all_children(db, org_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}/parents", response_model=List[OrganizationResponse])
|
|
||||||
def get_organization_parents(
|
|
||||||
org_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
递归获取所有父机构
|
|
||||||
|
|
||||||
- **org_id**: 子机构ID
|
|
||||||
|
|
||||||
返回从根到直接父节点的所有父机构列表
|
|
||||||
"""
|
|
||||||
return organization_service.get_parents(db, org_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=OrganizationResponse, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_organization(
|
|
||||||
obj_in: OrganizationCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建机构
|
|
||||||
|
|
||||||
- **org_code**: 机构代码(唯一)
|
|
||||||
- **org_name**: 机构名称
|
|
||||||
- **org_type**: 机构类型(province/city/outlet)
|
|
||||||
- **parent_id**: 父机构ID(可选)
|
|
||||||
- **address**: 地址
|
|
||||||
- **contact_person**: 联系人
|
|
||||||
- **contact_phone**: 联系电话
|
|
||||||
- **sort_order**: 排序
|
|
||||||
"""
|
|
||||||
return organization_service.create_organization(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
creator_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{org_id}", response_model=OrganizationResponse)
|
|
||||||
def update_organization(
|
|
||||||
org_id: int,
|
|
||||||
obj_in: OrganizationUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新机构
|
|
||||||
|
|
||||||
- **org_id**: 机构ID
|
|
||||||
- **org_name**: 机构名称
|
|
||||||
- **org_type**: 机构类型
|
|
||||||
- **parent_id**: 父机构ID
|
|
||||||
- **address**: 地址
|
|
||||||
- **contact_person**: 联系人
|
|
||||||
- **contact_phone**: 联系电话
|
|
||||||
- **status**: 状态
|
|
||||||
- **sort_order**: 排序
|
|
||||||
"""
|
|
||||||
return organization_service.update_organization(
|
|
||||||
db=db,
|
|
||||||
org_id=org_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{org_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_organization(
|
|
||||||
org_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除机构
|
|
||||||
|
|
||||||
- **org_id**: 机构ID
|
|
||||||
|
|
||||||
软删除机构(如果机构下存在子机构则无法删除)
|
|
||||||
"""
|
|
||||||
organization_service.delete_organization(
|
|
||||||
db=db,
|
|
||||||
org_id=org_id,
|
|
||||||
deleter_id=current_user.id
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
"""
|
|
||||||
资产回收管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.recovery import (
|
|
||||||
AssetRecoveryOrderCreate,
|
|
||||||
AssetRecoveryOrderUpdate,
|
|
||||||
AssetRecoveryOrderWithRelations,
|
|
||||||
AssetRecoveryOrderQueryParams,
|
|
||||||
AssetRecoveryStatistics
|
|
||||||
)
|
|
||||||
from app.services.recovery_service import recovery_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list)
|
|
||||||
def get_recovery_orders(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
recovery_type: Optional[str] = Query(None, description="回收类型"),
|
|
||||||
approval_status: Optional[str] = Query(None, description="审批状态"),
|
|
||||||
execute_status: Optional[str] = Query(None, description="执行状态"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取回收单列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **recovery_type**: 回收类型(user=使用人回收/org=机构回收/scrap=报废回收)
|
|
||||||
- **approval_status**: 审批状态(pending/approved/rejected/cancelled)
|
|
||||||
- **execute_status**: 执行状态(pending/executing/completed/cancelled)
|
|
||||||
- **keyword**: 搜索关键词(单号/标题)
|
|
||||||
"""
|
|
||||||
items, total = recovery_service.get_orders(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
recovery_type=recovery_type,
|
|
||||||
approval_status=approval_status,
|
|
||||||
execute_status=execute_status,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=AssetRecoveryStatistics)
|
|
||||||
def get_recovery_statistics(
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取回收单统计信息
|
|
||||||
|
|
||||||
返回回收单总数、待审批数、已审批数等统计信息
|
|
||||||
"""
|
|
||||||
return recovery_service.get_statistics(db)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}", response_model=dict)
|
|
||||||
def get_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取回收单详情
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
|
|
||||||
返回回收单详情及其关联信息(包含明细列表)
|
|
||||||
"""
|
|
||||||
return recovery_service.get_order(db, order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}/items", response_model=list)
|
|
||||||
def get_recovery_order_items(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取回收单明细列表
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
|
|
||||||
返回该回收单的所有资产明细
|
|
||||||
"""
|
|
||||||
return recovery_service.get_order_items(db, order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=dict, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_recovery_order(
|
|
||||||
obj_in: AssetRecoveryOrderCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建回收单
|
|
||||||
|
|
||||||
- **recovery_type**: 回收类型
|
|
||||||
- user: 使用人回收(从使用人处回收)
|
|
||||||
- org: 机构回收(从机构回收)
|
|
||||||
- scrap: 报废回收(报废资产回收)
|
|
||||||
- **title**: 标题
|
|
||||||
- **asset_ids**: 资产ID列表
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
创建后状态为待审批,需要审批后才能执行
|
|
||||||
"""
|
|
||||||
return recovery_service.create_order(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
apply_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{order_id}", response_model=dict)
|
|
||||||
def update_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
obj_in: AssetRecoveryOrderUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新回收单
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
- **title**: 标题
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
只有待审批状态的回收单可以更新
|
|
||||||
"""
|
|
||||||
return recovery_service.update_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
obj_in=obj_in
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/approve", response_model=dict)
|
|
||||||
def approve_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
approval_status: str = Query(..., description="审批状态(approved/rejected)"),
|
|
||||||
approval_remark: Optional[str] = Query(None, description="审批备注"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
审批回收单
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
- **approval_status**: 审批状态(approved/rejected)
|
|
||||||
- **approval_remark**: 审批备注
|
|
||||||
|
|
||||||
审批通过后可以开始执行回收
|
|
||||||
"""
|
|
||||||
return recovery_service.approve_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
approval_status=approval_status,
|
|
||||||
approval_user_id=current_user.id,
|
|
||||||
approval_remark=approval_remark
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/start", response_model=dict)
|
|
||||||
def start_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
开始回收
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
|
|
||||||
开始执行已审批通过的回收单
|
|
||||||
"""
|
|
||||||
return recovery_service.start_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
execute_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/complete", response_model=dict)
|
|
||||||
def complete_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
完成回收
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
|
|
||||||
完成回收单,自动更新资产状态为库存中或报废
|
|
||||||
"""
|
|
||||||
return recovery_service.complete_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
execute_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/cancel", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def cancel_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
取消回收单
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
|
|
||||||
取消回收单(已完成的无法取消)
|
|
||||||
"""
|
|
||||||
recovery_service.cancel_order(db, order_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_recovery_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除回收单
|
|
||||||
|
|
||||||
- **order_id**: 回收单ID
|
|
||||||
|
|
||||||
只能删除已拒绝或已取消的回收单
|
|
||||||
"""
|
|
||||||
recovery_service.delete_order(db, order_id)
|
|
||||||
return None
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
"""
|
|
||||||
统计分析API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
from datetime import date
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.services.statistics_service import statistics_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/overview", response_model=Dict[str, Any])
|
|
||||||
async def get_statistics_overview(
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取总览统计
|
|
||||||
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回资产总数、总价值、各状态数量、采购统计、网点数等概览信息
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_overview(db, organization_id=organization_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/assets/purchase", response_model=Dict[str, Any])
|
|
||||||
async def get_purchase_statistics(
|
|
||||||
start_date: Optional[date] = Query(None, description="开始日期"),
|
|
||||||
end_date: Optional[date] = Query(None, description="结束日期"),
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取采购统计
|
|
||||||
|
|
||||||
- **start_date**: 开始日期
|
|
||||||
- **end_date**: 结束日期
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回采购数量、采购金额、月度趋势、供应商分布等统计信息
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_purchase_statistics(
|
|
||||||
db,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
organization_id=organization_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/assets/depreciation", response_model=Dict[str, Any])
|
|
||||||
async def get_depreciation_statistics(
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取折旧统计
|
|
||||||
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回折旧金额、折旧率、分类折旧等统计信息
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_depreciation_statistics(db, organization_id=organization_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/assets/value", response_model=Dict[str, Any])
|
|
||||||
async def get_value_statistics(
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取价值统计
|
|
||||||
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回资产总价值、净值、折旧、分类价值、网点价值、高价值资产等统计信息
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_value_statistics(db, organization_id=organization_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/assets/trend", response_model=Dict[str, Any])
|
|
||||||
async def get_trend_analysis(
|
|
||||||
start_date: Optional[date] = Query(None, description="开始日期"),
|
|
||||||
end_date: Optional[date] = Query(None, description="结束日期"),
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取趋势分析
|
|
||||||
|
|
||||||
- **start_date**: 开始日期
|
|
||||||
- **end_date**: 结束日期
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回资产数量趋势、价值趋势、采购趋势、维修趋势、调拨趋势等分析数据
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_trend_analysis(
|
|
||||||
db,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
organization_id=organization_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/maintenance/summary", response_model=Dict[str, Any])
|
|
||||||
async def get_maintenance_summary(
|
|
||||||
start_date: Optional[date] = Query(None, description="开始日期"),
|
|
||||||
end_date: Optional[date] = Query(None, description="结束日期"),
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取维修汇总
|
|
||||||
|
|
||||||
- **start_date**: 开始日期
|
|
||||||
- **end_date**: 结束日期
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回维修次数、维修费用、状态分布等统计信息
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_maintenance_statistics(
|
|
||||||
db,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
organization_id=organization_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/allocation/summary", response_model=Dict[str, Any])
|
|
||||||
async def get_allocation_summary(
|
|
||||||
start_date: Optional[date] = Query(None, description="开始日期"),
|
|
||||||
end_date: Optional[date] = Query(None, description="结束日期"),
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID筛选"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取分配汇总
|
|
||||||
|
|
||||||
- **start_date**: 开始日期
|
|
||||||
- **end_date**: 结束日期
|
|
||||||
- **organization_id**: 网点ID筛选
|
|
||||||
|
|
||||||
返回分配次数、状态分布、网点分配统计等信息
|
|
||||||
"""
|
|
||||||
return await statistics_service.get_allocation_statistics(
|
|
||||||
db,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
organization_id=organization_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/export")
|
|
||||||
async def export_statistics(
|
|
||||||
report_type: str = Query(..., description="报表类型"),
|
|
||||||
start_date: Optional[date] = Query(None, description="开始日期"),
|
|
||||||
end_date: Optional[date] = Query(None, description="结束日期"),
|
|
||||||
organization_id: Optional[int] = Query(None, description="网点ID"),
|
|
||||||
format: str = Query("xlsx", description="导出格式"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
导出统计报表
|
|
||||||
|
|
||||||
- **report_type**: 报表类型(overview/purchase/depreciation/value/trend/maintenance/allocation)
|
|
||||||
- **start_date**: 开始日期
|
|
||||||
- **end_date**: 结束日期
|
|
||||||
- **organization_id**: 网点ID
|
|
||||||
- **format**: 导出格式(xlsx/csv/pdf)
|
|
||||||
|
|
||||||
返回导出文件信息
|
|
||||||
"""
|
|
||||||
# 根据报表类型获取数据
|
|
||||||
if report_type == "overview":
|
|
||||||
data = await statistics_service.get_overview(db, organization_id)
|
|
||||||
elif report_type == "purchase":
|
|
||||||
data = await statistics_service.get_purchase_statistics(db, start_date, end_date, organization_id)
|
|
||||||
elif report_type == "depreciation":
|
|
||||||
data = await statistics_service.get_depreciation_statistics(db, organization_id)
|
|
||||||
elif report_type == "value":
|
|
||||||
data = await statistics_service.get_value_statistics(db, organization_id)
|
|
||||||
elif report_type == "trend":
|
|
||||||
data = await statistics_service.get_trend_analysis(db, start_date, end_date, organization_id)
|
|
||||||
elif report_type == "maintenance":
|
|
||||||
data = await statistics_service.get_maintenance_statistics(db, start_date, end_date, organization_id)
|
|
||||||
elif report_type == "allocation":
|
|
||||||
data = await statistics_service.get_allocation_statistics(db, start_date, end_date, organization_id)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"不支持的报表类型: {report_type}")
|
|
||||||
|
|
||||||
# TODO: 实现导出逻辑
|
|
||||||
# 1. 生成Excel/CSV/PDF文件
|
|
||||||
# 2. 保存到文件系统
|
|
||||||
# 3. 返回文件URL
|
|
||||||
|
|
||||||
return {
|
|
||||||
"message": "导出功能待实现",
|
|
||||||
"data": data,
|
|
||||||
"report_type": report_type,
|
|
||||||
"format": format
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
"""
|
|
||||||
系统配置管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.system_config import (
|
|
||||||
SystemConfigCreate,
|
|
||||||
SystemConfigUpdate,
|
|
||||||
SystemConfigResponse,
|
|
||||||
SystemConfigBatchUpdate,
|
|
||||||
SystemConfigQueryParams,
|
|
||||||
ConfigCategoryResponse
|
|
||||||
)
|
|
||||||
from app.services.system_config_service import system_config_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=Dict[str, Any])
|
|
||||||
async def get_configs(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
category: Optional[str] = Query(None, description="配置分类"),
|
|
||||||
is_active: Optional[bool] = Query(None, description="是否启用"),
|
|
||||||
is_system: Optional[bool] = Query(None, description="是否系统配置"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取系统配置列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **keyword**: 搜索关键词(配置键/配置名称/描述)
|
|
||||||
- **category**: 配置分类筛选
|
|
||||||
- **is_active**: 是否启用筛选
|
|
||||||
- **is_system**: 是否系统配置筛选
|
|
||||||
"""
|
|
||||||
return await system_config_service.get_configs(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
keyword=keyword,
|
|
||||||
category=category,
|
|
||||||
is_active=is_active,
|
|
||||||
is_system=is_system
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/categories", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_config_categories(
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取所有配置分类
|
|
||||||
|
|
||||||
返回配置分类及每个分类的配置数量
|
|
||||||
"""
|
|
||||||
return await system_config_service.get_categories(db)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/category/{category}", response_model=List[Dict[str, Any]])
|
|
||||||
async def get_configs_by_category(
|
|
||||||
category: str,
|
|
||||||
is_active: bool = Query(True, description="是否启用"),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
根据分类获取配置
|
|
||||||
|
|
||||||
- **category**: 配置分类
|
|
||||||
- **is_active**: 是否启用
|
|
||||||
"""
|
|
||||||
return await system_config_service.get_configs_by_category(
|
|
||||||
db,
|
|
||||||
category=category,
|
|
||||||
is_active=is_active
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/key/{config_key}", response_model=Any)
|
|
||||||
async def get_config_by_key(
|
|
||||||
config_key: str,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
根据配置键获取配置值
|
|
||||||
|
|
||||||
- **config_key**: 配置键
|
|
||||||
|
|
||||||
返回配置的实际值(已根据类型转换)
|
|
||||||
"""
|
|
||||||
value = await system_config_service.get_config_by_key(db, config_key)
|
|
||||||
if value is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"配置键 {config_key} 不存在或未启用"
|
|
||||||
)
|
|
||||||
return {"config_key": config_key, "value": value}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{config_id}", response_model=Dict[str, Any])
|
|
||||||
async def get_config(
|
|
||||||
config_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取配置详情
|
|
||||||
|
|
||||||
- **config_id**: 配置ID
|
|
||||||
"""
|
|
||||||
config = await system_config_service.get_config(db, config_id)
|
|
||||||
if not config:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="配置不存在"
|
|
||||||
)
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Dict[str, Any], status_code=status.HTTP_201_CREATED)
|
|
||||||
async def create_config(
|
|
||||||
obj_in: SystemConfigCreate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建系统配置
|
|
||||||
|
|
||||||
- **config_key**: 配置键(唯一)
|
|
||||||
- **config_name**: 配置名称
|
|
||||||
- **config_value**: 配置值
|
|
||||||
- **value_type**: 值类型(string/number/boolean/json)
|
|
||||||
- **category**: 配置分类
|
|
||||||
- **description**: 配置描述
|
|
||||||
- **is_system**: 是否系统配置(系统配置不允许删除和修改部分字段)
|
|
||||||
- **is_encrypted**: 是否加密存储
|
|
||||||
- **options**: 可选值配置
|
|
||||||
- **default_value**: 默认值
|
|
||||||
- **sort_order**: 排序序号
|
|
||||||
- **is_active**: 是否启用
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await system_config_service.create_config(
|
|
||||||
db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
creator_id=current_user.id
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{config_id}", response_model=Dict[str, Any])
|
|
||||||
async def update_config(
|
|
||||||
config_id: int,
|
|
||||||
obj_in: SystemConfigUpdate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新系统配置
|
|
||||||
|
|
||||||
- **config_id**: 配置ID
|
|
||||||
- **config_name**: 配置名称
|
|
||||||
- **config_value**: 配置值
|
|
||||||
- **description**: 配置描述
|
|
||||||
- **options**: 可选值配置
|
|
||||||
- **default_value**: 默认值
|
|
||||||
- **sort_order**: 排序序号
|
|
||||||
- **is_active**: 是否启用
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await system_config_service.update_config(
|
|
||||||
db,
|
|
||||||
config_id=config_id,
|
|
||||||
obj_in=obj_in,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/batch", response_model=Dict[str, Any])
|
|
||||||
async def batch_update_configs(
|
|
||||||
batch_update: SystemConfigBatchUpdate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
批量更新配置
|
|
||||||
|
|
||||||
- **configs**: 配置键值对字典
|
|
||||||
|
|
||||||
示例:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"configs": {
|
|
||||||
"system.title": "资产管理系统",
|
|
||||||
"system.max_upload_size": 10485760
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
return await system_config_service.batch_update_configs(
|
|
||||||
db,
|
|
||||||
configs=batch_update.configs,
|
|
||||||
updater_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
async def delete_config(
|
|
||||||
config_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除系统配置
|
|
||||||
|
|
||||||
- **config_id**: 配置ID
|
|
||||||
|
|
||||||
注意:系统配置不允许删除
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await system_config_service.delete_config(db, config_id)
|
|
||||||
except ValueError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
"""
|
|
||||||
资产调拨管理API路由
|
|
||||||
"""
|
|
||||||
from typing import Optional
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.core.deps import get_db, get_current_user
|
|
||||||
from app.schemas.transfer import (
|
|
||||||
AssetTransferOrderCreate,
|
|
||||||
AssetTransferOrderUpdate,
|
|
||||||
AssetTransferOrderWithRelations,
|
|
||||||
AssetTransferOrderQueryParams,
|
|
||||||
AssetTransferStatistics
|
|
||||||
)
|
|
||||||
from app.services.transfer_service import transfer_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list)
|
|
||||||
def get_transfer_orders(
|
|
||||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
|
||||||
transfer_type: Optional[str] = Query(None, description="调拨类型"),
|
|
||||||
approval_status: Optional[str] = Query(None, description="审批状态"),
|
|
||||||
execute_status: Optional[str] = Query(None, description="执行状态"),
|
|
||||||
source_org_id: Optional[int] = Query(None, description="调出网点ID"),
|
|
||||||
target_org_id: Optional[int] = Query(None, description="调入网点ID"),
|
|
||||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取调拨单列表
|
|
||||||
|
|
||||||
- **skip**: 跳过条数
|
|
||||||
- **limit**: 返回条数(最大100)
|
|
||||||
- **transfer_type**: 调拨类型(internal=内部调拨/external=跨机构调拨)
|
|
||||||
- **approval_status**: 审批状态(pending/approved/rejected/cancelled)
|
|
||||||
- **execute_status**: 执行状态(pending/executing/completed/cancelled)
|
|
||||||
- **source_org_id**: 调出网点ID
|
|
||||||
- **target_org_id**: 调入网点ID
|
|
||||||
- **keyword**: 搜索关键词(单号/标题)
|
|
||||||
"""
|
|
||||||
items, total = transfer_service.get_orders(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
transfer_type=transfer_type,
|
|
||||||
approval_status=approval_status,
|
|
||||||
execute_status=execute_status,
|
|
||||||
source_org_id=source_org_id,
|
|
||||||
target_org_id=target_org_id,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/statistics", response_model=AssetTransferStatistics)
|
|
||||||
def get_transfer_statistics(
|
|
||||||
source_org_id: Optional[int] = Query(None, description="调出网点ID"),
|
|
||||||
target_org_id: Optional[int] = Query(None, description="调入网点ID"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取调拨单统计信息
|
|
||||||
|
|
||||||
- **source_org_id**: 调出网点ID(可选)
|
|
||||||
- **target_org_id**: 调入网点ID(可选)
|
|
||||||
|
|
||||||
返回调拨单总数、待审批数、已审批数等统计信息
|
|
||||||
"""
|
|
||||||
return transfer_service.get_statistics(db, source_org_id, target_org_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}", response_model=dict)
|
|
||||||
def get_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取调拨单详情
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
|
|
||||||
返回调拨单详情及其关联信息(包含明细列表)
|
|
||||||
"""
|
|
||||||
return transfer_service.get_order(db, order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{order_id}/items", response_model=list)
|
|
||||||
def get_transfer_order_items(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取调拨单明细列表
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
|
|
||||||
返回该调拨单的所有资产明细
|
|
||||||
"""
|
|
||||||
return transfer_service.get_order_items(db, order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=dict, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_transfer_order(
|
|
||||||
obj_in: AssetTransferOrderCreate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建调拨单
|
|
||||||
|
|
||||||
- **source_org_id**: 调出网点ID
|
|
||||||
- **target_org_id**: 调入网点ID
|
|
||||||
- **transfer_type**: 调拨类型(internal=内部调拨/external=跨机构调拨)
|
|
||||||
- **title**: 标题
|
|
||||||
- **asset_ids**: 资产ID列表
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
创建后状态为待审批,需要审批后才能执行
|
|
||||||
"""
|
|
||||||
return transfer_service.create_order(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
apply_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{order_id}", response_model=dict)
|
|
||||||
def update_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
obj_in: AssetTransferOrderUpdate,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新调拨单
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
- **title**: 标题
|
|
||||||
- **remark**: 备注
|
|
||||||
|
|
||||||
只有待审批状态的调拨单可以更新
|
|
||||||
"""
|
|
||||||
return transfer_service.update_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
obj_in=obj_in
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/approve", response_model=dict)
|
|
||||||
def approve_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
approval_status: str = Query(..., description="审批状态(approved/rejected)"),
|
|
||||||
approval_remark: Optional[str] = Query(None, description="审批备注"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
审批调拨单
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
- **approval_status**: 审批状态(approved/rejected)
|
|
||||||
- **approval_remark**: 审批备注
|
|
||||||
|
|
||||||
审批通过后可以开始执行调拨
|
|
||||||
"""
|
|
||||||
return transfer_service.approve_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
approval_status=approval_status,
|
|
||||||
approval_user_id=current_user.id,
|
|
||||||
approval_remark=approval_remark
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/start", response_model=dict)
|
|
||||||
def start_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
开始调拨
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
|
|
||||||
开始执行已审批通过的调拨单
|
|
||||||
"""
|
|
||||||
return transfer_service.start_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
execute_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/complete", response_model=dict)
|
|
||||||
def complete_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
完成调拨
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
|
|
||||||
完成调拨单,自动更新资产机构和状态
|
|
||||||
"""
|
|
||||||
return transfer_service.complete_order(
|
|
||||||
db=db,
|
|
||||||
order_id=order_id,
|
|
||||||
execute_user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{order_id}/cancel", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def cancel_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
取消调拨单
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
|
|
||||||
取消调拨单(已完成的无法取消)
|
|
||||||
"""
|
|
||||||
transfer_service.cancel_order(db, order_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_transfer_order(
|
|
||||||
order_id: int,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除调拨单
|
|
||||||
|
|
||||||
- **order_id**: 调拨单ID
|
|
||||||
|
|
||||||
只能删除已拒绝或已取消的调拨单
|
|
||||||
"""
|
|
||||||
transfer_service.delete_order(db, order_id)
|
|
||||||
return None
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
核心模块初始化
|
|
||||||
"""
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
__all__ = ["settings"]
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""
|
|
||||||
应用配置模块
|
|
||||||
"""
|
|
||||||
from typing import List, Optional
|
|
||||||
from pydantic import Field, field_validator
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
"""应用配置类"""
|
|
||||||
|
|
||||||
# 应用基本信息
|
|
||||||
APP_NAME: str = Field(default="资产管理系统", description="应用名称")
|
|
||||||
APP_VERSION: str = Field(default="1.0.0", description="应用版本")
|
|
||||||
APP_ENVIRONMENT: str = Field(default="development", description="运行环境")
|
|
||||||
DEBUG: bool = Field(default=False, description="调试模式")
|
|
||||||
API_V1_PREFIX: str = Field(default="/api/v1", description="API V1 前缀")
|
|
||||||
|
|
||||||
# 服务器配置
|
|
||||||
HOST: str = Field(default="0.0.0.0", description="服务器地址")
|
|
||||||
PORT: int = Field(default=8000, description="服务器端口")
|
|
||||||
|
|
||||||
# 数据库配置
|
|
||||||
DATABASE_URL: str = Field(
|
|
||||||
default="postgresql+asyncpg://postgres:postgres@localhost:5432/asset_management",
|
|
||||||
description="数据库连接URL"
|
|
||||||
)
|
|
||||||
DATABASE_ECHO: bool = Field(default=False, description="是否打印SQL语句")
|
|
||||||
|
|
||||||
# Redis配置
|
|
||||||
REDIS_URL: str = Field(default="redis://localhost:6379/0", description="Redis连接URL")
|
|
||||||
REDIS_MAX_CONNECTIONS: int = Field(default=50, description="Redis最大连接数")
|
|
||||||
|
|
||||||
# JWT配置
|
|
||||||
SECRET_KEY: str = Field(default="your-secret-key-change-in-production", description="JWT密钥")
|
|
||||||
ALGORITHM: str = Field(default="HS256", description="JWT算法")
|
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=15, description="访问令牌过期时间(分钟)")
|
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, description="刷新令牌过期时间(天)")
|
|
||||||
|
|
||||||
# CORS配置
|
|
||||||
CORS_ORIGINS: List[str] = Field(
|
|
||||||
default=["http://localhost:5173", "http://localhost:3000"],
|
|
||||||
description="允许的跨域来源"
|
|
||||||
)
|
|
||||||
CORS_ALLOW_CREDENTIALS: bool = Field(default=True, description="允许携带凭证")
|
|
||||||
CORS_ALLOW_METHODS: List[str] = Field(default=["*"], description="允许的HTTP方法")
|
|
||||||
CORS_ALLOW_HEADERS: List[str] = Field(default=["*"], description="允许的请求头")
|
|
||||||
|
|
||||||
# 文件上传配置
|
|
||||||
UPLOAD_DIR: str = Field(default="uploads", description="上传文件目录")
|
|
||||||
MAX_UPLOAD_SIZE: int = Field(default=10485760, description="最大上传大小(字节)")
|
|
||||||
ALLOWED_EXTENSIONS: List[str] = Field(
|
|
||||||
default=["png", "jpg", "jpeg", "gif", "pdf", "xlsx", "xls"],
|
|
||||||
description="允许的文件扩展名"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证码配置
|
|
||||||
CAPTCHA_EXPIRE_SECONDS: int = Field(default=300, description="验证码过期时间(秒)")
|
|
||||||
CAPTCHA_LENGTH: int = Field(default=4, description="验证码长度")
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
LOG_LEVEL: str = Field(default="INFO", description="日志级别")
|
|
||||||
LOG_FILE: str = Field(default="logs/app.log", description="日志文件路径")
|
|
||||||
LOG_ROTATION: str = Field(default="500 MB", description="日志轮转大小")
|
|
||||||
LOG_RETENTION: str = Field(default="10 days", description="日志保留时间")
|
|
||||||
|
|
||||||
# 分页配置
|
|
||||||
DEFAULT_PAGE_SIZE: int = Field(default=20, description="默认每页数量")
|
|
||||||
MAX_PAGE_SIZE: int = Field(default=100, description="最大每页数量")
|
|
||||||
|
|
||||||
# 二维码配置
|
|
||||||
QR_CODE_DIR: str = Field(default="uploads/qrcodes", description="二维码保存目录")
|
|
||||||
QR_CODE_SIZE: int = Field(default=300, description="二维码尺寸")
|
|
||||||
QR_CODE_BORDER: int = Field(default=2, description="二维码边框")
|
|
||||||
|
|
||||||
@field_validator("CORS_ORIGINS", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def parse_cors_origins(cls, v: str) -> List[str]:
|
|
||||||
"""解析CORS来源"""
|
|
||||||
if isinstance(v, str):
|
|
||||||
return [origin.strip() for origin in v.split(",")]
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("ALLOWED_EXTENSIONS", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def parse_allowed_extensions(cls, v: str) -> List[str]:
|
|
||||||
"""解析允许的文件扩展名"""
|
|
||||||
if isinstance(v, str):
|
|
||||||
return [ext.strip() for ext in v.split(",")]
|
|
||||||
return v
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_development(self) -> bool:
|
|
||||||
"""是否为开发环境"""
|
|
||||||
return self.APP_ENVIRONMENT == "development"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_production(self) -> bool:
|
|
||||||
"""是否为生产环境"""
|
|
||||||
return self.APP_ENVIRONMENT == "production"
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
env_file_encoding = "utf-8"
|
|
||||||
case_sensitive = True
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局配置实例
|
|
||||||
settings = Settings()
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
"""
|
|
||||||
依赖注入模块
|
|
||||||
"""
|
|
||||||
from typing import Generator, Optional
|
|
||||||
from fastapi import Depends, HTTPException, status
|
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.db.session import async_session_maker
|
|
||||||
from app.core.security import security_manager
|
|
||||||
from app.models.user import User, Role, Permission, UserRole, RolePermission
|
|
||||||
|
|
||||||
# HTTP Bearer认证
|
|
||||||
security = HTTPBearer()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_db() -> Generator:
|
|
||||||
"""
|
|
||||||
获取数据库会话
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
AsyncSession: 数据库会话
|
|
||||||
"""
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
try:
|
|
||||||
yield session
|
|
||||||
await session.commit()
|
|
||||||
except Exception:
|
|
||||||
await session.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
await session.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
获取当前登录用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
credentials: HTTP认证凭据
|
|
||||||
db: 数据库会话
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 当前用户对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 认证失败或用户不存在
|
|
||||||
"""
|
|
||||||
from app.utils.redis_client import redis_client
|
|
||||||
|
|
||||||
token = credentials.credentials
|
|
||||||
|
|
||||||
# 检查Token是否在黑名单中
|
|
||||||
is_blacklisted = await redis_client.get(f"blacklist:{token}")
|
|
||||||
if is_blacklisted:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Token已失效,请重新登录",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"}
|
|
||||||
)
|
|
||||||
|
|
||||||
payload = security_manager.verify_token(token, token_type="access")
|
|
||||||
|
|
||||||
user_id: int = payload.get("sub")
|
|
||||||
if user_id is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="无效的认证凭据",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"}
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.crud.user import user_crud
|
|
||||||
user = await user_crud.get(db, id=user_id)
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="用户不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
if user.status != "active":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="用户已被禁用"
|
|
||||||
)
|
|
||||||
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_active_user(
|
|
||||||
current_user: User = Depends(get_current_user)
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
获取当前活跃用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_user: 当前用户
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 活跃用户对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 用户未激活
|
|
||||||
"""
|
|
||||||
if current_user.status != "active":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="用户账户未激活"
|
|
||||||
)
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_admin_user(
|
|
||||||
current_user: User = Depends(get_current_user)
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
获取当前管理员用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_user: 当前用户
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 管理员用户对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 用户不是管理员
|
|
||||||
"""
|
|
||||||
if not current_user.is_admin:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="权限不足,需要管理员权限"
|
|
||||||
)
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionChecker:
|
|
||||||
"""
|
|
||||||
权限检查器
|
|
||||||
"""
|
|
||||||
def __init__(self, required_permission: str):
|
|
||||||
self.required_permission = required_permission
|
|
||||||
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
检查用户是否有指定权限
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_user: 当前用户
|
|
||||||
db: 数据库会话
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
用户对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 权限不足
|
|
||||||
"""
|
|
||||||
# 管理员拥有所有权限
|
|
||||||
if current_user.is_admin:
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
# 查询用户的所有权限
|
|
||||||
# 获取用户的角色
|
|
||||||
result = await db.execute(
|
|
||||||
select(Role)
|
|
||||||
.join(UserRole, UserRole.role_id == Role.id)
|
|
||||||
.where(UserRole.user_id == current_user.id)
|
|
||||||
.where(Role.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
roles = result.scalars().all()
|
|
||||||
|
|
||||||
# 获取角色对应的所有权限编码
|
|
||||||
if not roles:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="权限不足"
|
|
||||||
)
|
|
||||||
|
|
||||||
role_ids = [role.id for role in roles]
|
|
||||||
result = await db.execute(
|
|
||||||
select(Permission.permission_code)
|
|
||||||
.join(RolePermission, RolePermission.permission_id == Permission.id)
|
|
||||||
.where(RolePermission.role_id.in_(role_ids))
|
|
||||||
.where(Permission.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
permissions = result.scalars().all()
|
|
||||||
|
|
||||||
# 检查是否有必需的权限
|
|
||||||
if self.required_permission not in permissions:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail=f"需要权限: {self.required_permission}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return current_user
|
|
||||||
|
|
||||||
|
|
||||||
# 常用权限检查器
|
|
||||||
require_asset_read = PermissionChecker("asset:asset:read")
|
|
||||||
require_asset_create = PermissionChecker("asset:asset:create")
|
|
||||||
require_asset_update = PermissionChecker("asset:asset:update")
|
|
||||||
require_asset_delete = PermissionChecker("asset:asset:delete")
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
"""
|
|
||||||
自定义异常类
|
|
||||||
"""
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from fastapi import HTTPException, status
|
|
||||||
|
|
||||||
|
|
||||||
class BusinessException(Exception):
|
|
||||||
"""业务逻辑异常基类"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
message: str,
|
|
||||||
code: int = status.HTTP_400_BAD_REQUEST,
|
|
||||||
error_code: Optional[str] = None,
|
|
||||||
data: Optional[Dict[str, Any]] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
初始化业务异常
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 错误消息
|
|
||||||
code: HTTP状态码
|
|
||||||
error_code: 业务错误码
|
|
||||||
data: 附加数据
|
|
||||||
"""
|
|
||||||
self.message = message
|
|
||||||
self.code = code
|
|
||||||
self.error_code = error_code
|
|
||||||
self.data = data
|
|
||||||
super().__init__(self.message)
|
|
||||||
|
|
||||||
|
|
||||||
class NotFoundException(BusinessException):
|
|
||||||
"""资源不存在异常"""
|
|
||||||
|
|
||||||
def __init__(self, resource: str = "资源"):
|
|
||||||
super().__init__(
|
|
||||||
message=f"{resource}不存在",
|
|
||||||
code=status.HTTP_404_NOT_FOUND,
|
|
||||||
error_code="RESOURCE_NOT_FOUND"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyExistsException(BusinessException):
|
|
||||||
"""资源已存在异常"""
|
|
||||||
|
|
||||||
def __init__(self, resource: str = "资源"):
|
|
||||||
super().__init__(
|
|
||||||
message=f"{resource}已存在",
|
|
||||||
code=status.HTTP_409_CONFLICT,
|
|
||||||
error_code="RESOURCE_ALREADY_EXISTS"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionDeniedException(BusinessException):
|
|
||||||
"""权限不足异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "权限不足"):
|
|
||||||
super().__init__(
|
|
||||||
message=message,
|
|
||||||
code=status.HTTP_403_FORBIDDEN,
|
|
||||||
error_code="PERMISSION_DENIED"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationFailedException(BusinessException):
|
|
||||||
"""认证失败异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "认证失败"):
|
|
||||||
super().__init__(
|
|
||||||
message=message,
|
|
||||||
code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
error_code="AUTHENTICATION_FAILED"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationFailedException(BusinessException):
|
|
||||||
"""验证失败异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "数据验证失败", errors: Optional[Dict] = None):
|
|
||||||
super().__init__(
|
|
||||||
message=message,
|
|
||||||
code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
||||||
error_code="VALIDATION_FAILED",
|
|
||||||
data=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidCredentialsException(AuthenticationFailedException):
|
|
||||||
"""无效凭据异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "用户名或密码错误"):
|
|
||||||
super().__init__(message)
|
|
||||||
self.error_code = "INVALID_CREDENTIALS"
|
|
||||||
|
|
||||||
|
|
||||||
class TokenExpiredException(AuthenticationFailedException):
|
|
||||||
"""令牌过期异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "令牌已过期,请重新登录"):
|
|
||||||
super().__init__(message)
|
|
||||||
self.error_code = "TOKEN_EXPIRED"
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidTokenException(AuthenticationFailedException):
|
|
||||||
"""无效令牌异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "无效的令牌"):
|
|
||||||
super().__init__(message)
|
|
||||||
self.error_code = "INVALID_TOKEN"
|
|
||||||
|
|
||||||
|
|
||||||
class CaptchaException(BusinessException):
|
|
||||||
"""验证码异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "验证码错误"):
|
|
||||||
super().__init__(
|
|
||||||
message=message,
|
|
||||||
code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
error_code="CAPTCHA_ERROR"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserLockedException(BusinessException):
|
|
||||||
"""用户被锁定异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "用户已被锁定,请联系管理员"):
|
|
||||||
super().__init__(
|
|
||||||
message=message,
|
|
||||||
code=status.HTTP_403_FORBIDDEN,
|
|
||||||
error_code="USER_LOCKED"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserDisabledException(BusinessException):
|
|
||||||
"""用户被禁用异常"""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "用户已被禁用"):
|
|
||||||
super().__init__(
|
|
||||||
message=message,
|
|
||||||
code=status.HTTP_403_FORBIDDEN,
|
|
||||||
error_code="USER_DISABLED"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StateTransitionException(BusinessException):
|
|
||||||
"""状态转换异常"""
|
|
||||||
|
|
||||||
def __init__(self, current_state: str, target_state: str):
|
|
||||||
super().__init__(
|
|
||||||
message=f"无法从状态 '{current_state}' 转换到 '{target_state}'",
|
|
||||||
code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
error_code="INVALID_STATE_TRANSITION"
|
|
||||||
)
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
"""
|
|
||||||
统一响应封装模块
|
|
||||||
"""
|
|
||||||
from typing import Any, Generic, TypeVar, Optional, List
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# 泛型类型变量
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseModel(BaseModel, Generic[T]):
|
|
||||||
"""统一响应模型"""
|
|
||||||
|
|
||||||
code: int = Field(default=200, description="响应状态码")
|
|
||||||
message: str = Field(default="success", description="响应消息")
|
|
||||||
data: Optional[T] = Field(default=None, description="响应数据")
|
|
||||||
timestamp: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="时间戳")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def success(cls, data: Optional[T] = None, message: str = "success") -> "ResponseModel[T]":
|
|
||||||
"""
|
|
||||||
成功响应
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 响应数据
|
|
||||||
message: 响应消息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ResponseModel: 响应对象
|
|
||||||
"""
|
|
||||||
return cls(code=200, message=message, data=data)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def error(
|
|
||||||
cls,
|
|
||||||
code: int,
|
|
||||||
message: str,
|
|
||||||
data: Optional[T] = None
|
|
||||||
) -> "ResponseModel[T]":
|
|
||||||
"""
|
|
||||||
错误响应
|
|
||||||
|
|
||||||
Args:
|
|
||||||
code: 错误码
|
|
||||||
message: 错误消息
|
|
||||||
data: 附加数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ResponseModel: 响应对象
|
|
||||||
"""
|
|
||||||
return cls(code=code, message=message, data=data)
|
|
||||||
|
|
||||||
|
|
||||||
class PaginationMeta(BaseModel):
|
|
||||||
"""分页元数据"""
|
|
||||||
|
|
||||||
total: int = Field(..., description="总记录数")
|
|
||||||
page: int = Field(..., ge=1, description="当前页码")
|
|
||||||
page_size: int = Field(..., ge=1, le=100, description="每页记录数")
|
|
||||||
total_pages: int = Field(..., ge=0, description="总页数")
|
|
||||||
|
|
||||||
|
|
||||||
class PaginatedResponse(BaseModel, Generic[T]):
|
|
||||||
"""分页响应模型"""
|
|
||||||
|
|
||||||
total: int = Field(..., description="总记录数")
|
|
||||||
page: int = Field(..., ge=1, description="当前页码")
|
|
||||||
page_size: int = Field(..., ge=1, description="每页记录数")
|
|
||||||
total_pages: int = Field(..., ge=0, description="总页数")
|
|
||||||
items: List[T] = Field(default_factory=list, description="数据列表")
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(BaseModel):
|
|
||||||
"""验证错误详情"""
|
|
||||||
|
|
||||||
field: str = Field(..., description="字段名")
|
|
||||||
message: str = Field(..., description="错误消息")
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(BaseModel):
|
|
||||||
"""错误响应模型"""
|
|
||||||
|
|
||||||
code: int = Field(..., description="错误码")
|
|
||||||
message: str = Field(..., description="错误消息")
|
|
||||||
errors: Optional[List[ValidationError]] = Field(default=None, description="错误详情列表")
|
|
||||||
timestamp: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="时间戳")
|
|
||||||
|
|
||||||
|
|
||||||
def success_response(data: Any = None, message: str = "success") -> dict:
|
|
||||||
"""
|
|
||||||
生成成功响应
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 响应数据
|
|
||||||
message: 响应消息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 响应字典
|
|
||||||
"""
|
|
||||||
return ResponseModel.success(data=data, message=message).model_dump()
|
|
||||||
|
|
||||||
|
|
||||||
def error_response(code: int, message: str, errors: Optional[List[dict]] = None) -> dict:
|
|
||||||
"""
|
|
||||||
生成错误响应
|
|
||||||
|
|
||||||
Args:
|
|
||||||
code: 错误码
|
|
||||||
message: 错误消息
|
|
||||||
errors: 错误详情列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 响应字典
|
|
||||||
"""
|
|
||||||
error_data = ErrorResponse(
|
|
||||||
code=code,
|
|
||||||
message=message,
|
|
||||||
errors=[ValidationError(**e) for e in errors] if errors else None
|
|
||||||
)
|
|
||||||
return error_data.model_dump()
|
|
||||||
|
|
||||||
|
|
||||||
def paginated_response(
|
|
||||||
items: List[Any],
|
|
||||||
total: int,
|
|
||||||
page: int,
|
|
||||||
page_size: int
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
生成分页响应
|
|
||||||
|
|
||||||
Args:
|
|
||||||
items: 数据列表
|
|
||||||
total: 总记录数
|
|
||||||
page: 当前页码
|
|
||||||
page_size: 每页记录数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 响应字典
|
|
||||||
"""
|
|
||||||
total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0
|
|
||||||
|
|
||||||
response = PaginatedResponse(
|
|
||||||
total=total,
|
|
||||||
page=page,
|
|
||||||
page_size=page_size,
|
|
||||||
total_pages=total_pages,
|
|
||||||
items=items
|
|
||||||
)
|
|
||||||
|
|
||||||
return success_response(data=response.model_dump())
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
"""
|
|
||||||
安全相关工具模块
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
from jose import JWTError, jwt
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
from fastapi import HTTPException, status
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
# 密码加密上下文
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityManager:
|
|
||||||
"""安全管理器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.secret_key = settings.SECRET_KEY
|
|
||||||
self.algorithm = settings.ALGORITHM
|
|
||||||
self.access_token_expire_minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
|
||||||
self.refresh_token_expire_days = settings.REFRESH_TOKEN_EXPIRE_DAYS
|
|
||||||
|
|
||||||
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
|
||||||
"""
|
|
||||||
验证密码
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plain_password: 明文密码
|
|
||||||
hashed_password: 哈希密码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 密码是否匹配
|
|
||||||
"""
|
|
||||||
return pwd_context.verify(plain_password, hashed_password)
|
|
||||||
|
|
||||||
def get_password_hash(self, password: str) -> str:
|
|
||||||
"""
|
|
||||||
获取密码哈希值
|
|
||||||
|
|
||||||
Args:
|
|
||||||
password: 明文密码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 哈希后的密码
|
|
||||||
"""
|
|
||||||
return pwd_context.hash(password)
|
|
||||||
|
|
||||||
def create_access_token(
|
|
||||||
self,
|
|
||||||
data: Dict[str, Any],
|
|
||||||
expires_delta: Optional[timedelta] = None
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
创建访问令牌
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 要编码的数据
|
|
||||||
expires_delta: 过期时间增量
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: JWT令牌
|
|
||||||
"""
|
|
||||||
to_encode = data.copy()
|
|
||||||
|
|
||||||
if expires_delta:
|
|
||||||
expire = datetime.utcnow() + expires_delta
|
|
||||||
else:
|
|
||||||
expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)
|
|
||||||
|
|
||||||
to_encode.update({
|
|
||||||
"exp": expire,
|
|
||||||
"type": "access"
|
|
||||||
})
|
|
||||||
|
|
||||||
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
|
|
||||||
return encoded_jwt
|
|
||||||
|
|
||||||
def create_refresh_token(
|
|
||||||
self,
|
|
||||||
data: Dict[str, Any],
|
|
||||||
expires_delta: Optional[timedelta] = None
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
创建刷新令牌
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: 要编码的数据
|
|
||||||
expires_delta: 过期时间增量
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: JWT令牌
|
|
||||||
"""
|
|
||||||
to_encode = data.copy()
|
|
||||||
|
|
||||||
if expires_delta:
|
|
||||||
expire = datetime.utcnow() + expires_delta
|
|
||||||
else:
|
|
||||||
expire = datetime.utcnow() + timedelta(days=self.refresh_token_expire_days)
|
|
||||||
|
|
||||||
to_encode.update({
|
|
||||||
"exp": expire,
|
|
||||||
"type": "refresh"
|
|
||||||
})
|
|
||||||
|
|
||||||
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
|
|
||||||
return encoded_jwt
|
|
||||||
|
|
||||||
def decode_token(self, token: str) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
解码令牌
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: JWT令牌
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict: 解码后的数据
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 令牌无效或过期
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
||||||
return payload
|
|
||||||
except JWTError as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="无效的认证凭据",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def verify_token(self, token: str, token_type: str = "access") -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
验证令牌
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: JWT令牌
|
|
||||||
token_type: 令牌类型(access/refresh)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict: 解码后的数据
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 令牌无效或类型不匹配
|
|
||||||
"""
|
|
||||||
payload = self.decode_token(token)
|
|
||||||
|
|
||||||
if payload.get("type") != token_type:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=f"令牌类型不匹配,期望{token_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局安全管理器实例
|
|
||||||
security_manager = SecurityManager()
|
|
||||||
|
|
||||||
|
|
||||||
def get_password_hash(password: str) -> str:
|
|
||||||
"""获取密码哈希值(便捷函数)"""
|
|
||||||
return security_manager.get_password_hash(password)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
||||||
"""验证密码(便捷函数)"""
|
|
||||||
return security_manager.verify_password(plain_password, hashed_password)
|
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
|
||||||
"""创建访问令牌(便捷函数)"""
|
|
||||||
return security_manager.create_access_token(data, expires_delta)
|
|
||||||
|
|
||||||
|
|
||||||
def create_refresh_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
|
||||||
"""创建刷新令牌(便捷函数)"""
|
|
||||||
return security_manager.create_refresh_token(data, expires_delta)
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
"""
|
|
||||||
资产分配相关CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import and_, or_
|
|
||||||
from app.models.allocation import AssetAllocationOrder, AssetAllocationItem
|
|
||||||
from app.models.asset import Asset
|
|
||||||
from app.schemas.allocation import AllocationOrderCreate, AllocationOrderUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderCRUD:
|
|
||||||
"""分配单CRUD操作"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[AssetAllocationOrder]:
|
|
||||||
"""根据ID获取分配单"""
|
|
||||||
return db.query(AssetAllocationOrder).filter(
|
|
||||||
AssetAllocationOrder.id == id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, order_code: str) -> Optional[AssetAllocationOrder]:
|
|
||||||
"""根据单号获取分配单"""
|
|
||||||
return db.query(AssetAllocationOrder).filter(
|
|
||||||
AssetAllocationOrder.order_code == order_code
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
order_type: Optional[str] = None,
|
|
||||||
approval_status: Optional[str] = None,
|
|
||||||
execute_status: Optional[str] = None,
|
|
||||||
applicant_id: Optional[int] = None,
|
|
||||||
target_organization_id: Optional[int] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[AssetAllocationOrder], int]:
|
|
||||||
"""获取分配单列表"""
|
|
||||||
query = db.query(AssetAllocationOrder)
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if order_type:
|
|
||||||
query = query.filter(AssetAllocationOrder.order_type == order_type)
|
|
||||||
if approval_status:
|
|
||||||
query = query.filter(AssetAllocationOrder.approval_status == approval_status)
|
|
||||||
if execute_status:
|
|
||||||
query = query.filter(AssetAllocationOrder.execute_status == execute_status)
|
|
||||||
if applicant_id:
|
|
||||||
query = query.filter(AssetAllocationOrder.applicant_id == applicant_id)
|
|
||||||
if target_organization_id:
|
|
||||||
query = query.filter(AssetAllocationOrder.target_organization_id == target_organization_id)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
AssetAllocationOrder.order_code.like(f"%{keyword}%"),
|
|
||||||
AssetAllocationOrder.title.like(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(AssetAllocationOrder.created_at.desc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AllocationOrderCreate,
|
|
||||||
order_code: str,
|
|
||||||
applicant_id: int
|
|
||||||
) -> AssetAllocationOrder:
|
|
||||||
"""创建分配单"""
|
|
||||||
# 创建分配单
|
|
||||||
db_obj = AssetAllocationOrder(
|
|
||||||
order_code=order_code,
|
|
||||||
order_type=obj_in.order_type,
|
|
||||||
title=obj_in.title,
|
|
||||||
source_organization_id=obj_in.source_organization_id,
|
|
||||||
target_organization_id=obj_in.target_organization_id,
|
|
||||||
applicant_id=applicant_id,
|
|
||||||
expect_execute_date=obj_in.expect_execute_date,
|
|
||||||
remark=obj_in.remark,
|
|
||||||
created_by=applicant_id,
|
|
||||||
approval_status="pending",
|
|
||||||
execute_status="pending"
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
|
|
||||||
# 创建分配单明细
|
|
||||||
self._create_items(
|
|
||||||
db=db,
|
|
||||||
order_id=db_obj.id,
|
|
||||||
asset_ids=obj_in.asset_ids,
|
|
||||||
target_org_id=obj_in.target_organization_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetAllocationOrder,
|
|
||||||
obj_in: AllocationOrderUpdate,
|
|
||||||
updater_id: int
|
|
||||||
) -> AssetAllocationOrder:
|
|
||||||
"""更新分配单"""
|
|
||||||
update_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def approve(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetAllocationOrder,
|
|
||||||
approval_status: str,
|
|
||||||
approver_id: int,
|
|
||||||
approval_remark: Optional[str] = None
|
|
||||||
) -> AssetAllocationOrder:
|
|
||||||
"""审批分配单"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.approval_status = approval_status
|
|
||||||
db_obj.approver_id = approver_id
|
|
||||||
db_obj.approval_time = datetime.utcnow()
|
|
||||||
db_obj.approval_remark = approval_remark
|
|
||||||
|
|
||||||
# 如果审批通过,自动设置为可执行状态
|
|
||||||
if approval_status == "approved":
|
|
||||||
db_obj.execute_status = "pending"
|
|
||||||
elif approval_status == "rejected":
|
|
||||||
db_obj.execute_status = "cancelled"
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def execute(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetAllocationOrder,
|
|
||||||
executor_id: int
|
|
||||||
) -> AssetAllocationOrder:
|
|
||||||
"""执行分配单"""
|
|
||||||
from datetime import datetime, date
|
|
||||||
|
|
||||||
db_obj.execute_status = "completed"
|
|
||||||
db_obj.actual_execute_date = date.today()
|
|
||||||
db_obj.executor_id = executor_id
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def cancel(self, db: Session, db_obj: AssetAllocationOrder) -> AssetAllocationOrder:
|
|
||||||
"""取消分配单"""
|
|
||||||
db_obj.approval_status = "cancelled"
|
|
||||||
db_obj.execute_status = "cancelled"
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int) -> bool:
|
|
||||||
"""删除分配单"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if obj:
|
|
||||||
db.delete(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
applicant_id: Optional[int] = None
|
|
||||||
) -> dict:
|
|
||||||
"""获取分配单统计信息"""
|
|
||||||
query = db.query(AssetAllocationOrder)
|
|
||||||
|
|
||||||
if applicant_id:
|
|
||||||
query = query.filter(AssetAllocationOrder.applicant_id == applicant_id)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
pending = query.filter(AssetAllocationOrder.approval_status == "pending").count()
|
|
||||||
approved = query.filter(AssetAllocationOrder.approval_status == "approved").count()
|
|
||||||
rejected = query.filter(AssetAllocationOrder.approval_status == "rejected").count()
|
|
||||||
executing = query.filter(AssetAllocationOrder.execute_status == "executing").count()
|
|
||||||
completed = query.filter(AssetAllocationOrder.execute_status == "completed").count()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total": total,
|
|
||||||
"pending": pending,
|
|
||||||
"approved": approved,
|
|
||||||
"rejected": rejected,
|
|
||||||
"executing": executing,
|
|
||||||
"completed": completed
|
|
||||||
}
|
|
||||||
|
|
||||||
def _create_items(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
asset_ids: List[int],
|
|
||||||
target_org_id: int
|
|
||||||
):
|
|
||||||
"""创建分配单明细"""
|
|
||||||
# 查询资产信息
|
|
||||||
assets = db.query(Asset).filter(Asset.id.in_(asset_ids)).all()
|
|
||||||
|
|
||||||
for asset in assets:
|
|
||||||
item = AssetAllocationItem(
|
|
||||||
order_id=order_id,
|
|
||||||
asset_id=asset.id,
|
|
||||||
asset_code=asset.asset_code,
|
|
||||||
asset_name=asset.asset_name,
|
|
||||||
from_organization_id=asset.organization_id,
|
|
||||||
to_organization_id=target_org_id,
|
|
||||||
from_status=asset.status,
|
|
||||||
to_status=self._get_target_status(asset.status),
|
|
||||||
execute_status="pending"
|
|
||||||
)
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
def _get_target_status(self, current_status: str) -> str:
|
|
||||||
"""根据当前状态获取目标状态"""
|
|
||||||
status_map = {
|
|
||||||
"in_stock": "transferring",
|
|
||||||
"in_use": "transferring",
|
|
||||||
"maintenance": "in_stock"
|
|
||||||
}
|
|
||||||
return status_map.get(current_status, "transferring")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationItemCRUD:
|
|
||||||
"""分配单明细CRUD操作"""
|
|
||||||
|
|
||||||
def get_by_order(self, db: Session, order_id: int) -> List[AssetAllocationItem]:
|
|
||||||
"""根据分配单ID获取明细列表"""
|
|
||||||
return db.query(AssetAllocationItem).filter(
|
|
||||||
AssetAllocationItem.order_id == order_id
|
|
||||||
).order_by(AssetAllocationItem.id).all()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
order_id: Optional[int] = None,
|
|
||||||
execute_status: Optional[str] = None
|
|
||||||
) -> Tuple[List[AssetAllocationItem], int]:
|
|
||||||
"""获取明细列表"""
|
|
||||||
query = db.query(AssetAllocationItem)
|
|
||||||
|
|
||||||
if order_id:
|
|
||||||
query = query.filter(AssetAllocationItem.order_id == order_id)
|
|
||||||
if execute_status:
|
|
||||||
query = query.filter(AssetAllocationItem.execute_status == execute_status)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def update_execute_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
item_id: int,
|
|
||||||
execute_status: str,
|
|
||||||
failure_reason: Optional[str] = None
|
|
||||||
) -> AssetAllocationItem:
|
|
||||||
"""更新明细执行状态"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
item = db.query(AssetAllocationItem).filter(
|
|
||||||
AssetAllocationItem.id == item_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if item:
|
|
||||||
item.execute_status = execute_status
|
|
||||||
item.execute_time = datetime.utcnow()
|
|
||||||
item.failure_reason = failure_reason
|
|
||||||
db.add(item)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(item)
|
|
||||||
|
|
||||||
return item
|
|
||||||
|
|
||||||
def batch_update_execute_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
execute_status: str
|
|
||||||
):
|
|
||||||
"""批量更新明细执行状态"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
items = db.query(AssetAllocationItem).filter(
|
|
||||||
and_(
|
|
||||||
AssetAllocationItem.order_id == order_id,
|
|
||||||
AssetAllocationItem.execute_status == "pending"
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
item.execute_status = execute_status
|
|
||||||
item.execute_time = datetime.utcnow()
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
allocation_order = AllocationOrderCRUD()
|
|
||||||
allocation_item = AllocationItemCRUD()
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
"""
|
|
||||||
资产CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple, Dict, Any
|
|
||||||
from sqlalchemy import and_, or_, func
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.models.asset import Asset, AssetStatusHistory
|
|
||||||
from app.schemas.asset import AssetCreate, AssetUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class AssetCRUD:
|
|
||||||
"""资产CRUD操作类"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[Asset]:
|
|
||||||
"""根据ID获取资产"""
|
|
||||||
return db.query(Asset).filter(
|
|
||||||
and_(
|
|
||||||
Asset.id == id,
|
|
||||||
Asset.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, code: str) -> Optional[Asset]:
|
|
||||||
"""根据编码获取资产"""
|
|
||||||
return db.query(Asset).filter(
|
|
||||||
and_(
|
|
||||||
Asset.asset_code == code,
|
|
||||||
Asset.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_serial_number(self, db: Session, serial_number: str) -> Optional[Asset]:
|
|
||||||
"""根据序列号获取资产"""
|
|
||||||
return db.query(Asset).filter(
|
|
||||||
and_(
|
|
||||||
Asset.serial_number == serial_number,
|
|
||||||
Asset.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
keyword: Optional[str] = None,
|
|
||||||
device_type_id: Optional[int] = None,
|
|
||||||
organization_id: Optional[int] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
purchase_date_start: Optional[Any] = None,
|
|
||||||
purchase_date_end: Optional[Any] = None
|
|
||||||
) -> Tuple[List[Asset], int]:
|
|
||||||
"""获取资产列表"""
|
|
||||||
query = db.query(Asset).filter(Asset.deleted_at.is_(None))
|
|
||||||
|
|
||||||
# 关键词搜索
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
Asset.asset_code.ilike(f"%{keyword}%"),
|
|
||||||
Asset.asset_name.ilike(f"%{keyword}%"),
|
|
||||||
Asset.model.ilike(f"%{keyword}%"),
|
|
||||||
Asset.serial_number.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if device_type_id:
|
|
||||||
query = query.filter(Asset.device_type_id == device_type_id)
|
|
||||||
if organization_id:
|
|
||||||
query = query.filter(Asset.organization_id == organization_id)
|
|
||||||
if status:
|
|
||||||
query = query.filter(Asset.status == status)
|
|
||||||
if purchase_date_start:
|
|
||||||
query = query.filter(Asset.purchase_date >= purchase_date_start)
|
|
||||||
if purchase_date_end:
|
|
||||||
query = query.filter(Asset.purchase_date <= purchase_date_end)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(Asset.id.desc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AssetCreate,
|
|
||||||
asset_code: str,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
) -> Asset:
|
|
||||||
"""创建资产"""
|
|
||||||
# 计算保修到期日期
|
|
||||||
warranty_expire_date = None
|
|
||||||
if obj_in.purchase_date and obj_in.warranty_period:
|
|
||||||
from datetime import timedelta
|
|
||||||
warranty_expire_date = obj_in.purchase_date + timedelta(days=obj_in.warranty_period * 30)
|
|
||||||
|
|
||||||
db_obj = Asset(
|
|
||||||
**obj_in.model_dump(),
|
|
||||||
asset_code=asset_code,
|
|
||||||
status="pending",
|
|
||||||
warranty_expire_date=warranty_expire_date,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: Asset,
|
|
||||||
obj_in: AssetUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> Asset:
|
|
||||||
"""更新资产"""
|
|
||||||
obj_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
|
|
||||||
# 重新计算保修到期日期
|
|
||||||
if "purchase_date" in obj_data or "warranty_period" in obj_data:
|
|
||||||
purchase_date = obj_data.get("purchase_date", db_obj.purchase_date)
|
|
||||||
warranty_period = obj_data.get("warranty_period", db_obj.warranty_period)
|
|
||||||
|
|
||||||
if purchase_date and warranty_period:
|
|
||||||
from datetime import timedelta
|
|
||||||
obj_data["warranty_expire_date"] = purchase_date + timedelta(days=warranty_period * 30)
|
|
||||||
|
|
||||||
for field, value in obj_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int, deleter_id: Optional[int] = None) -> bool:
|
|
||||||
"""删除资产(软删除)"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if not obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
obj.deleted_at = func.now()
|
|
||||||
obj.deleted_by = deleter_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_by_ids(self, db: Session, ids: List[int]) -> List[Asset]:
|
|
||||||
"""根据ID列表获取资产"""
|
|
||||||
return db.query(Asset).filter(
|
|
||||||
and_(
|
|
||||||
Asset.id.in_(ids),
|
|
||||||
Asset.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
def update_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
new_status: str,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> Optional[Asset]:
|
|
||||||
"""更新资产状态"""
|
|
||||||
obj = self.get(db, asset_id)
|
|
||||||
if not obj:
|
|
||||||
return None
|
|
||||||
|
|
||||||
obj.status = new_status
|
|
||||||
obj.updated_by = updater_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def search_by_dynamic_field(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
field_name: str,
|
|
||||||
field_value: Any,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20
|
|
||||||
) -> Tuple[List[Asset], int]:
|
|
||||||
"""
|
|
||||||
根据动态字段搜索资产
|
|
||||||
|
|
||||||
使用JSONB的@>操作符进行高效查询
|
|
||||||
"""
|
|
||||||
query = db.query(Asset).filter(
|
|
||||||
and_(
|
|
||||||
Asset.deleted_at.is_(None),
|
|
||||||
Asset.dynamic_attributes.has_key(field_name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 根据值类型进行不同的查询
|
|
||||||
if isinstance(field_value, str):
|
|
||||||
query = query.filter(Asset.dynamic_attributes[field_name].astext == field_value)
|
|
||||||
else:
|
|
||||||
query = query.filter(Asset.dynamic_attributes[field_name] == field_value)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
|
|
||||||
class AssetStatusHistoryCRUD:
|
|
||||||
"""资产状态历史CRUD操作类"""
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
old_status: Optional[str],
|
|
||||||
new_status: str,
|
|
||||||
operation_type: str,
|
|
||||||
operator_id: int,
|
|
||||||
operator_name: Optional[str] = None,
|
|
||||||
organization_id: Optional[int] = None,
|
|
||||||
remark: Optional[str] = None,
|
|
||||||
extra_data: Optional[Dict[str, Any]] = None
|
|
||||||
) -> AssetStatusHistory:
|
|
||||||
"""创建状态历史记录"""
|
|
||||||
db_obj = AssetStatusHistory(
|
|
||||||
asset_id=asset_id,
|
|
||||||
old_status=old_status,
|
|
||||||
new_status=new_status,
|
|
||||||
operation_type=operation_type,
|
|
||||||
operator_id=operator_id,
|
|
||||||
operator_name=operator_name,
|
|
||||||
organization_id=organization_id,
|
|
||||||
remark=remark,
|
|
||||||
extra_data=extra_data
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def get_by_asset(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50
|
|
||||||
) -> List[AssetStatusHistory]:
|
|
||||||
"""获取资产的状态历史"""
|
|
||||||
return db.query(AssetStatusHistory).filter(
|
|
||||||
AssetStatusHistory.asset_id == asset_id
|
|
||||||
).order_by(
|
|
||||||
AssetStatusHistory.created_at.desc()
|
|
||||||
).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
asset = AssetCRUD()
|
|
||||||
asset_status_history = AssetStatusHistoryCRUD()
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
"""
|
|
||||||
品牌和供应商CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy import and_, or_, func
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.models.brand_supplier import Brand, Supplier
|
|
||||||
from app.schemas.brand_supplier import (
|
|
||||||
BrandCreate,
|
|
||||||
BrandUpdate,
|
|
||||||
SupplierCreate,
|
|
||||||
SupplierUpdate
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BrandCRUD:
|
|
||||||
"""品牌CRUD操作类"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[Brand]:
|
|
||||||
"""根据ID获取品牌"""
|
|
||||||
return db.query(Brand).filter(
|
|
||||||
and_(
|
|
||||||
Brand.id == id,
|
|
||||||
Brand.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, code: str) -> Optional[Brand]:
|
|
||||||
"""根据代码获取品牌"""
|
|
||||||
return db.query(Brand).filter(
|
|
||||||
and_(
|
|
||||||
Brand.brand_code == code,
|
|
||||||
Brand.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[Brand], int]:
|
|
||||||
"""获取品牌列表"""
|
|
||||||
query = db.query(Brand).filter(Brand.deleted_at.is_(None))
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.filter(Brand.status == status)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
Brand.brand_code.ilike(f"%{keyword}%"),
|
|
||||||
Brand.brand_name.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
query = query.order_by(Brand.sort_order.asc(), Brand.id.desc())
|
|
||||||
total = query.count()
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: BrandCreate, creator_id: Optional[int] = None) -> Brand:
|
|
||||||
"""创建品牌"""
|
|
||||||
if self.get_by_code(db, obj_in.brand_code):
|
|
||||||
raise ValueError(f"品牌代码 '{obj_in.brand_code}' 已存在")
|
|
||||||
|
|
||||||
db_obj = Brand(**obj_in.model_dump(), created_by=creator_id)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: Brand,
|
|
||||||
obj_in: BrandUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> Brand:
|
|
||||||
"""更新品牌"""
|
|
||||||
obj_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in obj_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int, deleter_id: Optional[int] = None) -> bool:
|
|
||||||
"""删除品牌(软删除)"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if not obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
obj.deleted_at = func.now()
|
|
||||||
obj.deleted_by = deleter_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierCRUD:
|
|
||||||
"""供应商CRUD操作类"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[Supplier]:
|
|
||||||
"""根据ID获取供应商"""
|
|
||||||
return db.query(Supplier).filter(
|
|
||||||
and_(
|
|
||||||
Supplier.id == id,
|
|
||||||
Supplier.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, code: str) -> Optional[Supplier]:
|
|
||||||
"""根据代码获取供应商"""
|
|
||||||
return db.query(Supplier).filter(
|
|
||||||
and_(
|
|
||||||
Supplier.supplier_code == code,
|
|
||||||
Supplier.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[Supplier], int]:
|
|
||||||
"""获取供应商列表"""
|
|
||||||
query = db.query(Supplier).filter(Supplier.deleted_at.is_(None))
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.filter(Supplier.status == status)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
Supplier.supplier_code.ilike(f"%{keyword}%"),
|
|
||||||
Supplier.supplier_name.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
query = query.order_by(Supplier.id.desc())
|
|
||||||
total = query.count()
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: SupplierCreate, creator_id: Optional[int] = None) -> Supplier:
|
|
||||||
"""创建供应商"""
|
|
||||||
if self.get_by_code(db, obj_in.supplier_code):
|
|
||||||
raise ValueError(f"供应商代码 '{obj_in.supplier_code}' 已存在")
|
|
||||||
|
|
||||||
db_obj = Supplier(**obj_in.model_dump(), created_by=creator_id)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: Supplier,
|
|
||||||
obj_in: SupplierUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> Supplier:
|
|
||||||
"""更新供应商"""
|
|
||||||
obj_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in obj_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int, deleter_id: Optional[int] = None) -> bool:
|
|
||||||
"""删除供应商(软删除)"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if not obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
obj.deleted_at = func.now()
|
|
||||||
obj.deleted_by = deleter_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
brand = BrandCRUD()
|
|
||||||
supplier = SupplierCRUD()
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
"""
|
|
||||||
设备类型CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy import select, and_, or_, func
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.models.device_type import DeviceType, DeviceTypeField
|
|
||||||
from app.schemas.device_type import DeviceTypeCreate, DeviceTypeUpdate, DeviceTypeFieldCreate, DeviceTypeFieldUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeCRUD:
|
|
||||||
"""设备类型CRUD操作类"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[DeviceType]:
|
|
||||||
"""
|
|
||||||
根据ID获取设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 设备类型ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DeviceType对象或None
|
|
||||||
"""
|
|
||||||
return db.query(DeviceType).filter(
|
|
||||||
and_(
|
|
||||||
DeviceType.id == id,
|
|
||||||
DeviceType.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, code: str) -> Optional[DeviceType]:
|
|
||||||
"""
|
|
||||||
根据代码获取设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
code: 设备类型代码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DeviceType对象或None
|
|
||||||
"""
|
|
||||||
return db.query(DeviceType).filter(
|
|
||||||
and_(
|
|
||||||
DeviceType.type_code == code,
|
|
||||||
DeviceType.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
category: Optional[str] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[DeviceType], int]:
|
|
||||||
"""
|
|
||||||
获取设备类型列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
category: 设备分类筛选
|
|
||||||
status: 状态筛选
|
|
||||||
keyword: 搜索关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(设备类型列表, 总数)
|
|
||||||
"""
|
|
||||||
query = db.query(DeviceType).filter(DeviceType.deleted_at.is_(None))
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if category:
|
|
||||||
query = query.filter(DeviceType.category == category)
|
|
||||||
if status:
|
|
||||||
query = query.filter(DeviceType.status == status)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
DeviceType.type_code.ilike(f"%{keyword}%"),
|
|
||||||
DeviceType.type_name.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(DeviceType.sort_order.asc(), DeviceType.id.desc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(self, db: Session, obj_in: DeviceTypeCreate, creator_id: Optional[int] = None) -> DeviceType:
|
|
||||||
"""
|
|
||||||
创建设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的DeviceType对象
|
|
||||||
"""
|
|
||||||
# 检查代码是否已存在
|
|
||||||
if self.get_by_code(db, obj_in.type_code):
|
|
||||||
raise ValueError(f"设备类型代码 '{obj_in.type_code}' 已存在")
|
|
||||||
|
|
||||||
db_obj = DeviceType(**obj_in.model_dump(), created_by=creator_id)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: DeviceType,
|
|
||||||
obj_in: DeviceTypeUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> DeviceType:
|
|
||||||
"""
|
|
||||||
更新设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
db_obj: 数据库对象
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新后的DeviceType对象
|
|
||||||
"""
|
|
||||||
obj_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in obj_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int, deleter_id: Optional[int] = None) -> bool:
|
|
||||||
"""
|
|
||||||
删除设备类型(软删除)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 设备类型ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否删除成功
|
|
||||||
"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if not obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
obj.deleted_at = func.now()
|
|
||||||
obj.deleted_by = deleter_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_all_categories(self, db: Session) -> List[str]:
|
|
||||||
"""
|
|
||||||
获取所有设备分类
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
设备分类列表
|
|
||||||
"""
|
|
||||||
result = db.query(DeviceType.category).filter(
|
|
||||||
and_(
|
|
||||||
DeviceType.deleted_at.is_(None),
|
|
||||||
DeviceType.category.isnot(None)
|
|
||||||
)
|
|
||||||
).distinct().all()
|
|
||||||
return [r[0] for r in result if r[0]]
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFieldCRUD:
|
|
||||||
"""设备类型字段CRUD操作类"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[DeviceTypeField]:
|
|
||||||
"""
|
|
||||||
根据ID获取字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 字段ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DeviceTypeField对象或None
|
|
||||||
"""
|
|
||||||
return db.query(DeviceTypeField).filter(
|
|
||||||
and_(
|
|
||||||
DeviceTypeField.id == id,
|
|
||||||
DeviceTypeField.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_device_type(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
device_type_id: int,
|
|
||||||
status: Optional[str] = None
|
|
||||||
) -> List[DeviceTypeField]:
|
|
||||||
"""
|
|
||||||
获取设备类型的所有字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
status: 状态筛选
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
字段列表
|
|
||||||
"""
|
|
||||||
query = db.query(DeviceTypeField).filter(
|
|
||||||
and_(
|
|
||||||
DeviceTypeField.device_type_id == device_type_id,
|
|
||||||
DeviceTypeField.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.filter(DeviceTypeField.status == status)
|
|
||||||
|
|
||||||
return query.order_by(DeviceTypeField.sort_order.asc(), DeviceTypeField.id.asc()).all()
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: DeviceTypeFieldCreate,
|
|
||||||
device_type_id: int,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
) -> DeviceTypeField:
|
|
||||||
"""
|
|
||||||
创建字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的DeviceTypeField对象
|
|
||||||
"""
|
|
||||||
# 检查字段代码是否已存在
|
|
||||||
existing = db.query(DeviceTypeField).filter(
|
|
||||||
and_(
|
|
||||||
DeviceTypeField.device_type_id == device_type_id,
|
|
||||||
DeviceTypeField.field_code == obj_in.field_code,
|
|
||||||
DeviceTypeField.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
raise ValueError(f"字段代码 '{obj_in.field_code}' 已存在")
|
|
||||||
|
|
||||||
db_obj = DeviceTypeField(
|
|
||||||
**obj_in.model_dump(),
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: DeviceTypeField,
|
|
||||||
obj_in: DeviceTypeFieldUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> DeviceTypeField:
|
|
||||||
"""
|
|
||||||
更新字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
db_obj: 数据库对象
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新后的DeviceTypeField对象
|
|
||||||
"""
|
|
||||||
obj_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in obj_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int, deleter_id: Optional[int] = None) -> bool:
|
|
||||||
"""
|
|
||||||
删除字段(软删除)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 字段ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否删除成功
|
|
||||||
"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if not obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
obj.deleted_at = func.now()
|
|
||||||
obj.deleted_by = deleter_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def batch_create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
fields_in: List[DeviceTypeFieldCreate],
|
|
||||||
device_type_id: int,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
) -> List[DeviceTypeField]:
|
|
||||||
"""
|
|
||||||
批量创建字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
fields_in: 字段创建列表
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的字段列表
|
|
||||||
"""
|
|
||||||
db_objs = [
|
|
||||||
DeviceTypeField(
|
|
||||||
**field.model_dump(),
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
for field in fields_in
|
|
||||||
]
|
|
||||||
db.add_all(db_objs)
|
|
||||||
db.commit()
|
|
||||||
for obj in db_objs:
|
|
||||||
db.refresh(obj)
|
|
||||||
return db_objs
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
device_type = DeviceTypeCRUD()
|
|
||||||
device_type_field = DeviceTypeFieldCRUD()
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
"""
|
|
||||||
文件管理CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Dict, Any, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import and_, or_, func, desc
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from app.models.file_management import UploadedFile
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDUploadedFile:
|
|
||||||
"""上传文件CRUD操作"""
|
|
||||||
|
|
||||||
def create(self, db: Session, *, obj_in: Dict[str, Any]) -> UploadedFile:
|
|
||||||
"""创建文件记录"""
|
|
||||||
db_obj = UploadedFile(**obj_in)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[UploadedFile]:
|
|
||||||
"""根据ID获取文件"""
|
|
||||||
return db.query(UploadedFile).filter(
|
|
||||||
and_(
|
|
||||||
UploadedFile.id == id,
|
|
||||||
UploadedFile.is_deleted == 0
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_share_code(self, db: Session, share_code: str) -> Optional[UploadedFile]:
|
|
||||||
"""根据分享码获取文件"""
|
|
||||||
now = datetime.utcnow()
|
|
||||||
return db.query(UploadedFile).filter(
|
|
||||||
and_(
|
|
||||||
UploadedFile.share_code == share_code,
|
|
||||||
UploadedFile.is_deleted == 0,
|
|
||||||
or_(
|
|
||||||
UploadedFile.share_expire_time.is_(None),
|
|
||||||
UploadedFile.share_expire_time > now
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
*,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
keyword: Optional[str] = None,
|
|
||||||
file_type: Optional[str] = None,
|
|
||||||
uploader_id: Optional[int] = None,
|
|
||||||
start_date: Optional[str] = None,
|
|
||||||
end_date: Optional[str] = None
|
|
||||||
) -> Tuple[List[UploadedFile], int]:
|
|
||||||
"""获取文件列表"""
|
|
||||||
query = db.query(UploadedFile).filter(UploadedFile.is_deleted == 0)
|
|
||||||
|
|
||||||
# 关键词搜索
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
UploadedFile.original_name.like(f"%{keyword}%"),
|
|
||||||
UploadedFile.file_name.like(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 文件类型筛选
|
|
||||||
if file_type:
|
|
||||||
query = query.filter(UploadedFile.file_type == file_type)
|
|
||||||
|
|
||||||
# 上传者筛选
|
|
||||||
if uploader_id:
|
|
||||||
query = query.filter(UploadedFile.uploader_id == uploader_id)
|
|
||||||
|
|
||||||
# 日期范围筛选
|
|
||||||
if start_date:
|
|
||||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
|
||||||
query = query.filter(UploadedFile.upload_time >= start)
|
|
||||||
|
|
||||||
if end_date:
|
|
||||||
end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
|
||||||
query = query.filter(UploadedFile.upload_time < end)
|
|
||||||
|
|
||||||
# 获取总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.order_by(desc(UploadedFile.upload_time)).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def update(self, db: Session, *, db_obj: UploadedFile, obj_in: Dict[str, Any]) -> UploadedFile:
|
|
||||||
"""更新文件记录"""
|
|
||||||
for field, value in obj_in.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, *, db_obj: UploadedFile, deleter_id: int) -> UploadedFile:
|
|
||||||
"""软删除文件"""
|
|
||||||
db_obj.is_deleted = 1
|
|
||||||
db_obj.deleted_at = datetime.utcnow()
|
|
||||||
db_obj.deleted_by = deleter_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete_batch(self, db: Session, *, file_ids: List[int], deleter_id: int) -> int:
|
|
||||||
"""批量删除文件"""
|
|
||||||
now = datetime.utcnow()
|
|
||||||
count = db.query(UploadedFile).filter(
|
|
||||||
and_(
|
|
||||||
UploadedFile.id.in_(file_ids),
|
|
||||||
UploadedFile.is_deleted == 0
|
|
||||||
)
|
|
||||||
).update({
|
|
||||||
"is_deleted": 1,
|
|
||||||
"deleted_at": now,
|
|
||||||
"deleted_by": deleter_id
|
|
||||||
}, synchronize_session=False)
|
|
||||||
db.commit()
|
|
||||||
return count
|
|
||||||
|
|
||||||
def increment_download_count(self, db: Session, *, file_id: int) -> int:
|
|
||||||
"""增加下载次数"""
|
|
||||||
file_obj = self.get(db, file_id)
|
|
||||||
if file_obj:
|
|
||||||
file_obj.download_count = (file_obj.download_count or 0) + 1
|
|
||||||
db.add(file_obj)
|
|
||||||
db.commit()
|
|
||||||
return file_obj.download_count
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def generate_share_code(self, db: Session, *, file_id: int, expire_days: int = 7) -> str:
|
|
||||||
"""生成分享码"""
|
|
||||||
import secrets
|
|
||||||
import string
|
|
||||||
|
|
||||||
file_obj = self.get(db, file_id)
|
|
||||||
if not file_obj:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 生成随机分享码
|
|
||||||
alphabet = string.ascii_uppercase + string.ascii_lowercase + string.digits
|
|
||||||
share_code = ''.join(secrets.choice(alphabet) for _ in range(16))
|
|
||||||
|
|
||||||
# 设置过期时间
|
|
||||||
expire_time = datetime.utcnow() + timedelta(days=expire_days)
|
|
||||||
|
|
||||||
# 更新文件记录
|
|
||||||
self.update(db, db_obj=file_obj, obj_in={
|
|
||||||
"share_code": share_code,
|
|
||||||
"share_expire_time": expire_time
|
|
||||||
})
|
|
||||||
|
|
||||||
return share_code
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
*,
|
|
||||||
uploader_id: Optional[int] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""获取文件统计信息"""
|
|
||||||
# 基础查询
|
|
||||||
query = db.query(UploadedFile).filter(UploadedFile.is_deleted == 0)
|
|
||||||
|
|
||||||
if uploader_id:
|
|
||||||
query = query.filter(UploadedFile.uploader_id == uploader_id)
|
|
||||||
|
|
||||||
# 总文件数和总大小
|
|
||||||
total_stats = query.with_entities(
|
|
||||||
func.count(UploadedFile.id).label('count'),
|
|
||||||
func.sum(UploadedFile.file_size).label('size')
|
|
||||||
).first()
|
|
||||||
|
|
||||||
# 文件类型分布
|
|
||||||
type_dist = query.with_entities(
|
|
||||||
UploadedFile.file_type,
|
|
||||||
func.count(UploadedFile.id).label('count')
|
|
||||||
).group_by(UploadedFile.file_type).all()
|
|
||||||
|
|
||||||
type_distribution = {file_type: count for file_type, count in type_dist}
|
|
||||||
|
|
||||||
# 今日上传数
|
|
||||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
upload_today = query.filter(UploadedFile.upload_time >= today_start).count()
|
|
||||||
|
|
||||||
# 本周上传数
|
|
||||||
week_start = today_start - timedelta(days=today_start.weekday())
|
|
||||||
upload_this_week = query.filter(UploadedFile.upload_time >= week_start).count()
|
|
||||||
|
|
||||||
# 本月上传数
|
|
||||||
month_start = today_start.replace(day=1)
|
|
||||||
upload_this_month = query.filter(UploadedFile.upload_time >= month_start).count()
|
|
||||||
|
|
||||||
# 上传排行
|
|
||||||
uploader_ranking = query.with_entities(
|
|
||||||
UploadedFile.uploader_id,
|
|
||||||
func.count(UploadedFile.id).label('count')
|
|
||||||
).group_by(UploadedFile.uploader_id).order_by(desc('count')).limit(10).all()
|
|
||||||
|
|
||||||
# 转换为人类可读的文件大小
|
|
||||||
total_size = total_stats.size or 0
|
|
||||||
total_size_human = self._format_size(total_size)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_files": total_stats.count or 0,
|
|
||||||
"total_size": total_size,
|
|
||||||
"total_size_human": total_size_human,
|
|
||||||
"type_distribution": type_distribution,
|
|
||||||
"upload_today": upload_today,
|
|
||||||
"upload_this_week": upload_this_week,
|
|
||||||
"upload_this_month": upload_this_month,
|
|
||||||
"top_uploaders": [{"uploader_id": uid, "count": count} for uid, count in uploader_ranking]
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_size(size_bytes: int) -> str:
|
|
||||||
"""格式化文件大小"""
|
|
||||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
||||||
if size_bytes < 1024.0:
|
|
||||||
return f"{size_bytes:.2f} {unit}"
|
|
||||||
size_bytes /= 1024.0
|
|
||||||
return f"{size_bytes:.2f} PB"
|
|
||||||
|
|
||||||
|
|
||||||
# 创建CRUD实例
|
|
||||||
uploaded_file = CRUDUploadedFile()
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
"""
|
|
||||||
维修管理相关CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import and_, or_, func
|
|
||||||
from app.models.maintenance import MaintenanceRecord
|
|
||||||
from app.schemas.maintenance import MaintenanceRecordCreate, MaintenanceRecordUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordCRUD:
|
|
||||||
"""维修记录CRUD操作"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[MaintenanceRecord]:
|
|
||||||
"""根据ID获取维修记录"""
|
|
||||||
return db.query(MaintenanceRecord).filter(
|
|
||||||
MaintenanceRecord.id == id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, record_code: str) -> Optional[MaintenanceRecord]:
|
|
||||||
"""根据单号获取维修记录"""
|
|
||||||
return db.query(MaintenanceRecord).filter(
|
|
||||||
MaintenanceRecord.record_code == record_code
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
asset_id: Optional[int] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
fault_type: Optional[str] = None,
|
|
||||||
priority: Optional[str] = None,
|
|
||||||
maintenance_type: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[MaintenanceRecord], int]:
|
|
||||||
"""获取维修记录列表"""
|
|
||||||
query = db.query(MaintenanceRecord)
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if asset_id:
|
|
||||||
query = query.filter(MaintenanceRecord.asset_id == asset_id)
|
|
||||||
if status:
|
|
||||||
query = query.filter(MaintenanceRecord.status == status)
|
|
||||||
if fault_type:
|
|
||||||
query = query.filter(MaintenanceRecord.fault_type == fault_type)
|
|
||||||
if priority:
|
|
||||||
query = query.filter(MaintenanceRecord.priority == priority)
|
|
||||||
if maintenance_type:
|
|
||||||
query = query.filter(MaintenanceRecord.maintenance_type == maintenance_type)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
MaintenanceRecord.record_code.like(f"%{keyword}%"),
|
|
||||||
MaintenanceRecord.asset_code.like(f"%{keyword}%"),
|
|
||||||
MaintenanceRecord.fault_description.like(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(MaintenanceRecord.report_time.desc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: MaintenanceRecordCreate,
|
|
||||||
record_code: str,
|
|
||||||
asset_code: str,
|
|
||||||
report_user_id: int,
|
|
||||||
creator_id: int
|
|
||||||
) -> MaintenanceRecord:
|
|
||||||
"""创建维修记录"""
|
|
||||||
db_obj = MaintenanceRecord(
|
|
||||||
record_code=record_code,
|
|
||||||
asset_id=obj_in.asset_id,
|
|
||||||
asset_code=asset_code,
|
|
||||||
fault_description=obj_in.fault_description,
|
|
||||||
fault_type=obj_in.fault_type,
|
|
||||||
report_user_id=report_user_id,
|
|
||||||
priority=obj_in.priority,
|
|
||||||
maintenance_type=obj_in.maintenance_type,
|
|
||||||
vendor_id=obj_in.vendor_id,
|
|
||||||
maintenance_cost=obj_in.maintenance_cost,
|
|
||||||
maintenance_result=obj_in.maintenance_result,
|
|
||||||
replaced_parts=obj_in.replaced_parts,
|
|
||||||
images=obj_in.images,
|
|
||||||
remark=obj_in.remark,
|
|
||||||
status="pending",
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: MaintenanceRecord,
|
|
||||||
obj_in: MaintenanceRecordUpdate,
|
|
||||||
updater_id: int
|
|
||||||
) -> MaintenanceRecord:
|
|
||||||
"""更新维修记录"""
|
|
||||||
update_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def start_maintenance(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: MaintenanceRecord,
|
|
||||||
maintenance_type: str,
|
|
||||||
maintenance_user_id: int,
|
|
||||||
vendor_id: Optional[int] = None
|
|
||||||
) -> MaintenanceRecord:
|
|
||||||
"""开始维修"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.status = "in_progress"
|
|
||||||
db_obj.start_time = datetime.utcnow()
|
|
||||||
db_obj.maintenance_type = maintenance_type
|
|
||||||
db_obj.maintenance_user_id = maintenance_user_id
|
|
||||||
if vendor_id:
|
|
||||||
db_obj.vendor_id = vendor_id
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def complete_maintenance(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: MaintenanceRecord,
|
|
||||||
maintenance_result: str,
|
|
||||||
maintenance_cost: Optional[float] = None,
|
|
||||||
replaced_parts: Optional[str] = None,
|
|
||||||
images: Optional[str] = None
|
|
||||||
) -> MaintenanceRecord:
|
|
||||||
"""完成维修"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.status = "completed"
|
|
||||||
db_obj.complete_time = datetime.utcnow()
|
|
||||||
db_obj.maintenance_result = maintenance_result
|
|
||||||
if maintenance_cost is not None:
|
|
||||||
db_obj.maintenance_cost = maintenance_cost
|
|
||||||
if replaced_parts:
|
|
||||||
db_obj.replaced_parts = replaced_parts
|
|
||||||
if images:
|
|
||||||
db_obj.images = images
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def cancel_maintenance(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: MaintenanceRecord
|
|
||||||
) -> MaintenanceRecord:
|
|
||||||
"""取消维修"""
|
|
||||||
db_obj.status = "cancelled"
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int) -> bool:
|
|
||||||
"""删除维修记录"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if obj:
|
|
||||||
db.delete(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: Optional[int] = None
|
|
||||||
) -> dict:
|
|
||||||
"""获取维修统计信息"""
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
query = db.query(MaintenanceRecord)
|
|
||||||
|
|
||||||
if asset_id:
|
|
||||||
query = query.filter(MaintenanceRecord.asset_id == asset_id)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
pending = query.filter(MaintenanceRecord.status == "pending").count()
|
|
||||||
in_progress = query.filter(MaintenanceRecord.status == "in_progress").count()
|
|
||||||
completed = query.filter(MaintenanceRecord.status == "completed").count()
|
|
||||||
cancelled = query.filter(MaintenanceRecord.status == "cancelled").count()
|
|
||||||
|
|
||||||
# 总维修费用
|
|
||||||
total_cost_result = query.filter(
|
|
||||||
MaintenanceRecord.status == "completed",
|
|
||||||
MaintenanceRecord.maintenance_cost.isnot(None)
|
|
||||||
).with_entities(
|
|
||||||
func.sum(MaintenanceRecord.maintenance_cost)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
total_cost = total_cost_result[0] if total_cost_result and total_cost_result[0] else Decimal("0.00")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total": total,
|
|
||||||
"pending": pending,
|
|
||||||
"in_progress": in_progress,
|
|
||||||
"completed": completed,
|
|
||||||
"cancelled": cancelled,
|
|
||||||
"total_cost": total_cost
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_by_asset(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50
|
|
||||||
) -> List[MaintenanceRecord]:
|
|
||||||
"""根据资产ID获取维修记录"""
|
|
||||||
return db.query(MaintenanceRecord).filter(
|
|
||||||
MaintenanceRecord.asset_id == asset_id
|
|
||||||
).order_by(
|
|
||||||
MaintenanceRecord.report_time.desc()
|
|
||||||
).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
maintenance_record = MaintenanceRecordCRUD()
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
"""
|
|
||||||
消息通知CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import select, and_, or_, func, desc, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.models.notification import Notification, NotificationTemplate
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationCRUD:
|
|
||||||
"""消息通知CRUD类"""
|
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, notification_id: int) -> Optional[Notification]:
|
|
||||||
"""
|
|
||||||
根据ID获取消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_id: 通知ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Notification对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(Notification).where(Notification.id == notification_id)
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_multi(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 100,
|
|
||||||
recipient_id: Optional[int] = None,
|
|
||||||
notification_type: Optional[str] = None,
|
|
||||||
priority: Optional[str] = None,
|
|
||||||
is_read: Optional[bool] = None,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> tuple[List[Notification], int]:
|
|
||||||
"""
|
|
||||||
获取消息通知列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
notification_type: 通知类型
|
|
||||||
priority: 优先级
|
|
||||||
is_read: 是否已读
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
keyword: 关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(通知列表, 总数)
|
|
||||||
"""
|
|
||||||
# 构建查询条件
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
if recipient_id:
|
|
||||||
conditions.append(Notification.recipient_id == recipient_id)
|
|
||||||
|
|
||||||
if notification_type:
|
|
||||||
conditions.append(Notification.notification_type == notification_type)
|
|
||||||
|
|
||||||
if priority:
|
|
||||||
conditions.append(Notification.priority == priority)
|
|
||||||
|
|
||||||
if is_read is not None:
|
|
||||||
conditions.append(Notification.is_read == is_read)
|
|
||||||
|
|
||||||
if start_time:
|
|
||||||
conditions.append(Notification.created_at >= start_time)
|
|
||||||
|
|
||||||
if end_time:
|
|
||||||
conditions.append(Notification.created_at <= end_time)
|
|
||||||
|
|
||||||
if keyword:
|
|
||||||
conditions.append(
|
|
||||||
or_(
|
|
||||||
Notification.title.ilike(f"%{keyword}%"),
|
|
||||||
Notification.content.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 查询总数
|
|
||||||
count_query = select(func.count(Notification.id))
|
|
||||||
if conditions:
|
|
||||||
count_query = count_query.where(and_(*conditions))
|
|
||||||
total_result = await db.execute(count_query)
|
|
||||||
total = total_result.scalar() or 0
|
|
||||||
|
|
||||||
# 查询数据
|
|
||||||
query = select(Notification)
|
|
||||||
if conditions:
|
|
||||||
query = query.where(and_(*conditions))
|
|
||||||
query = query.order_by(
|
|
||||||
Notification.is_read.asc(), # 未读优先
|
|
||||||
desc(Notification.created_at) # 按时间倒序
|
|
||||||
)
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
|
|
||||||
result = await db.execute(query)
|
|
||||||
items = result.scalars().all()
|
|
||||||
|
|
||||||
return list(items), total
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
obj_in: Dict[str, Any]
|
|
||||||
) -> Notification:
|
|
||||||
"""
|
|
||||||
创建消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Notification对象
|
|
||||||
"""
|
|
||||||
db_obj = Notification(**obj_in)
|
|
||||||
db.add(db_obj)
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
async def batch_create(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
recipient_ids: List[int],
|
|
||||||
notification_data: Dict[str, Any]
|
|
||||||
) -> List[Notification]:
|
|
||||||
"""
|
|
||||||
批量创建消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_ids: 接收人ID列表
|
|
||||||
notification_data: 通知数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Notification对象列表
|
|
||||||
"""
|
|
||||||
notifications = []
|
|
||||||
for recipient_id in recipient_ids:
|
|
||||||
obj_data = notification_data.copy()
|
|
||||||
obj_data["recipient_id"] = recipient_id
|
|
||||||
db_obj = Notification(**obj_data)
|
|
||||||
db.add(db_obj)
|
|
||||||
notifications.append(db_obj)
|
|
||||||
|
|
||||||
await db.flush()
|
|
||||||
return notifications
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
db_obj: Notification,
|
|
||||||
obj_in: Dict[str, Any]
|
|
||||||
) -> Notification:
|
|
||||||
"""
|
|
||||||
更新消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
db_obj: 数据库对象
|
|
||||||
obj_in: 更新数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Notification对象
|
|
||||||
"""
|
|
||||||
for field, value in obj_in.items():
|
|
||||||
if hasattr(db_obj, field):
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
async def mark_as_read(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
notification_id: int,
|
|
||||||
read_at: Optional[datetime] = None
|
|
||||||
) -> Optional[Notification]:
|
|
||||||
"""
|
|
||||||
标记为已读
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_id: 通知ID
|
|
||||||
read_at: 已读时间
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Notification对象或None
|
|
||||||
"""
|
|
||||||
db_obj = await self.get(db, notification_id)
|
|
||||||
if not db_obj:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not db_obj.is_read:
|
|
||||||
db_obj.is_read = True
|
|
||||||
db_obj.read_at = read_at or datetime.utcnow()
|
|
||||||
await db.flush()
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
async def mark_all_as_read(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
recipient_id: int,
|
|
||||||
read_at: Optional[datetime] = None
|
|
||||||
) -> int:
|
|
||||||
"""
|
|
||||||
标记所有未读为已读
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
read_at: 已读时间
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新数量
|
|
||||||
"""
|
|
||||||
stmt = (
|
|
||||||
update(Notification)
|
|
||||||
.where(
|
|
||||||
and_(
|
|
||||||
Notification.recipient_id == recipient_id,
|
|
||||||
Notification.is_read == False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.values(
|
|
||||||
is_read=True,
|
|
||||||
read_at=read_at or datetime.utcnow()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
await db.flush()
|
|
||||||
return result.rowcount
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, *, notification_id: int) -> Optional[Notification]:
|
|
||||||
"""
|
|
||||||
删除消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_id: 通知ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除的Notification对象或None
|
|
||||||
"""
|
|
||||||
obj = await self.get(db, notification_id)
|
|
||||||
if obj:
|
|
||||||
await db.delete(obj)
|
|
||||||
await db.flush()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
async def batch_delete(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
notification_ids: List[int]
|
|
||||||
) -> int:
|
|
||||||
"""
|
|
||||||
批量删除通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_ids: 通知ID列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除数量
|
|
||||||
"""
|
|
||||||
from sqlalchemy import delete
|
|
||||||
|
|
||||||
stmt = delete(Notification).where(Notification.id.in_(notification_ids))
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
await db.flush()
|
|
||||||
return result.rowcount
|
|
||||||
|
|
||||||
async def get_unread_count(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
recipient_id: int
|
|
||||||
) -> int:
|
|
||||||
"""
|
|
||||||
获取未读通知数量
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
未读数量
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(func.count(Notification.id)).where(
|
|
||||||
and_(
|
|
||||||
Notification.recipient_id == recipient_id,
|
|
||||||
Notification.is_read == False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return result.scalar() or 0
|
|
||||||
|
|
||||||
async def get_statistics(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
recipient_id: int
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取通知统计信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
统计信息
|
|
||||||
"""
|
|
||||||
# 总数
|
|
||||||
total_result = await db.execute(
|
|
||||||
select(func.count(Notification.id)).where(Notification.recipient_id == recipient_id)
|
|
||||||
)
|
|
||||||
total_count = total_result.scalar() or 0
|
|
||||||
|
|
||||||
# 未读数
|
|
||||||
unread_result = await db.execute(
|
|
||||||
select(func.count(Notification.id)).where(
|
|
||||||
and_(
|
|
||||||
Notification.recipient_id == recipient_id,
|
|
||||||
Notification.is_read == False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
unread_count = unread_result.scalar() or 0
|
|
||||||
|
|
||||||
# 已读数
|
|
||||||
read_count = total_count - unread_count
|
|
||||||
|
|
||||||
# 高优先级数
|
|
||||||
high_priority_result = await db.execute(
|
|
||||||
select(func.count(Notification.id)).where(
|
|
||||||
and_(
|
|
||||||
Notification.recipient_id == recipient_id,
|
|
||||||
Notification.priority.in_(["high", "urgent"]),
|
|
||||||
Notification.is_read == False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
high_priority_count = high_priority_result.scalar() or 0
|
|
||||||
|
|
||||||
# 紧急通知数
|
|
||||||
urgent_result = await db.execute(
|
|
||||||
select(func.count(Notification.id)).where(
|
|
||||||
and_(
|
|
||||||
Notification.recipient_id == recipient_id,
|
|
||||||
Notification.priority == "urgent",
|
|
||||||
Notification.is_read == False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
urgent_count = urgent_result.scalar() or 0
|
|
||||||
|
|
||||||
# 类型分布
|
|
||||||
type_result = await db.execute(
|
|
||||||
select(
|
|
||||||
Notification.notification_type,
|
|
||||||
func.count(Notification.id).label('count')
|
|
||||||
)
|
|
||||||
.where(Notification.recipient_id == recipient_id)
|
|
||||||
.group_by(Notification.notification_type)
|
|
||||||
)
|
|
||||||
type_distribution = [
|
|
||||||
{"type": row[0], "count": row[1]}
|
|
||||||
for row in type_result
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_count": total_count,
|
|
||||||
"unread_count": unread_count,
|
|
||||||
"read_count": read_count,
|
|
||||||
"high_priority_count": high_priority_count,
|
|
||||||
"urgent_count": urgent_count,
|
|
||||||
"type_distribution": type_distribution,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
notification_crud = NotificationCRUD()
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
"""
|
|
||||||
操作日志CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from sqlalchemy import select, and_, or_, func, desc
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.models.operation_log import OperationLog
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogCRUD:
|
|
||||||
"""操作日志CRUD类"""
|
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, log_id: int) -> Optional[OperationLog]:
|
|
||||||
"""
|
|
||||||
根据ID获取操作日志
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
log_id: 日志ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
OperationLog对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(OperationLog).where(OperationLog.id == log_id)
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_multi(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 100,
|
|
||||||
operator_id: Optional[int] = None,
|
|
||||||
operator_name: Optional[str] = None,
|
|
||||||
module: Optional[str] = None,
|
|
||||||
operation_type: Optional[str] = None,
|
|
||||||
result: Optional[str] = None,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> tuple[List[OperationLog], int]:
|
|
||||||
"""
|
|
||||||
获取操作日志列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
operator_id: 操作人ID
|
|
||||||
operator_name: 操作人姓名
|
|
||||||
module: 模块名称
|
|
||||||
operation_type: 操作类型
|
|
||||||
result: 操作结果
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
keyword: 关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(日志列表, 总数)
|
|
||||||
"""
|
|
||||||
# 构建查询条件
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
if operator_id:
|
|
||||||
conditions.append(OperationLog.operator_id == operator_id)
|
|
||||||
|
|
||||||
if operator_name:
|
|
||||||
conditions.append(OperationLog.operator_name.ilike(f"%{operator_name}%"))
|
|
||||||
|
|
||||||
if module:
|
|
||||||
conditions.append(OperationLog.module == module)
|
|
||||||
|
|
||||||
if operation_type:
|
|
||||||
conditions.append(OperationLog.operation_type == operation_type)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
conditions.append(OperationLog.result == result)
|
|
||||||
|
|
||||||
if start_time:
|
|
||||||
conditions.append(OperationLog.created_at >= start_time)
|
|
||||||
|
|
||||||
if end_time:
|
|
||||||
conditions.append(OperationLog.created_at <= end_time)
|
|
||||||
|
|
||||||
if keyword:
|
|
||||||
conditions.append(
|
|
||||||
or_(
|
|
||||||
OperationLog.url.ilike(f"%{keyword}%"),
|
|
||||||
OperationLog.params.ilike(f"%{keyword}%"),
|
|
||||||
OperationLog.error_msg.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 查询总数
|
|
||||||
count_query = select(func.count(OperationLog.id))
|
|
||||||
if conditions:
|
|
||||||
count_query = count_query.where(and_(*conditions))
|
|
||||||
total_result = await db.execute(count_query)
|
|
||||||
total = total_result.scalar() or 0
|
|
||||||
|
|
||||||
# 查询数据
|
|
||||||
query = select(OperationLog)
|
|
||||||
if conditions:
|
|
||||||
query = query.where(and_(*conditions))
|
|
||||||
query = query.order_by(desc(OperationLog.created_at))
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
|
|
||||||
result = await db.execute(query)
|
|
||||||
items = result.scalars().all()
|
|
||||||
|
|
||||||
return list(items), total
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
obj_in: Dict[str, Any]
|
|
||||||
) -> OperationLog:
|
|
||||||
"""
|
|
||||||
创建操作日志
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
OperationLog对象
|
|
||||||
"""
|
|
||||||
db_obj = OperationLog(**obj_in)
|
|
||||||
db.add(db_obj)
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
async def get_statistics(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取操作日志统计信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
统计信息
|
|
||||||
"""
|
|
||||||
# 构建时间条件
|
|
||||||
conditions = []
|
|
||||||
if start_time:
|
|
||||||
conditions.append(OperationLog.created_at >= start_time)
|
|
||||||
if end_time:
|
|
||||||
conditions.append(OperationLog.created_at <= end_time)
|
|
||||||
|
|
||||||
where_clause = and_(*conditions) if conditions else None
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total_query = select(func.count(OperationLog.id))
|
|
||||||
if where_clause:
|
|
||||||
total_query = total_query.where(where_clause)
|
|
||||||
total_result = await db.execute(total_query)
|
|
||||||
total_count = total_result.scalar() or 0
|
|
||||||
|
|
||||||
# 成功数
|
|
||||||
success_query = select(func.count(OperationLog.id)).where(OperationLog.result == "success")
|
|
||||||
if where_clause:
|
|
||||||
success_query = success_query.where(where_clause)
|
|
||||||
success_result = await db.execute(success_query)
|
|
||||||
success_count = success_result.scalar() or 0
|
|
||||||
|
|
||||||
# 失败数
|
|
||||||
failed_count = total_count - success_count
|
|
||||||
|
|
||||||
# 今日操作数
|
|
||||||
today = datetime.utcnow().date()
|
|
||||||
today_start = datetime.combine(today, datetime.min.time())
|
|
||||||
today_query = select(func.count(OperationLog.id)).where(OperationLog.created_at >= today_start)
|
|
||||||
today_result = await db.execute(today_query)
|
|
||||||
today_count = today_result.scalar() or 0
|
|
||||||
|
|
||||||
# 模块分布
|
|
||||||
module_query = select(
|
|
||||||
OperationLog.module,
|
|
||||||
func.count(OperationLog.id).label('count')
|
|
||||||
).group_by(OperationLog.module)
|
|
||||||
if where_clause:
|
|
||||||
module_query = module_query.where(where_clause)
|
|
||||||
module_result = await db.execute(module_query)
|
|
||||||
module_distribution = [
|
|
||||||
{"module": row[0], "count": row[1]}
|
|
||||||
for row in module_result
|
|
||||||
]
|
|
||||||
|
|
||||||
# 操作类型分布
|
|
||||||
operation_query = select(
|
|
||||||
OperationLog.operation_type,
|
|
||||||
func.count(OperationLog.id).label('count')
|
|
||||||
).group_by(OperationLog.operation_type)
|
|
||||||
if where_clause:
|
|
||||||
operation_query = operation_query.where(where_clause)
|
|
||||||
operation_result = await db.execute(operation_query)
|
|
||||||
operation_distribution = [
|
|
||||||
{"operation_type": row[0], "count": row[1]}
|
|
||||||
for row in operation_result
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_count": total_count,
|
|
||||||
"success_count": success_count,
|
|
||||||
"failed_count": failed_count,
|
|
||||||
"today_count": today_count,
|
|
||||||
"module_distribution": module_distribution,
|
|
||||||
"operation_distribution": operation_distribution,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def delete_old_logs(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
days: int = 90
|
|
||||||
) -> int:
|
|
||||||
"""
|
|
||||||
删除旧日志
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
days: 保留天数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除的日志数量
|
|
||||||
"""
|
|
||||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
|
||||||
|
|
||||||
# 查询要删除的日志
|
|
||||||
result = await db.execute(
|
|
||||||
select(OperationLog.id).where(OperationLog.created_at < cutoff_date)
|
|
||||||
)
|
|
||||||
ids_to_delete = [row[0] for row in result]
|
|
||||||
|
|
||||||
if not ids_to_delete:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# 批量删除
|
|
||||||
from sqlalchemy import delete
|
|
||||||
delete_stmt = delete(OperationLog).where(OperationLog.id.in_(ids_to_delete))
|
|
||||||
await db.execute(delete_stmt)
|
|
||||||
|
|
||||||
return len(ids_to_delete)
|
|
||||||
|
|
||||||
async def get_operator_top(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
limit: int = 10,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取操作排行榜
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
limit: 返回条数
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
操作排行列表
|
|
||||||
"""
|
|
||||||
# 构建时间条件
|
|
||||||
conditions = []
|
|
||||||
if start_time:
|
|
||||||
conditions.append(OperationLog.created_at >= start_time)
|
|
||||||
if end_time:
|
|
||||||
conditions.append(OperationLog.created_at <= end_time)
|
|
||||||
|
|
||||||
query = select(
|
|
||||||
OperationLog.operator_id,
|
|
||||||
OperationLog.operator_name,
|
|
||||||
func.count(OperationLog.id).label('count')
|
|
||||||
).group_by(
|
|
||||||
OperationLog.operator_id,
|
|
||||||
OperationLog.operator_name
|
|
||||||
).order_by(
|
|
||||||
desc('count')
|
|
||||||
).limit(limit)
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
query = query.where(and_(*conditions))
|
|
||||||
|
|
||||||
result = await db.execute(query)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"operator_id": row[0],
|
|
||||||
"operator_name": row[1],
|
|
||||||
"count": row[2]
|
|
||||||
}
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
operation_log_crud = OperationLogCRUD()
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
"""
|
|
||||||
机构网点CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy import select, and_, or_, func
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.models.organization import Organization
|
|
||||||
from app.schemas.organization import OrganizationCreate, OrganizationUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationCRUD:
|
|
||||||
"""机构网点CRUD操作类"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[Organization]:
|
|
||||||
"""
|
|
||||||
根据ID获取机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Organization对象或None
|
|
||||||
"""
|
|
||||||
return db.query(Organization).filter(
|
|
||||||
and_(
|
|
||||||
Organization.id == id,
|
|
||||||
Organization.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, code: str) -> Optional[Organization]:
|
|
||||||
"""
|
|
||||||
根据代码获取机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
code: 机构代码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Organization对象或None
|
|
||||||
"""
|
|
||||||
return db.query(Organization).filter(
|
|
||||||
and_(
|
|
||||||
Organization.org_code == code,
|
|
||||||
Organization.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
org_type: Optional[str] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[Organization], int]:
|
|
||||||
"""
|
|
||||||
获取机构列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
org_type: 机构类型筛选
|
|
||||||
status: 状态筛选
|
|
||||||
keyword: 搜索关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(机构列表, 总数)
|
|
||||||
"""
|
|
||||||
query = db.query(Organization).filter(Organization.deleted_at.is_(None))
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if org_type:
|
|
||||||
query = query.filter(Organization.org_type == org_type)
|
|
||||||
if status:
|
|
||||||
query = query.filter(Organization.status == status)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
Organization.org_code.ilike(f"%{keyword}%"),
|
|
||||||
Organization.org_name.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(Organization.tree_level.asc(), Organization.sort_order.asc(), Organization.id.asc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def get_tree(self, db: Session, status: Optional[str] = None) -> List[Organization]:
|
|
||||||
"""
|
|
||||||
获取机构树
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
status: 状态筛选
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
机构树列表
|
|
||||||
"""
|
|
||||||
query = db.query(Organization).filter(Organization.deleted_at.is_(None))
|
|
||||||
|
|
||||||
if status:
|
|
||||||
query = query.filter(Organization.status == status)
|
|
||||||
|
|
||||||
# 获取所有机构
|
|
||||||
all_orgs = query.order_by(Organization.tree_level.asc(), Organization.sort_order.asc()).all()
|
|
||||||
|
|
||||||
# 构建树形结构
|
|
||||||
org_map = {org.id: org for org in all_orgs}
|
|
||||||
tree = []
|
|
||||||
|
|
||||||
for org in all_orgs:
|
|
||||||
# 清空children列表
|
|
||||||
org.children = []
|
|
||||||
|
|
||||||
if org.parent_id is None:
|
|
||||||
# 根节点
|
|
||||||
tree.append(org)
|
|
||||||
else:
|
|
||||||
# 添加到父节点的children
|
|
||||||
parent = org_map.get(org.parent_id)
|
|
||||||
if parent:
|
|
||||||
if not hasattr(parent, 'children'):
|
|
||||||
parent.children = []
|
|
||||||
parent.children.append(org)
|
|
||||||
|
|
||||||
return tree
|
|
||||||
|
|
||||||
def get_children(self, db: Session, parent_id: int) -> List[Organization]:
|
|
||||||
"""
|
|
||||||
获取子机构列表(直接子节点)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
parent_id: 父机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
子机构列表
|
|
||||||
"""
|
|
||||||
return db.query(Organization).filter(
|
|
||||||
and_(
|
|
||||||
Organization.parent_id == parent_id,
|
|
||||||
Organization.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).order_by(Organization.sort_order.asc(), Organization.id.asc()).all()
|
|
||||||
|
|
||||||
def get_all_children(self, db: Session, parent_id: int) -> List[Organization]:
|
|
||||||
"""
|
|
||||||
递归获取所有子机构(包括子节点的子节点)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
parent_id: 父机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
所有子机构列表
|
|
||||||
"""
|
|
||||||
# 获取父节点的tree_path
|
|
||||||
parent = self.get(db, parent_id)
|
|
||||||
if not parent:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 构建查询路径
|
|
||||||
if parent.tree_path:
|
|
||||||
search_path = f"{parent.tree_path}{parent.id}/"
|
|
||||||
else:
|
|
||||||
search_path = f"/{parent.id}/"
|
|
||||||
|
|
||||||
# 查询所有以该路径开头的机构
|
|
||||||
return db.query(Organization).filter(
|
|
||||||
and_(
|
|
||||||
Organization.tree_path.like(f"{search_path}%"),
|
|
||||||
Organization.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).order_by(Organization.tree_level.asc(), Organization.sort_order.asc()).all()
|
|
||||||
|
|
||||||
def get_parents(self, db: Session, child_id: int) -> List[Organization]:
|
|
||||||
"""
|
|
||||||
递归获取所有父机构(从根到直接父节点)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
child_id: 子机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
所有父机构列表(从根到父)
|
|
||||||
"""
|
|
||||||
child = self.get(db, child_id)
|
|
||||||
if not child or not child.tree_path:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 解析tree_path,提取所有ID
|
|
||||||
path_ids = [int(id) for id in child.tree_path.split("/") if id]
|
|
||||||
|
|
||||||
if not path_ids:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 查询所有父机构
|
|
||||||
return db.query(Organization).filter(
|
|
||||||
and_(
|
|
||||||
Organization.id.in_(path_ids),
|
|
||||||
Organization.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
).order_by(Organization.tree_level.asc()).all()
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: OrganizationCreate,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
) -> Organization:
|
|
||||||
"""
|
|
||||||
创建机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的Organization对象
|
|
||||||
"""
|
|
||||||
# 检查代码是否已存在
|
|
||||||
if self.get_by_code(db, obj_in.org_code):
|
|
||||||
raise ValueError(f"机构代码 '{obj_in.org_code}' 已存在")
|
|
||||||
|
|
||||||
# 计算tree_path和tree_level
|
|
||||||
tree_path = None
|
|
||||||
tree_level = 0
|
|
||||||
|
|
||||||
if obj_in.parent_id:
|
|
||||||
parent = self.get(db, obj_in.parent_id)
|
|
||||||
if not parent:
|
|
||||||
raise ValueError(f"父机构ID {obj_in.parent_id} 不存在")
|
|
||||||
|
|
||||||
# 构建tree_path
|
|
||||||
if parent.tree_path:
|
|
||||||
tree_path = f"{parent.tree_path}{parent.id}/"
|
|
||||||
else:
|
|
||||||
tree_path = f"/{parent.id}/"
|
|
||||||
|
|
||||||
tree_level = parent.tree_level + 1
|
|
||||||
|
|
||||||
db_obj = Organization(
|
|
||||||
**obj_in.model_dump(),
|
|
||||||
tree_path=tree_path,
|
|
||||||
tree_level=tree_level,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: Organization,
|
|
||||||
obj_in: OrganizationUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> Organization:
|
|
||||||
"""
|
|
||||||
更新机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
db_obj: 数据库对象
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新后的Organization对象
|
|
||||||
"""
|
|
||||||
obj_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
|
|
||||||
# 如果更新了parent_id,需要重新计算tree_path和tree_level
|
|
||||||
if "parent_id" in obj_data:
|
|
||||||
new_parent_id = obj_data["parent_id"]
|
|
||||||
old_parent_id = db_obj.parent_id
|
|
||||||
|
|
||||||
if new_parent_id != old_parent_id:
|
|
||||||
# 重新计算当前节点的路径
|
|
||||||
if new_parent_id:
|
|
||||||
new_parent = self.get(db, new_parent_id)
|
|
||||||
if not new_parent:
|
|
||||||
raise ValueError(f"父机构ID {new_parent_id} 不存在")
|
|
||||||
|
|
||||||
if new_parent.tree_path:
|
|
||||||
db_obj.tree_path = f"{new_parent.tree_path}{new_parent.id}/"
|
|
||||||
else:
|
|
||||||
db_obj.tree_path = f"/{new_parent.id}/"
|
|
||||||
|
|
||||||
db_obj.tree_level = new_parent.tree_level + 1
|
|
||||||
else:
|
|
||||||
# 变为根节点
|
|
||||||
db_obj.tree_path = None
|
|
||||||
db_obj.tree_level = 0
|
|
||||||
|
|
||||||
# TODO: 需要递归更新所有子节点的tree_path和tree_level
|
|
||||||
# 这里需要批量更新,暂时跳过
|
|
||||||
|
|
||||||
for field, value in obj_data.items():
|
|
||||||
if field != "parent_id": # parent_id已经处理
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int, deleter_id: Optional[int] = None) -> bool:
|
|
||||||
"""
|
|
||||||
删除机构(软删除)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 机构ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否删除成功
|
|
||||||
"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if not obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查是否有子机构
|
|
||||||
children = self.get_children(db, id)
|
|
||||||
if children:
|
|
||||||
raise ValueError("该机构下存在子机构,无法删除")
|
|
||||||
|
|
||||||
obj.deleted_at = func.now()
|
|
||||||
obj.deleted_by = deleter_id
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
organization = OrganizationCRUD()
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
"""
|
|
||||||
资产回收相关CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import and_, or_
|
|
||||||
from app.models.recovery import AssetRecoveryOrder, AssetRecoveryItem
|
|
||||||
from app.models.asset import Asset
|
|
||||||
from app.schemas.recovery import AssetRecoveryOrderCreate, AssetRecoveryOrderUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderCRUD:
|
|
||||||
"""回收单CRUD操作"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[AssetRecoveryOrder]:
|
|
||||||
"""根据ID获取回收单"""
|
|
||||||
return db.query(AssetRecoveryOrder).filter(
|
|
||||||
AssetRecoveryOrder.id == id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, order_code: str) -> Optional[AssetRecoveryOrder]:
|
|
||||||
"""根据单号获取回收单"""
|
|
||||||
return db.query(AssetRecoveryOrder).filter(
|
|
||||||
AssetRecoveryOrder.order_code == order_code
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
recovery_type: Optional[str] = None,
|
|
||||||
approval_status: Optional[str] = None,
|
|
||||||
execute_status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[AssetRecoveryOrder], int]:
|
|
||||||
"""获取回收单列表"""
|
|
||||||
query = db.query(AssetRecoveryOrder)
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if recovery_type:
|
|
||||||
query = query.filter(AssetRecoveryOrder.recovery_type == recovery_type)
|
|
||||||
if approval_status:
|
|
||||||
query = query.filter(AssetRecoveryOrder.approval_status == approval_status)
|
|
||||||
if execute_status:
|
|
||||||
query = query.filter(AssetRecoveryOrder.execute_status == execute_status)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
AssetRecoveryOrder.order_code.like(f"%{keyword}%"),
|
|
||||||
AssetRecoveryOrder.title.like(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(AssetRecoveryOrder.created_at.desc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AssetRecoveryOrderCreate,
|
|
||||||
order_code: str,
|
|
||||||
apply_user_id: int
|
|
||||||
) -> AssetRecoveryOrder:
|
|
||||||
"""创建回收单"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# 创建回收单
|
|
||||||
db_obj = AssetRecoveryOrder(
|
|
||||||
order_code=order_code,
|
|
||||||
recovery_type=obj_in.recovery_type,
|
|
||||||
title=obj_in.title,
|
|
||||||
asset_count=len(obj_in.asset_ids),
|
|
||||||
apply_user_id=apply_user_id,
|
|
||||||
apply_time=datetime.utcnow(),
|
|
||||||
remark=obj_in.remark,
|
|
||||||
approval_status="pending",
|
|
||||||
execute_status="pending"
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
|
|
||||||
# 创建回收单明细
|
|
||||||
self._create_items(
|
|
||||||
db=db,
|
|
||||||
order_id=db_obj.id,
|
|
||||||
asset_ids=obj_in.asset_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetRecoveryOrder,
|
|
||||||
obj_in: AssetRecoveryOrderUpdate
|
|
||||||
) -> AssetRecoveryOrder:
|
|
||||||
"""更新回收单"""
|
|
||||||
update_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def approve(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetRecoveryOrder,
|
|
||||||
approval_status: str,
|
|
||||||
approval_user_id: int,
|
|
||||||
approval_remark: Optional[str] = None
|
|
||||||
) -> AssetRecoveryOrder:
|
|
||||||
"""审批回收单"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.approval_status = approval_status
|
|
||||||
db_obj.approval_user_id = approval_user_id
|
|
||||||
db_obj.approval_time = datetime.utcnow()
|
|
||||||
db_obj.approval_remark = approval_remark
|
|
||||||
|
|
||||||
# 如果审批通过,自动设置为可执行状态
|
|
||||||
if approval_status == "approved":
|
|
||||||
db_obj.execute_status = "pending"
|
|
||||||
elif approval_status == "rejected":
|
|
||||||
db_obj.execute_status = "cancelled"
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def start(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetRecoveryOrder,
|
|
||||||
execute_user_id: int
|
|
||||||
) -> AssetRecoveryOrder:
|
|
||||||
"""开始回收"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.execute_status = "executing"
|
|
||||||
db_obj.execute_user_id = execute_user_id
|
|
||||||
db_obj.execute_time = datetime.utcnow()
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def complete(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetRecoveryOrder,
|
|
||||||
execute_user_id: int
|
|
||||||
) -> AssetRecoveryOrder:
|
|
||||||
"""完成回收"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.execute_status = "completed"
|
|
||||||
db_obj.execute_user_id = execute_user_id
|
|
||||||
db_obj.execute_time = datetime.utcnow()
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def cancel(self, db: Session, db_obj: AssetRecoveryOrder) -> AssetRecoveryOrder:
|
|
||||||
"""取消回收单"""
|
|
||||||
db_obj.approval_status = "cancelled"
|
|
||||||
db_obj.execute_status = "cancelled"
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int) -> bool:
|
|
||||||
"""删除回收单"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if obj:
|
|
||||||
db.delete(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session
|
|
||||||
) -> dict:
|
|
||||||
"""获取回收单统计信息"""
|
|
||||||
query = db.query(AssetRecoveryOrder)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
pending = query.filter(AssetRecoveryOrder.approval_status == "pending").count()
|
|
||||||
approved = query.filter(AssetRecoveryOrder.approval_status == "approved").count()
|
|
||||||
rejected = query.filter(AssetRecoveryOrder.approval_status == "rejected").count()
|
|
||||||
executing = query.filter(AssetRecoveryOrder.execute_status == "executing").count()
|
|
||||||
completed = query.filter(AssetRecoveryOrder.execute_status == "completed").count()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total": total,
|
|
||||||
"pending": pending,
|
|
||||||
"approved": approved,
|
|
||||||
"rejected": rejected,
|
|
||||||
"executing": executing,
|
|
||||||
"completed": completed
|
|
||||||
}
|
|
||||||
|
|
||||||
def _create_items(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
asset_ids: List[int]
|
|
||||||
):
|
|
||||||
"""创建回收单明细"""
|
|
||||||
# 查询资产信息
|
|
||||||
assets = db.query(Asset).filter(Asset.id.in_(asset_ids)).all()
|
|
||||||
|
|
||||||
for asset in assets:
|
|
||||||
item = AssetRecoveryItem(
|
|
||||||
order_id=order_id,
|
|
||||||
asset_id=asset.id,
|
|
||||||
asset_code=asset.asset_code,
|
|
||||||
recovery_status="pending"
|
|
||||||
)
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryItemCRUD:
|
|
||||||
"""回收单明细CRUD操作"""
|
|
||||||
|
|
||||||
def get_by_order(self, db: Session, order_id: int) -> List[AssetRecoveryItem]:
|
|
||||||
"""根据回收单ID获取明细列表"""
|
|
||||||
return db.query(AssetRecoveryItem).filter(
|
|
||||||
AssetRecoveryItem.order_id == order_id
|
|
||||||
).order_by(AssetRecoveryItem.id).all()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
order_id: Optional[int] = None,
|
|
||||||
recovery_status: Optional[str] = None
|
|
||||||
) -> Tuple[List[AssetRecoveryItem], int]:
|
|
||||||
"""获取明细列表"""
|
|
||||||
query = db.query(AssetRecoveryItem)
|
|
||||||
|
|
||||||
if order_id:
|
|
||||||
query = query.filter(AssetRecoveryItem.order_id == order_id)
|
|
||||||
if recovery_status:
|
|
||||||
query = query.filter(AssetRecoveryItem.recovery_status == recovery_status)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def update_recovery_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
item_id: int,
|
|
||||||
recovery_status: str
|
|
||||||
) -> AssetRecoveryItem:
|
|
||||||
"""更新明细回收状态"""
|
|
||||||
item = db.query(AssetRecoveryItem).filter(
|
|
||||||
AssetRecoveryItem.id == item_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if item:
|
|
||||||
item.recovery_status = recovery_status
|
|
||||||
db.add(item)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(item)
|
|
||||||
|
|
||||||
return item
|
|
||||||
|
|
||||||
def batch_update_recovery_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
recovery_status: str
|
|
||||||
):
|
|
||||||
"""批量更新明细回收状态"""
|
|
||||||
items = db.query(AssetRecoveryItem).filter(
|
|
||||||
and_(
|
|
||||||
AssetRecoveryItem.order_id == order_id,
|
|
||||||
AssetRecoveryItem.recovery_status == "pending"
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
item.recovery_status = recovery_status
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
recovery_order = AssetRecoveryOrderCRUD()
|
|
||||||
recovery_item = AssetRecoveryItemCRUD()
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
"""
|
|
||||||
系统配置CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from sqlalchemy import select, and_, or_, func
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.models.system_config import SystemConfig
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigCRUD:
|
|
||||||
"""系统配置CRUD类"""
|
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, config_id: int) -> Optional[SystemConfig]:
|
|
||||||
"""
|
|
||||||
根据ID获取系统配置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
config_id: 配置ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SystemConfig对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(SystemConfig).where(SystemConfig.id == config_id)
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_by_key(self, db: AsyncSession, config_key: str) -> Optional[SystemConfig]:
|
|
||||||
"""
|
|
||||||
根据配置键获取系统配置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
config_key: 配置键
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SystemConfig对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(SystemConfig).where(SystemConfig.config_key == config_key)
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_multi(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 100,
|
|
||||||
keyword: Optional[str] = None,
|
|
||||||
category: Optional[str] = None,
|
|
||||||
is_active: Optional[bool] = None,
|
|
||||||
is_system: Optional[bool] = None
|
|
||||||
) -> tuple[List[SystemConfig], int]:
|
|
||||||
"""
|
|
||||||
获取系统配置列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
keyword: 搜索关键词
|
|
||||||
category: 配置分类
|
|
||||||
is_active: 是否启用
|
|
||||||
is_system: 是否系统配置
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(配置列表, 总数)
|
|
||||||
"""
|
|
||||||
# 构建查询条件
|
|
||||||
conditions = []
|
|
||||||
|
|
||||||
if keyword:
|
|
||||||
conditions.append(
|
|
||||||
or_(
|
|
||||||
SystemConfig.config_key.ilike(f"%{keyword}%"),
|
|
||||||
SystemConfig.config_name.ilike(f"%{keyword}%"),
|
|
||||||
SystemConfig.description.ilike(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if category:
|
|
||||||
conditions.append(SystemConfig.category == category)
|
|
||||||
|
|
||||||
if is_active is not None:
|
|
||||||
conditions.append(SystemConfig.is_active == is_active)
|
|
||||||
|
|
||||||
if is_system is not None:
|
|
||||||
conditions.append(SystemConfig.is_system == is_system)
|
|
||||||
|
|
||||||
# 查询总数
|
|
||||||
count_query = select(func.count(SystemConfig.id))
|
|
||||||
if conditions:
|
|
||||||
count_query = count_query.where(and_(*conditions))
|
|
||||||
total_result = await db.execute(count_query)
|
|
||||||
total = total_result.scalar() or 0
|
|
||||||
|
|
||||||
# 查询数据
|
|
||||||
query = select(SystemConfig)
|
|
||||||
if conditions:
|
|
||||||
query = query.where(and_(*conditions))
|
|
||||||
query = query.order_by(SystemConfig.category, SystemConfig.sort_order, SystemConfig.id)
|
|
||||||
query = query.offset(skip).limit(limit)
|
|
||||||
|
|
||||||
result = await db.execute(query)
|
|
||||||
items = result.scalars().all()
|
|
||||||
|
|
||||||
return list(items), total
|
|
||||||
|
|
||||||
async def get_by_category(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
category: str,
|
|
||||||
*,
|
|
||||||
is_active: bool = True
|
|
||||||
) -> List[SystemConfig]:
|
|
||||||
"""
|
|
||||||
根据分类获取配置列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
category: 配置分类
|
|
||||||
is_active: 是否启用
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
配置列表
|
|
||||||
"""
|
|
||||||
conditions = [SystemConfig.category == category]
|
|
||||||
|
|
||||||
if is_active:
|
|
||||||
conditions.append(SystemConfig.is_active == True)
|
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
select(SystemConfig)
|
|
||||||
.where(and_(*conditions))
|
|
||||||
.order_by(SystemConfig.sort_order, SystemConfig.id)
|
|
||||||
)
|
|
||||||
return list(result.scalars().all())
|
|
||||||
|
|
||||||
async def get_categories(
|
|
||||||
self,
|
|
||||||
db: AsyncSession
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取所有配置分类及统计信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
分类列表
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(
|
|
||||||
SystemConfig.category,
|
|
||||||
func.count(SystemConfig.id).label('count')
|
|
||||||
)
|
|
||||||
.group_by(SystemConfig.category)
|
|
||||||
.order_by(SystemConfig.category)
|
|
||||||
)
|
|
||||||
|
|
||||||
categories = []
|
|
||||||
for row in result:
|
|
||||||
categories.append({
|
|
||||||
"category": row[0],
|
|
||||||
"count": row[1]
|
|
||||||
})
|
|
||||||
|
|
||||||
return categories
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
obj_in: Dict[str, Any]
|
|
||||||
) -> SystemConfig:
|
|
||||||
"""
|
|
||||||
创建系统配置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SystemConfig对象
|
|
||||||
"""
|
|
||||||
db_obj = SystemConfig(**obj_in)
|
|
||||||
db.add(db_obj)
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
db_obj: SystemConfig,
|
|
||||||
obj_in: Dict[str, Any]
|
|
||||||
) -> SystemConfig:
|
|
||||||
"""
|
|
||||||
更新系统配置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
db_obj: 数据库对象
|
|
||||||
obj_in: 更新数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SystemConfig对象
|
|
||||||
"""
|
|
||||||
for field, value in obj_in.items():
|
|
||||||
if hasattr(db_obj, field):
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
async def batch_update(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
configs: Dict[str, Any],
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
) -> List[SystemConfig]:
|
|
||||||
"""
|
|
||||||
批量更新配置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
configs: 配置键值对
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新的配置列表
|
|
||||||
"""
|
|
||||||
updated_configs = []
|
|
||||||
|
|
||||||
for config_key, config_value in configs.items():
|
|
||||||
db_obj = await self.get_by_key(db, config_key)
|
|
||||||
if db_obj:
|
|
||||||
# 转换为字符串存储
|
|
||||||
if isinstance(config_value, (dict, list)):
|
|
||||||
config_value = json.dumps(config_value, ensure_ascii=False)
|
|
||||||
elif isinstance(config_value, bool):
|
|
||||||
config_value = str(config_value).lower()
|
|
||||||
else:
|
|
||||||
config_value = str(config_value)
|
|
||||||
|
|
||||||
db_obj.config_value = config_value
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
updated_configs.append(db_obj)
|
|
||||||
|
|
||||||
await db.flush()
|
|
||||||
return updated_configs
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, *, config_id: int) -> Optional[SystemConfig]:
|
|
||||||
"""
|
|
||||||
删除系统配置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
config_id: 配置ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除的SystemConfig对象或None
|
|
||||||
"""
|
|
||||||
obj = await self.get(db, config_id)
|
|
||||||
if obj:
|
|
||||||
# 系统配置不允许删除
|
|
||||||
if obj.is_system:
|
|
||||||
raise ValueError("系统配置不允许删除")
|
|
||||||
|
|
||||||
await db.delete(obj)
|
|
||||||
await db.flush()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
async def get_value(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
config_key: str,
|
|
||||||
default: Any = None
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
获取配置值(自动转换类型)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
config_key: 配置键
|
|
||||||
default: 默认值
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
配置值
|
|
||||||
"""
|
|
||||||
config = await self.get_by_key(db, config_key)
|
|
||||||
if not config or not config.is_active:
|
|
||||||
return default
|
|
||||||
|
|
||||||
value = config.config_value
|
|
||||||
|
|
||||||
# 根据类型转换
|
|
||||||
if config.value_type == "boolean":
|
|
||||||
return value.lower() in ("true", "1", "yes") if value else False
|
|
||||||
elif config.value_type == "number":
|
|
||||||
try:
|
|
||||||
return int(value) if value else 0
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
return float(value) if value else 0.0
|
|
||||||
except ValueError:
|
|
||||||
return 0
|
|
||||||
elif config.value_type == "json":
|
|
||||||
try:
|
|
||||||
return json.loads(value) if value else {}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return {}
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
system_config_crud = SystemConfigCRUD()
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
"""
|
|
||||||
资产调拨相关CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import and_, or_
|
|
||||||
from app.models.transfer import AssetTransferOrder, AssetTransferItem
|
|
||||||
from app.models.asset import Asset
|
|
||||||
from app.schemas.transfer import AssetTransferOrderCreate, AssetTransferOrderUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderCRUD:
|
|
||||||
"""调拨单CRUD操作"""
|
|
||||||
|
|
||||||
def get(self, db: Session, id: int) -> Optional[AssetTransferOrder]:
|
|
||||||
"""根据ID获取调拨单"""
|
|
||||||
return db.query(AssetTransferOrder).filter(
|
|
||||||
AssetTransferOrder.id == id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_by_code(self, db: Session, order_code: str) -> Optional[AssetTransferOrder]:
|
|
||||||
"""根据单号获取调拨单"""
|
|
||||||
return db.query(AssetTransferOrder).filter(
|
|
||||||
AssetTransferOrder.order_code == order_code
|
|
||||||
).first()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
transfer_type: Optional[str] = None,
|
|
||||||
approval_status: Optional[str] = None,
|
|
||||||
execute_status: Optional[str] = None,
|
|
||||||
source_org_id: Optional[int] = None,
|
|
||||||
target_org_id: Optional[int] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List[AssetTransferOrder], int]:
|
|
||||||
"""获取调拨单列表"""
|
|
||||||
query = db.query(AssetTransferOrder)
|
|
||||||
|
|
||||||
# 筛选条件
|
|
||||||
if transfer_type:
|
|
||||||
query = query.filter(AssetTransferOrder.transfer_type == transfer_type)
|
|
||||||
if approval_status:
|
|
||||||
query = query.filter(AssetTransferOrder.approval_status == approval_status)
|
|
||||||
if execute_status:
|
|
||||||
query = query.filter(AssetTransferOrder.execute_status == execute_status)
|
|
||||||
if source_org_id:
|
|
||||||
query = query.filter(AssetTransferOrder.source_org_id == source_org_id)
|
|
||||||
if target_org_id:
|
|
||||||
query = query.filter(AssetTransferOrder.target_org_id == target_org_id)
|
|
||||||
if keyword:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
AssetTransferOrder.order_code.like(f"%{keyword}%"),
|
|
||||||
AssetTransferOrder.title.like(f"%{keyword}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 排序
|
|
||||||
query = query.order_by(AssetTransferOrder.created_at.desc())
|
|
||||||
|
|
||||||
# 总数
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# 分页
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AssetTransferOrderCreate,
|
|
||||||
order_code: str,
|
|
||||||
apply_user_id: int
|
|
||||||
) -> AssetTransferOrder:
|
|
||||||
"""创建调拨单"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# 创建调拨单
|
|
||||||
db_obj = AssetTransferOrder(
|
|
||||||
order_code=order_code,
|
|
||||||
source_org_id=obj_in.source_org_id,
|
|
||||||
target_org_id=obj_in.target_org_id,
|
|
||||||
transfer_type=obj_in.transfer_type,
|
|
||||||
title=obj_in.title,
|
|
||||||
asset_count=len(obj_in.asset_ids),
|
|
||||||
apply_user_id=apply_user_id,
|
|
||||||
apply_time=datetime.utcnow(),
|
|
||||||
remark=obj_in.remark,
|
|
||||||
approval_status="pending",
|
|
||||||
execute_status="pending"
|
|
||||||
)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
|
|
||||||
# 创建调拨单明细
|
|
||||||
self._create_items(
|
|
||||||
db=db,
|
|
||||||
order_id=db_obj.id,
|
|
||||||
asset_ids=obj_in.asset_ids,
|
|
||||||
source_org_id=obj_in.source_org_id,
|
|
||||||
target_org_id=obj_in.target_org_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetTransferOrder,
|
|
||||||
obj_in: AssetTransferOrderUpdate
|
|
||||||
) -> AssetTransferOrder:
|
|
||||||
"""更新调拨单"""
|
|
||||||
update_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def approve(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetTransferOrder,
|
|
||||||
approval_status: str,
|
|
||||||
approval_user_id: int,
|
|
||||||
approval_remark: Optional[str] = None
|
|
||||||
) -> AssetTransferOrder:
|
|
||||||
"""审批调拨单"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.approval_status = approval_status
|
|
||||||
db_obj.approval_user_id = approval_user_id
|
|
||||||
db_obj.approval_time = datetime.utcnow()
|
|
||||||
db_obj.approval_remark = approval_remark
|
|
||||||
|
|
||||||
# 如果审批通过,自动设置为可执行状态
|
|
||||||
if approval_status == "approved":
|
|
||||||
db_obj.execute_status = "pending"
|
|
||||||
elif approval_status == "rejected":
|
|
||||||
db_obj.execute_status = "cancelled"
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def start(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetTransferOrder,
|
|
||||||
execute_user_id: int
|
|
||||||
) -> AssetTransferOrder:
|
|
||||||
"""开始调拨"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.execute_status = "executing"
|
|
||||||
db_obj.execute_user_id = execute_user_id
|
|
||||||
db_obj.execute_time = datetime.utcnow()
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def complete(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
db_obj: AssetTransferOrder,
|
|
||||||
execute_user_id: int
|
|
||||||
) -> AssetTransferOrder:
|
|
||||||
"""完成调拨"""
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
db_obj.execute_status = "completed"
|
|
||||||
db_obj.execute_user_id = execute_user_id
|
|
||||||
db_obj.execute_time = datetime.utcnow()
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def cancel(self, db: Session, db_obj: AssetTransferOrder) -> AssetTransferOrder:
|
|
||||||
"""取消调拨单"""
|
|
||||||
db_obj.approval_status = "cancelled"
|
|
||||||
db_obj.execute_status = "cancelled"
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def delete(self, db: Session, id: int) -> bool:
|
|
||||||
"""删除调拨单"""
|
|
||||||
obj = self.get(db, id)
|
|
||||||
if obj:
|
|
||||||
db.delete(obj)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
source_org_id: Optional[int] = None,
|
|
||||||
target_org_id: Optional[int] = None
|
|
||||||
) -> dict:
|
|
||||||
"""获取调拨单统计信息"""
|
|
||||||
query = db.query(AssetTransferOrder)
|
|
||||||
|
|
||||||
if source_org_id:
|
|
||||||
query = query.filter(AssetTransferOrder.source_org_id == source_org_id)
|
|
||||||
if target_org_id:
|
|
||||||
query = query.filter(AssetTransferOrder.target_org_id == target_org_id)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
pending = query.filter(AssetTransferOrder.approval_status == "pending").count()
|
|
||||||
approved = query.filter(AssetTransferOrder.approval_status == "approved").count()
|
|
||||||
rejected = query.filter(AssetTransferOrder.approval_status == "rejected").count()
|
|
||||||
executing = query.filter(AssetTransferOrder.execute_status == "executing").count()
|
|
||||||
completed = query.filter(AssetTransferOrder.execute_status == "completed").count()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total": total,
|
|
||||||
"pending": pending,
|
|
||||||
"approved": approved,
|
|
||||||
"rejected": rejected,
|
|
||||||
"executing": executing,
|
|
||||||
"completed": completed
|
|
||||||
}
|
|
||||||
|
|
||||||
def _create_items(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
asset_ids: List[int],
|
|
||||||
source_org_id: int,
|
|
||||||
target_org_id: int
|
|
||||||
):
|
|
||||||
"""创建调拨单明细"""
|
|
||||||
# 查询资产信息
|
|
||||||
assets = db.query(Asset).filter(Asset.id.in_(asset_ids)).all()
|
|
||||||
|
|
||||||
for asset in assets:
|
|
||||||
item = AssetTransferItem(
|
|
||||||
order_id=order_id,
|
|
||||||
asset_id=asset.id,
|
|
||||||
asset_code=asset.asset_code,
|
|
||||||
source_organization_id=source_org_id,
|
|
||||||
target_organization_id=target_org_id,
|
|
||||||
transfer_status="pending"
|
|
||||||
)
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferItemCRUD:
|
|
||||||
"""调拨单明细CRUD操作"""
|
|
||||||
|
|
||||||
def get_by_order(self, db: Session, order_id: int) -> List[AssetTransferItem]:
|
|
||||||
"""根据调拨单ID获取明细列表"""
|
|
||||||
return db.query(AssetTransferItem).filter(
|
|
||||||
AssetTransferItem.order_id == order_id
|
|
||||||
).order_by(AssetTransferItem.id).all()
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
order_id: Optional[int] = None,
|
|
||||||
transfer_status: Optional[str] = None
|
|
||||||
) -> Tuple[List[AssetTransferItem], int]:
|
|
||||||
"""获取明细列表"""
|
|
||||||
query = db.query(AssetTransferItem)
|
|
||||||
|
|
||||||
if order_id:
|
|
||||||
query = query.filter(AssetTransferItem.order_id == order_id)
|
|
||||||
if transfer_status:
|
|
||||||
query = query.filter(AssetTransferItem.transfer_status == transfer_status)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
items = query.offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def update_transfer_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
item_id: int,
|
|
||||||
transfer_status: str
|
|
||||||
) -> AssetTransferItem:
|
|
||||||
"""更新明细调拨状态"""
|
|
||||||
item = db.query(AssetTransferItem).filter(
|
|
||||||
AssetTransferItem.id == item_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if item:
|
|
||||||
item.transfer_status = transfer_status
|
|
||||||
db.add(item)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(item)
|
|
||||||
|
|
||||||
return item
|
|
||||||
|
|
||||||
def batch_update_transfer_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
transfer_status: str
|
|
||||||
):
|
|
||||||
"""批量更新明细调拨状态"""
|
|
||||||
items = db.query(AssetTransferItem).filter(
|
|
||||||
and_(
|
|
||||||
AssetTransferItem.order_id == order_id,
|
|
||||||
AssetTransferItem.transfer_status == "pending"
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
item.transfer_status = transfer_status
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
transfer_order = AssetTransferOrderCRUD()
|
|
||||||
transfer_item = AssetTransferItemCRUD()
|
|
||||||
@@ -1,435 +0,0 @@
|
|||||||
"""
|
|
||||||
用户CRUD操作
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Tuple
|
|
||||||
from sqlalchemy import select, and_, or_
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.models.user import User, Role, UserRole, Permission, RolePermission
|
|
||||||
from app.schemas.user import UserCreate, UserUpdate, RoleCreate, RoleUpdate
|
|
||||||
from app.core.security import get_password_hash
|
|
||||||
|
|
||||||
|
|
||||||
class UserCRUD:
|
|
||||||
"""用户CRUD类"""
|
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, id: int) -> Optional[User]:
|
|
||||||
"""
|
|
||||||
根据ID获取用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 用户ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 用户对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(User)
|
|
||||||
.options(selectinload(User.roles).selectinload(Role.permissions))
|
|
||||||
.where(User.id == id, User.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_by_username(self, db: AsyncSession, username: str) -> Optional[User]:
|
|
||||||
"""
|
|
||||||
根据用户名获取用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
username: 用户名
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 用户对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(User)
|
|
||||||
.options(selectinload(User.roles).selectinload(Role.permissions))
|
|
||||||
.where(User.username == username, User.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_by_email(self, db: AsyncSession, email: str) -> Optional[User]:
|
|
||||||
"""
|
|
||||||
根据邮箱获取用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
email: 邮箱
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 用户对象或None
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(User)
|
|
||||||
.where(User.email == email, User.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_multi(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
keyword: Optional[str] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
role_id: Optional[int] = None
|
|
||||||
) -> Tuple[List[User], int]:
|
|
||||||
"""
|
|
||||||
获取用户列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
keyword: 搜索关键词
|
|
||||||
status: 状态筛选
|
|
||||||
role_id: 角色ID筛选
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[List[User], int]: 用户列表和总数
|
|
||||||
"""
|
|
||||||
# 构建查询条件
|
|
||||||
conditions = [User.deleted_at.is_(None)]
|
|
||||||
|
|
||||||
if keyword:
|
|
||||||
keyword_pattern = f"%{keyword}%"
|
|
||||||
conditions.append(
|
|
||||||
or_(
|
|
||||||
User.username.ilike(keyword_pattern),
|
|
||||||
User.real_name.ilike(keyword_pattern),
|
|
||||||
User.phone.ilike(keyword_pattern)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
conditions.append(User.status == status)
|
|
||||||
|
|
||||||
# 构建基础查询
|
|
||||||
query = select(User).options(selectinload(User.roles)).where(*conditions)
|
|
||||||
|
|
||||||
# 如果需要按角色筛选
|
|
||||||
if role_id:
|
|
||||||
query = query.join(UserRole).where(UserRole.role_id == role_id)
|
|
||||||
|
|
||||||
# 按ID降序排序
|
|
||||||
query = query.order_by(User.id.desc())
|
|
||||||
|
|
||||||
# 获取总数
|
|
||||||
count_query = select(User.id).where(*conditions)
|
|
||||||
if role_id:
|
|
||||||
count_query = count_query.join(UserRole).where(UserRole.role_id == role_id)
|
|
||||||
|
|
||||||
result = await db.execute(select(User.id).where(*conditions))
|
|
||||||
total = len(result.all())
|
|
||||||
|
|
||||||
# 分页查询
|
|
||||||
result = await db.execute(query.offset(skip).limit(limit))
|
|
||||||
users = result.scalars().all()
|
|
||||||
|
|
||||||
return list(users), total
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: UserCreate, creator_id: int) -> User:
|
|
||||||
"""
|
|
||||||
创建用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 创建的用户对象
|
|
||||||
"""
|
|
||||||
# 检查用户名是否已存在
|
|
||||||
existing_user = await self.get_by_username(db, obj_in.username)
|
|
||||||
if existing_user:
|
|
||||||
raise ValueError("用户名已存在")
|
|
||||||
|
|
||||||
# 检查邮箱是否已存在
|
|
||||||
if obj_in.email:
|
|
||||||
existing_email = await self.get_by_email(db, obj_in.email)
|
|
||||||
if existing_email:
|
|
||||||
raise ValueError("邮箱已存在")
|
|
||||||
|
|
||||||
# 创建用户对象
|
|
||||||
db_obj = User(
|
|
||||||
username=obj_in.username,
|
|
||||||
password_hash=get_password_hash(obj_in.password),
|
|
||||||
real_name=obj_in.real_name,
|
|
||||||
email=obj_in.email,
|
|
||||||
phone=obj_in.phone,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
|
|
||||||
# 分配角色
|
|
||||||
for role_id in obj_in.role_ids:
|
|
||||||
user_role = UserRole(
|
|
||||||
user_id=db_obj.id,
|
|
||||||
role_id=role_id,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
db.add(user_role)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
|
|
||||||
return await self.get(db, db_obj.id)
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
db_obj: User,
|
|
||||||
obj_in: UserUpdate,
|
|
||||||
updater_id: int
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
更新用户
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
db_obj: 数据库中的用户对象
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User: 更新后的用户对象
|
|
||||||
"""
|
|
||||||
update_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
|
|
||||||
# 检查邮箱是否已被其他用户使用
|
|
||||||
if "email" in update_data and update_data["email"]:
|
|
||||||
existing_user = await db.execute(
|
|
||||||
select(User).where(
|
|
||||||
User.email == update_data["email"],
|
|
||||||
User.id != db_obj.id,
|
|
||||||
User.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if existing_user.scalar_one_or_none():
|
|
||||||
raise ValueError("邮箱已被使用")
|
|
||||||
|
|
||||||
# 更新字段
|
|
||||||
for field, value in update_data.items():
|
|
||||||
if field == "role_ids":
|
|
||||||
continue
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
|
|
||||||
# 更新角色
|
|
||||||
if "role_ids" in update_data:
|
|
||||||
# 删除旧角色
|
|
||||||
await db.execute(
|
|
||||||
select(UserRole).where(UserRole.user_id == db_obj.id)
|
|
||||||
)
|
|
||||||
old_roles = await db.execute(
|
|
||||||
select(UserRole).where(UserRole.user_id == db_obj.id)
|
|
||||||
)
|
|
||||||
for old_role in old_roles.scalars().all():
|
|
||||||
await db.delete(old_role)
|
|
||||||
|
|
||||||
# 添加新角色
|
|
||||||
for role_id in update_data["role_ids"]:
|
|
||||||
user_role = UserRole(
|
|
||||||
user_id=db_obj.id,
|
|
||||||
role_id=role_id,
|
|
||||||
created_by=updater_id
|
|
||||||
)
|
|
||||||
db.add(user_role)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
|
|
||||||
return await self.get(db, db_obj.id)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, id: int, deleter_id: int) -> bool:
|
|
||||||
"""
|
|
||||||
删除用户(软删除)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
id: 用户ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否删除成功
|
|
||||||
"""
|
|
||||||
db_obj = await self.get(db, id)
|
|
||||||
if not db_obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
db_obj.deleted_at = datetime.utcnow()
|
|
||||||
db_obj.deleted_by = deleter_id
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def update_password(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
user: User,
|
|
||||||
new_password: str
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
更新用户密码
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
user: 用户对象
|
|
||||||
new_password: 新密码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否更新成功
|
|
||||||
"""
|
|
||||||
user.password_hash = get_password_hash(new_password)
|
|
||||||
user.login_fail_count = 0
|
|
||||||
user.locked_until = None
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def update_last_login(self, db: AsyncSession, user: User) -> bool:
|
|
||||||
"""
|
|
||||||
更新用户最后登录时间
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
user: 用户对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否更新成功
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
user.last_login_at = datetime.utcnow()
|
|
||||||
user.login_fail_count = 0
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class RoleCRUD:
|
|
||||||
"""角色CRUD类"""
|
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, id: int) -> Optional[Role]:
|
|
||||||
"""根据ID获取角色"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(Role)
|
|
||||||
.options(selectinload(Role.permissions))
|
|
||||||
.where(Role.id == id, Role.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_by_code(self, db: AsyncSession, role_code: str) -> Optional[Role]:
|
|
||||||
"""根据代码获取角色"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(Role).where(Role.role_code == role_code, Role.deleted_at.is_(None))
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_multi(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
status: Optional[str] = None
|
|
||||||
) -> List[Role]:
|
|
||||||
"""获取角色列表"""
|
|
||||||
conditions = [Role.deleted_at.is_(None)]
|
|
||||||
|
|
||||||
if status:
|
|
||||||
conditions.append(Role.status == status)
|
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
select(Role)
|
|
||||||
.options(selectinload(Role.permissions))
|
|
||||||
.where(*conditions)
|
|
||||||
.order_by(Role.sort_order, Role.id)
|
|
||||||
)
|
|
||||||
return list(result.scalars().all())
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: RoleCreate, creator_id: int) -> Role:
|
|
||||||
"""创建角色"""
|
|
||||||
# 检查代码是否已存在
|
|
||||||
existing_role = await self.get_by_code(db, obj_in.role_code)
|
|
||||||
if existing_role:
|
|
||||||
raise ValueError("角色代码已存在")
|
|
||||||
|
|
||||||
db_obj = Role(
|
|
||||||
role_name=obj_in.role_name,
|
|
||||||
role_code=obj_in.role_code,
|
|
||||||
description=obj_in.description,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
|
|
||||||
db.add(db_obj)
|
|
||||||
await db.flush()
|
|
||||||
await db.refresh(db_obj)
|
|
||||||
|
|
||||||
# 分配权限
|
|
||||||
for permission_id in obj_in.permission_ids:
|
|
||||||
role_permission = RolePermission(
|
|
||||||
role_id=db_obj.id,
|
|
||||||
permission_id=permission_id,
|
|
||||||
created_by=creator_id
|
|
||||||
)
|
|
||||||
db.add(role_permission)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return await self.get(db, db_obj.id)
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
db_obj: Role,
|
|
||||||
obj_in: RoleUpdate,
|
|
||||||
updater_id: int
|
|
||||||
) -> Role:
|
|
||||||
"""更新角色"""
|
|
||||||
update_data = obj_in.model_dump(exclude_unset=True)
|
|
||||||
|
|
||||||
for field, value in update_data.items():
|
|
||||||
if field == "permission_ids":
|
|
||||||
continue
|
|
||||||
setattr(db_obj, field, value)
|
|
||||||
|
|
||||||
db_obj.updated_by = updater_id
|
|
||||||
|
|
||||||
# 更新权限
|
|
||||||
if "permission_ids" in update_data:
|
|
||||||
# 删除旧权限
|
|
||||||
old_permissions = await db.execute(
|
|
||||||
select(RolePermission).where(RolePermission.role_id == db_obj.id)
|
|
||||||
)
|
|
||||||
for old_perm in old_permissions.scalars().all():
|
|
||||||
await db.delete(old_perm)
|
|
||||||
|
|
||||||
# 添加新权限
|
|
||||||
for permission_id in update_data["permission_ids"]:
|
|
||||||
role_permission = RolePermission(
|
|
||||||
role_id=db_obj.id,
|
|
||||||
permission_id=permission_id,
|
|
||||||
created_by=updater_id
|
|
||||||
)
|
|
||||||
db.add(role_permission)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return await self.get(db, db_obj.id)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, id: int) -> bool:
|
|
||||||
"""删除角色(软删除)"""
|
|
||||||
db_obj = await self.get(db, id)
|
|
||||||
if not db_obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
db_obj.deleted_at = datetime.utcnow()
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# 创建CRUD实例
|
|
||||||
user_crud = UserCRUD()
|
|
||||||
role_crud = RoleCRUD()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
"""
|
|
||||||
数据库模块初始化
|
|
||||||
"""
|
|
||||||
from app.db.session import engine, async_session_maker, get_db, init_db, close_db
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"engine",
|
|
||||||
"async_session_maker",
|
|
||||||
"get_db",
|
|
||||||
"init_db",
|
|
||||||
"close_db",
|
|
||||||
]
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
"""
|
|
||||||
数据库基类和配置
|
|
||||||
"""
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
|
||||||
"""数据库模型基类"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Base"]
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"""
|
|
||||||
数据库会话管理
|
|
||||||
"""
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
# 创建异步引擎
|
|
||||||
engine = create_async_engine(
|
|
||||||
settings.DATABASE_URL,
|
|
||||||
echo=settings.DATABASE_ECHO,
|
|
||||||
pool_pre_ping=True,
|
|
||||||
pool_size=50, # 从20增加到50,提高并发性能
|
|
||||||
max_overflow=10, # 从0增加到10,允许峰值时的额外连接
|
|
||||||
)
|
|
||||||
|
|
||||||
# 创建异步会话工厂
|
|
||||||
async_session_maker = async_sessionmaker(
|
|
||||||
engine,
|
|
||||||
class_=AsyncSession,
|
|
||||||
expire_on_commit=False,
|
|
||||||
autocommit=False,
|
|
||||||
autoflush=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
||||||
"""
|
|
||||||
获取数据库会话
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
AsyncSession: 数据库会话
|
|
||||||
"""
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
try:
|
|
||||||
yield session
|
|
||||||
await session.commit()
|
|
||||||
except Exception:
|
|
||||||
await session.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
await session.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def init_db() -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库(创建所有表)
|
|
||||||
注意:生产环境应使用Alembic迁移
|
|
||||||
"""
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
# 导入所有模型以确保它们被注册
|
|
||||||
from app.models import user, asset, device_type, organization
|
|
||||||
|
|
||||||
# 创建所有表
|
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
|
||||||
|
|
||||||
|
|
||||||
async def close_db() -> None:
|
|
||||||
"""关闭数据库连接"""
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"engine",
|
|
||||||
"async_session_maker",
|
|
||||||
"get_db",
|
|
||||||
"init_db",
|
|
||||||
"close_db",
|
|
||||||
]
|
|
||||||
-177
@@ -1,177 +0,0 @@
|
|||||||
"""
|
|
||||||
FastAPI应用主入口
|
|
||||||
"""
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI, Request, status
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from fastapi.exceptions import RequestValidationError
|
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
||||||
from loguru import logger
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.exceptions import BusinessException
|
|
||||||
from app.core.response import error_response
|
|
||||||
from app.api.v1 import api_router
|
|
||||||
from app.db.session import init_db, close_db
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logger.remove()
|
|
||||||
logger.add(
|
|
||||||
sys.stderr,
|
|
||||||
level=settings.LOG_LEVEL,
|
|
||||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
|
||||||
colorize=True
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.add(
|
|
||||||
settings.LOG_FILE,
|
|
||||||
rotation=settings.LOG_ROTATION,
|
|
||||||
retention=settings.LOG_RETENTION,
|
|
||||||
level=settings.LOG_LEVEL,
|
|
||||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""应用生命周期管理"""
|
|
||||||
# 启动时执行
|
|
||||||
logger.info("🚀 应用启动中...")
|
|
||||||
logger.info(f"📦 环境: {settings.APP_ENVIRONMENT}")
|
|
||||||
logger.info(f"🔗 数据库: {settings.DATABASE_URL}")
|
|
||||||
|
|
||||||
# 初始化数据库(生产环境使用Alembic迁移)
|
|
||||||
if settings.is_development:
|
|
||||||
await init_db()
|
|
||||||
logger.info("✅ 数据库初始化完成")
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# 关闭时执行
|
|
||||||
logger.info("🛑 应用关闭中...")
|
|
||||||
await close_db()
|
|
||||||
logger.info("✅ 数据库连接已关闭")
|
|
||||||
|
|
||||||
|
|
||||||
# 创建FastAPI应用
|
|
||||||
app = FastAPI(
|
|
||||||
title=settings.APP_NAME,
|
|
||||||
version=settings.APP_VERSION,
|
|
||||||
description="企业级资产管理系统后端API",
|
|
||||||
docs_url="/docs" if settings.DEBUG else None,
|
|
||||||
redoc_url="/redoc" if settings.DEBUG else None,
|
|
||||||
openapi_url="/openapi.json" if settings.DEBUG else None,
|
|
||||||
lifespan=lifespan
|
|
||||||
)
|
|
||||||
|
|
||||||
# 配置CORS
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
|
||||||
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
|
|
||||||
allow_methods=settings.CORS_ALLOW_METHODS,
|
|
||||||
allow_headers=settings.CORS_ALLOW_HEADERS,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# 自定义异常处理器
|
|
||||||
@app.exception_handler(BusinessException)
|
|
||||||
async def business_exception_handler(request: Request, exc: BusinessException):
|
|
||||||
"""业务异常处理"""
|
|
||||||
logger.warning(f"业务异常: {exc.message} - 错误码: {exc.error_code}")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=exc.code,
|
|
||||||
content=error_response(
|
|
||||||
code=exc.code,
|
|
||||||
message=exc.message,
|
|
||||||
errors=[{"field": k, "message": v} for k, v in exc.data.items()] if exc.data else None
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(StarletteHTTPException)
|
|
||||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
|
||||||
"""HTTP异常处理"""
|
|
||||||
logger.warning(f"HTTP异常: {exc.status_code} - {exc.detail}")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=exc.status_code,
|
|
||||||
content=error_response(
|
|
||||||
code=exc.status_code,
|
|
||||||
message=str(exc.detail)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
|
||||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
||||||
"""请求验证异常处理"""
|
|
||||||
errors = []
|
|
||||||
for error in exc.errors():
|
|
||||||
errors.append({
|
|
||||||
"field": ".".join(str(loc) for loc in error["loc"]),
|
|
||||||
"message": error["msg"]
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.warning(f"验证异常: {errors}")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
||||||
content=error_response(
|
|
||||||
code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
||||||
message="参数验证失败",
|
|
||||||
errors=errors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
|
||||||
async def general_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""通用异常处理"""
|
|
||||||
logger.error(f"未处理的异常: {type(exc).__name__} - {str(exc)}", exc_info=True)
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
content=error_response(
|
|
||||||
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
message="服务器内部错误" if not settings.DEBUG else str(exc)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# 注册路由
|
|
||||||
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
|
|
||||||
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
@app.get("/health", tags=["系统"])
|
|
||||||
async def health_check():
|
|
||||||
"""健康检查接口"""
|
|
||||||
return {
|
|
||||||
"status": "ok",
|
|
||||||
"app_name": settings.APP_NAME,
|
|
||||||
"version": settings.APP_VERSION,
|
|
||||||
"environment": settings.APP_ENVIRONMENT
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 根路径
|
|
||||||
@app.get("/", tags=["系统"])
|
|
||||||
async def root():
|
|
||||||
"""根路径"""
|
|
||||||
return {
|
|
||||||
"message": f"欢迎使用{settings.APP_NAME} API",
|
|
||||||
"version": settings.APP_VERSION,
|
|
||||||
"docs": "/docs" if settings.DEBUG else None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(
|
|
||||||
"app.main:app",
|
|
||||||
host=settings.HOST,
|
|
||||||
port=settings.PORT,
|
|
||||||
reload=settings.DEBUG,
|
|
||||||
log_level=settings.LOG_LEVEL.lower()
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
中间件模块
|
|
||||||
"""
|
|
||||||
from app.middleware.operation_log import OperationLogMiddleware
|
|
||||||
|
|
||||||
__all__ = ["OperationLogMiddleware"]
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
"""
|
|
||||||
操作日志中间件
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
from typing import Callable
|
|
||||||
from fastapi import Request, Response
|
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.db.session import async_session_maker
|
|
||||||
from app.schemas.operation_log import OperationLogCreate, OperationModuleEnum, OperationTypeEnum, OperationResultEnum
|
|
||||||
from app.services.operation_log_service import operation_log_service
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogMiddleware(BaseHTTPMiddleware):
|
|
||||||
"""操作日志中间件"""
|
|
||||||
|
|
||||||
# 不需要记录的路径
|
|
||||||
EXCLUDE_PATHS = [
|
|
||||||
"/health",
|
|
||||||
"/docs",
|
|
||||||
"/openapi.json",
|
|
||||||
"/api/v1/auth/login",
|
|
||||||
"/api/v1/auth/captcha",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 路径到模块的映射
|
|
||||||
PATH_MODULE_MAP = {
|
|
||||||
"/auth": OperationModuleEnum.AUTH,
|
|
||||||
"/device-types": OperationModuleEnum.DEVICE_TYPE,
|
|
||||||
"/organizations": OperationModuleEnum.ORGANIZATION,
|
|
||||||
"/assets": OperationModuleEnum.ASSET,
|
|
||||||
"/brands": OperationModuleEnum.BRAND_SUPPLIER,
|
|
||||||
"/suppliers": OperationModuleEnum.BRAND_SUPPLIER,
|
|
||||||
"/allocation-orders": OperationModuleEnum.ALLOCATION,
|
|
||||||
"/maintenance-records": OperationModuleEnum.MAINTENANCE,
|
|
||||||
"/system-config": OperationModuleEnum.SYSTEM_CONFIG,
|
|
||||||
"/users": OperationModuleEnum.USER,
|
|
||||||
"/statistics": OperationModuleEnum.STATISTICS,
|
|
||||||
"/operation-logs": OperationModuleEnum.SYSTEM_CONFIG,
|
|
||||||
"/notifications": OperationModuleEnum.SYSTEM_CONFIG,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 方法到操作类型的映射
|
|
||||||
METHOD_OPERATION_MAP = {
|
|
||||||
"GET": OperationTypeEnum.QUERY,
|
|
||||||
"POST": OperationTypeEnum.CREATE,
|
|
||||||
"PUT": OperationTypeEnum.UPDATE,
|
|
||||||
"PATCH": OperationTypeEnum.UPDATE,
|
|
||||||
"DELETE": OperationTypeEnum.DELETE,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
||||||
"""处理请求"""
|
|
||||||
# 检查是否需要记录
|
|
||||||
if self._should_log(request):
|
|
||||||
# 记录开始时间
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
# 获取用户信息
|
|
||||||
user = getattr(request.state, "user", None)
|
|
||||||
|
|
||||||
# 处理请求
|
|
||||||
response = await call_next(request)
|
|
||||||
|
|
||||||
# 计算执行时长
|
|
||||||
duration = int((time.time() - start_time) * 1000)
|
|
||||||
|
|
||||||
# 异步记录日志
|
|
||||||
if user:
|
|
||||||
await self._log_operation(request, response, user, duration)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
return await call_next(request)
|
|
||||||
|
|
||||||
def _should_log(self, request: Request) -> bool:
|
|
||||||
"""判断是否需要记录日志"""
|
|
||||||
path = request.url.path
|
|
||||||
|
|
||||||
# 检查排除路径
|
|
||||||
for exclude_path in self.EXCLUDE_PATHS:
|
|
||||||
if path.startswith(exclude_path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 只记录API请求
|
|
||||||
return path.startswith("/api/")
|
|
||||||
|
|
||||||
async def _log_operation(
|
|
||||||
self,
|
|
||||||
request: Request,
|
|
||||||
response: Response,
|
|
||||||
user,
|
|
||||||
duration: int
|
|
||||||
):
|
|
||||||
"""记录操作日志"""
|
|
||||||
try:
|
|
||||||
# 获取模块
|
|
||||||
module = self._get_module(request.url.path)
|
|
||||||
|
|
||||||
# 获取操作类型
|
|
||||||
operation_type = self.METHOD_OPERATION_MAP.get(request.method, OperationTypeEnum.QUERY)
|
|
||||||
|
|
||||||
# 特殊处理:如果是登录/登出
|
|
||||||
if "/auth/login" in request.url.path:
|
|
||||||
operation_type = OperationTypeEnum.LOGIN
|
|
||||||
elif "/auth/logout" in request.url.path:
|
|
||||||
operation_type = OperationTypeEnum.LOGOUT
|
|
||||||
|
|
||||||
# 获取请求参数
|
|
||||||
params = await self._get_request_params(request)
|
|
||||||
|
|
||||||
# 构建日志数据
|
|
||||||
log_data = OperationLogCreate(
|
|
||||||
operator_id=user.id,
|
|
||||||
operator_name=user.real_name or user.username,
|
|
||||||
operator_ip=request.client.host if request.client else None,
|
|
||||||
module=module,
|
|
||||||
operation_type=operation_type,
|
|
||||||
method=request.method,
|
|
||||||
url=request.url.path,
|
|
||||||
params=params,
|
|
||||||
result=OperationResultEnum.SUCCESS if response.status_code < 400 else OperationResultEnum.FAILED,
|
|
||||||
error_msg=None if response.status_code < 400 else f"HTTP {response.status_code}",
|
|
||||||
duration=duration,
|
|
||||||
user_agent=request.headers.get("user-agent"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 异步保存日志
|
|
||||||
async with async_session_maker() as db:
|
|
||||||
await operation_log_service.create_log(db, log_data)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 记录日志失败不应影响业务
|
|
||||||
print(f"Failed to log operation: {e}")
|
|
||||||
|
|
||||||
def _get_module(self, path: str) -> OperationModuleEnum:
|
|
||||||
"""根据路径获取模块"""
|
|
||||||
for path_prefix, module in self.PATH_MODULE_MAP.items():
|
|
||||||
if path_prefix in path:
|
|
||||||
return module
|
|
||||||
return OperationModuleEnum.SYSTEM_CONFIG
|
|
||||||
|
|
||||||
async def _get_request_params(self, request: Request) -> str:
|
|
||||||
"""获取请求参数"""
|
|
||||||
try:
|
|
||||||
# GET请求
|
|
||||||
if request.method == "GET":
|
|
||||||
params = dict(request.query_params)
|
|
||||||
return json.dumps(params, ensure_ascii=False)
|
|
||||||
|
|
||||||
# POST/PUT/DELETE请求
|
|
||||||
if request.method in ["POST", "PUT", "DELETE", "PATCH"]:
|
|
||||||
try:
|
|
||||||
body = await request.body()
|
|
||||||
if body:
|
|
||||||
# 尝试解析JSON
|
|
||||||
try:
|
|
||||||
body_json = json.loads(body.decode())
|
|
||||||
# 过滤敏感字段
|
|
||||||
filtered_body = self._filter_sensitive_data(body_json)
|
|
||||||
return json.dumps(filtered_body, ensure_ascii=False)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# 不是JSON,返回原始数据
|
|
||||||
return body.decode()[:500] # 限制长度
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return ""
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _filter_sensitive_data(self, data: dict) -> dict:
|
|
||||||
"""过滤敏感数据"""
|
|
||||||
sensitive_fields = ["password", "old_password", "new_password", "token", "secret"]
|
|
||||||
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
return data
|
|
||||||
|
|
||||||
filtered = {}
|
|
||||||
for key, value in data.items():
|
|
||||||
if key in sensitive_fields:
|
|
||||||
filtered[key] = "******"
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
filtered[key] = self._filter_sensitive_data(value)
|
|
||||||
elif isinstance(value, list):
|
|
||||||
filtered[key] = [
|
|
||||||
self._filter_sensitive_data(item) if isinstance(item, dict) else item
|
|
||||||
for item in value
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
filtered[key] = value
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
"""
|
|
||||||
数据模型模块初始化
|
|
||||||
"""
|
|
||||||
from app.models.user import User, Role, UserRole, Permission, RolePermission
|
|
||||||
from app.models.device_type import DeviceType, DeviceTypeField
|
|
||||||
from app.models.organization import Organization
|
|
||||||
from app.models.brand_supplier import Brand, Supplier
|
|
||||||
from app.models.asset import Asset, AssetStatusHistory
|
|
||||||
from app.models.allocation import AssetAllocationOrder, AssetAllocationItem
|
|
||||||
from app.models.maintenance import MaintenanceRecord
|
|
||||||
from app.models.transfer import AssetTransferOrder, AssetTransferItem
|
|
||||||
from app.models.recovery import AssetRecoveryOrder, AssetRecoveryItem
|
|
||||||
from app.models.system_config import SystemConfig
|
|
||||||
from app.models.operation_log import OperationLog
|
|
||||||
from app.models.notification import Notification, NotificationTemplate
|
|
||||||
from app.models.file_management import UploadedFile
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"User",
|
|
||||||
"Role",
|
|
||||||
"UserRole",
|
|
||||||
"Permission",
|
|
||||||
"RolePermission",
|
|
||||||
"DeviceType",
|
|
||||||
"DeviceTypeField",
|
|
||||||
"Organization",
|
|
||||||
"Brand",
|
|
||||||
"Supplier",
|
|
||||||
"Asset",
|
|
||||||
"AssetStatusHistory",
|
|
||||||
"AssetAllocationOrder",
|
|
||||||
"AssetAllocationItem",
|
|
||||||
"MaintenanceRecord",
|
|
||||||
"AssetTransferOrder",
|
|
||||||
"AssetTransferItem",
|
|
||||||
"AssetRecoveryOrder",
|
|
||||||
"AssetRecoveryItem",
|
|
||||||
"SystemConfig",
|
|
||||||
"OperationLog",
|
|
||||||
"Notification",
|
|
||||||
"NotificationTemplate",
|
|
||||||
"UploadedFile",
|
|
||||||
]
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"""
|
|
||||||
资产分配相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime, date
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Date, Numeric, Index
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class AssetAllocationOrder(Base):
|
|
||||||
"""资产分配单表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_allocation_orders"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
order_code = Column(String(50), unique=True, nullable=False, index=True, comment="分配单号")
|
|
||||||
order_type = Column(String(20), nullable=False, index=True, comment="单据类型")
|
|
||||||
title = Column(String(200), nullable=False, comment="标题")
|
|
||||||
source_organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=True, comment="调出网点ID")
|
|
||||||
target_organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=False, index=True, comment="调入网点ID")
|
|
||||||
applicant_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True, comment="申请人ID")
|
|
||||||
approver_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="审批人ID")
|
|
||||||
approval_status = Column(String(20), default="pending", nullable=False, index=True, comment="审批状态")
|
|
||||||
approval_time = Column(DateTime, nullable=True, comment="审批时间")
|
|
||||||
approval_remark = Column(Text, nullable=True, comment="审批备注")
|
|
||||||
expect_execute_date = Column(Date, nullable=True, comment="预计执行日期")
|
|
||||||
actual_execute_date = Column(Date, nullable=True, comment="实际执行日期")
|
|
||||||
executor_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="执行人ID")
|
|
||||||
execute_status = Column(String(20), default="pending", nullable=False, comment="执行状态")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=False)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
source_organization = relationship("Organization", foreign_keys=[source_organization_id])
|
|
||||||
target_organization = relationship("Organization", foreign_keys=[target_organization_id])
|
|
||||||
applicant = relationship("User", foreign_keys=[applicant_id])
|
|
||||||
approver = relationship("User", foreign_keys=[approver_id])
|
|
||||||
executor = relationship("User", foreign_keys=[executor_id])
|
|
||||||
items = relationship("AssetAllocationItem", back_populates="order", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_allocation_orders_code", "order_code"),
|
|
||||||
Index("idx_allocation_orders_target_org", "target_organization_id"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetAllocationOrder(id={self.id}, order_code={self.order_code}, order_type={self.order_type})>"
|
|
||||||
|
|
||||||
|
|
||||||
class AssetAllocationItem(Base):
|
|
||||||
"""资产分配单明细表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_allocation_items"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
order_id = Column(BigInteger, ForeignKey("asset_allocation_orders.id", ondelete="CASCADE"), nullable=False, comment="分配单ID")
|
|
||||||
asset_id = Column(BigInteger, ForeignKey("assets.id"), nullable=False, comment="资产ID")
|
|
||||||
asset_code = Column(String(50), nullable=False, comment="资产编码")
|
|
||||||
asset_name = Column(String(200), nullable=False, comment="资产名称")
|
|
||||||
from_organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=True, comment="原网点ID")
|
|
||||||
to_organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=True, comment="目标网点ID")
|
|
||||||
from_status = Column(String(20), nullable=True, comment="原状态")
|
|
||||||
to_status = Column(String(20), nullable=True, comment="目标状态")
|
|
||||||
execute_status = Column(String(20), default="pending", nullable=False, index=True, comment="执行状态")
|
|
||||||
execute_time = Column(DateTime, nullable=True, comment="执行时间")
|
|
||||||
failure_reason = Column(Text, nullable=True, comment="失败原因")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
order = relationship("AssetAllocationOrder", back_populates="items")
|
|
||||||
asset = relationship("Asset")
|
|
||||||
from_organization = relationship("Organization", foreign_keys=[from_organization_id])
|
|
||||||
to_organization = relationship("Organization", foreign_keys=[to_organization_id])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_allocation_items_order", "order_id"),
|
|
||||||
Index("idx_allocation_items_asset", "asset_id"),
|
|
||||||
Index("idx_allocation_items_status", "execute_status"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetAllocationItem(id={self.id}, order_id={self.order_id}, asset_code={self.asset_code})>"
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
"""
|
|
||||||
资产相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime, date
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Date, Numeric, Index
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Asset(Base):
|
|
||||||
"""资产表"""
|
|
||||||
|
|
||||||
__tablename__ = "assets"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
asset_code = Column(String(50), unique=True, nullable=False, index=True, comment="资产编码")
|
|
||||||
asset_name = Column(String(200), nullable=False, comment="资产名称")
|
|
||||||
device_type_id = Column(BigInteger, ForeignKey("device_types.id"), nullable=False, comment="设备类型ID")
|
|
||||||
brand_id = Column(BigInteger, ForeignKey("brands.id"), nullable=True, comment="品牌ID")
|
|
||||||
model = Column(String(200), nullable=True, comment="规格型号")
|
|
||||||
serial_number = Column(String(200), nullable=True, index=True, comment="序列号(SN)")
|
|
||||||
supplier_id = Column(BigInteger, ForeignKey("suppliers.id"), nullable=True, comment="供应商ID")
|
|
||||||
purchase_date = Column(Date, nullable=True, index=True, comment="采购日期")
|
|
||||||
purchase_price = Column(Numeric(18, 2), nullable=True, comment="采购价格")
|
|
||||||
warranty_period = Column(Integer, nullable=True, comment="保修期(月)")
|
|
||||||
warranty_expire_date = Column(Date, nullable=True, comment="保修到期日期")
|
|
||||||
organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=False, comment="所属网点ID")
|
|
||||||
location = Column(String(500), nullable=True, comment="存放位置")
|
|
||||||
status = Column(String(20), default="pending", nullable=False, index=True, comment="状态")
|
|
||||||
dynamic_attributes = Column(JSONB, default={}, comment="动态字段值")
|
|
||||||
qr_code_url = Column(String(500), nullable=True, comment="二维码图片URL")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
device_type = relationship("DeviceType", back_populates="assets")
|
|
||||||
brand = relationship("Brand", back_populates="assets")
|
|
||||||
supplier = relationship("Supplier", back_populates="assets")
|
|
||||||
organization = relationship("Organization")
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
status_history = relationship("AssetStatusHistory", back_populates="asset", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Asset(id={self.id}, asset_code={self.asset_code}, asset_name={self.asset_name})>"
|
|
||||||
|
|
||||||
|
|
||||||
class AssetStatusHistory(Base):
|
|
||||||
"""资产状态历史表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_status_history"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
asset_id = Column(BigInteger, ForeignKey("assets.id", ondelete="CASCADE"), nullable=False, comment="资产ID")
|
|
||||||
old_status = Column(String(20), nullable=True, comment="原状态")
|
|
||||||
new_status = Column(String(20), nullable=False, index=True, comment="新状态")
|
|
||||||
operation_type = Column(String(50), nullable=False, comment="操作类型")
|
|
||||||
operator_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, comment="操作人ID")
|
|
||||||
operator_name = Column(String(100), nullable=True, comment="操作人姓名(冗余)")
|
|
||||||
organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=True, comment="相关网点ID")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
extra_data = Column(JSONB, nullable=True, comment="额外数据")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
asset = relationship("Asset", back_populates="status_history")
|
|
||||||
operator = relationship("User", foreign_keys=[operator_id])
|
|
||||||
organization = relationship("Organization")
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_asset_status_history_asset", "asset_id"),
|
|
||||||
Index("idx_asset_status_history_time", "created_at"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetStatusHistory(id={self.id}, asset_id={self.asset_id}, old_status={self.old_status}, new_status={self.new_status})>"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"""
|
|
||||||
品牌和供应商数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Brand(Base):
|
|
||||||
"""品牌表"""
|
|
||||||
|
|
||||||
__tablename__ = "brands"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
brand_code = Column(String(50), unique=True, nullable=False, index=True, comment="品牌代码")
|
|
||||||
brand_name = Column(String(200), nullable=False, comment="品牌名称")
|
|
||||||
logo_url = Column(String(500), nullable=True, comment="Logo URL")
|
|
||||||
website = Column(String(500), nullable=True, comment="官网地址")
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="状态: active, inactive")
|
|
||||||
sort_order = Column(Integer, default=0, nullable=False, comment="排序")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
assets = relationship("Asset", back_populates="brand")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Brand(id={self.id}, brand_code={self.brand_code}, brand_name={self.brand_name})>"
|
|
||||||
|
|
||||||
|
|
||||||
class Supplier(Base):
|
|
||||||
"""供应商表"""
|
|
||||||
|
|
||||||
__tablename__ = "suppliers"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
supplier_code = Column(String(50), unique=True, nullable=False, index=True, comment="供应商代码")
|
|
||||||
supplier_name = Column(String(200), nullable=False, comment="供应商名称")
|
|
||||||
contact_person = Column(String(100), nullable=True, comment="联系人")
|
|
||||||
contact_phone = Column(String(20), nullable=True, comment="联系电话")
|
|
||||||
email = Column(String(255), nullable=True, comment="邮箱")
|
|
||||||
address = Column(String(500), nullable=True, comment="地址")
|
|
||||||
credit_code = Column(String(50), nullable=True, comment="统一社会信用代码")
|
|
||||||
bank_name = Column(String(200), nullable=True, comment="开户银行")
|
|
||||||
bank_account = Column(String(100), nullable=True, comment="银行账号")
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="状态: active, inactive")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
assets = relationship("Asset", back_populates="supplier")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Supplier(id={self.id}, supplier_code={self.supplier_code}, supplier_name={self.supplier_name})>"
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"""
|
|
||||||
设备类型相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Index
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(Base):
|
|
||||||
"""设备类型表"""
|
|
||||||
|
|
||||||
__tablename__ = "device_types"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
type_code = Column(String(50), unique=True, nullable=False, index=True, comment="设备类型代码")
|
|
||||||
type_name = Column(String(200), nullable=False, comment="设备类型名称")
|
|
||||||
category = Column(String(50), nullable=True, comment="设备分类: IT设备, 办公设备, 生产设备等")
|
|
||||||
description = Column(Text, nullable=True, comment="描述")
|
|
||||||
icon = Column(String(100), nullable=True, comment="图标名称")
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="状态: active, inactive")
|
|
||||||
sort_order = Column(Integer, default=0, nullable=False, comment="排序")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
fields = relationship("DeviceTypeField", back_populates="device_type", cascade="all, delete-orphan")
|
|
||||||
assets = relationship("Asset", back_populates="device_type")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<DeviceType(id={self.id}, type_code={self.type_code}, type_name={self.type_name})>"
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeField(Base):
|
|
||||||
"""设备类型字段定义表(动态字段)"""
|
|
||||||
|
|
||||||
__tablename__ = "device_type_fields"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
device_type_id = Column(BigInteger, ForeignKey("device_types.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
field_code = Column(String(50), nullable=False, comment="字段代码")
|
|
||||||
field_name = Column(String(100), nullable=False, comment="字段名称")
|
|
||||||
field_type = Column(String(20), nullable=False, comment="字段类型: text, number, date, select, multiselect, boolean, textarea")
|
|
||||||
is_required = Column(BigInteger, default=False, nullable=False, comment="是否必填")
|
|
||||||
default_value = Column(Text, nullable=True, comment="默认值")
|
|
||||||
options = Column(JSONB, nullable=True, comment="select类型的选项: [{'label': '选项1', 'value': '1'}]")
|
|
||||||
validation_rules = Column(JSONB, nullable=True, comment="验证规则: {'min': 0, 'max': 100, 'pattern': '^A-Z'}")
|
|
||||||
placeholder = Column(String(200), nullable=True, comment="占位符")
|
|
||||||
help_text = Column(Text, nullable=True, comment="帮助文本")
|
|
||||||
sort_order = Column(Integer, default=0, nullable=False, comment="排序")
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="状态: active, inactive")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
device_type = relationship("DeviceType", back_populates="fields")
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_device_type_fields_type", "device_type_id"),
|
|
||||||
Index("idx_device_type_fields_code", "field_code"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<DeviceTypeField(id={self.id}, field_code={self.field_code}, field_name={self.field_name})>"
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""
|
|
||||||
文件管理数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, DateTime, Text, Index, Boolean, ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedFile(Base):
|
|
||||||
"""上传文件表"""
|
|
||||||
|
|
||||||
__tablename__ = "uploaded_files"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
file_name = Column(String(255), nullable=False, comment="存储文件名(UUID)")
|
|
||||||
original_name = Column(String(255), nullable=False, index=True, comment="原始文件名")
|
|
||||||
file_path = Column(String(500), nullable=False, comment="文件存储路径")
|
|
||||||
file_size = Column(BigInteger, nullable=False, comment="文件大小(字节)")
|
|
||||||
file_type = Column(String(100), nullable=False, index=True, comment="文件类型(MIME)")
|
|
||||||
file_ext = Column(String(50), nullable=False, comment="文件扩展名")
|
|
||||||
uploader_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="上传人ID")
|
|
||||||
upload_time = Column(DateTime, default=datetime.utcnow, nullable=False, index=True, comment="上传时间")
|
|
||||||
thumbnail_path = Column(String(500), nullable=True, comment="缩略图路径")
|
|
||||||
share_code = Column(String(100), nullable=True, unique=True, index=True, comment="分享码")
|
|
||||||
share_expire_time = Column(DateTime, nullable=True, index=True, comment="分享过期时间")
|
|
||||||
download_count = Column(BigInteger, default=0, comment="下载次数")
|
|
||||||
is_deleted = Column(Boolean, default=False, nullable=False, comment="是否删除")
|
|
||||||
deleted_at = Column(DateTime, nullable=True, comment="删除时间")
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="删除人ID")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
uploader = relationship("User", foreign_keys=[uploader_id])
|
|
||||||
deleter = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_uploaded_files_uploader", "uploader_id"),
|
|
||||||
Index("idx_uploaded_files_deleted", "is_deleted"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<UploadedFile(id={self.id}, original_name={self.original_name}, file_type={self.file_type})>"
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
"""
|
|
||||||
维修管理相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime, date
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Date, Numeric, Index
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecord(Base):
|
|
||||||
"""维修记录表"""
|
|
||||||
|
|
||||||
__tablename__ = "maintenance_records"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
record_code = Column(String(50), unique=True, nullable=False, index=True, comment="维修单号")
|
|
||||||
asset_id = Column(BigInteger, ForeignKey("assets.id"), nullable=False, index=True, comment="资产ID")
|
|
||||||
asset_code = Column(String(50), nullable=False, comment="资产编码")
|
|
||||||
fault_description = Column(Text, nullable=False, comment="故障描述")
|
|
||||||
fault_type = Column(String(50), nullable=True, comment="故障类型")
|
|
||||||
report_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="报修人ID")
|
|
||||||
report_time = Column(DateTime, default=datetime.utcnow, nullable=False, index=True, comment="报修时间")
|
|
||||||
priority = Column(String(20), default="normal", nullable=False, comment="优先级")
|
|
||||||
maintenance_type = Column(String(20), nullable=True, comment="维修类型")
|
|
||||||
vendor_id = Column(BigInteger, ForeignKey("suppliers.id"), nullable=True, comment="维修供应商ID")
|
|
||||||
maintenance_cost = Column(Numeric(18, 2), nullable=True, comment="维修费用")
|
|
||||||
start_time = Column(DateTime, nullable=True, comment="开始维修时间")
|
|
||||||
complete_time = Column(DateTime, nullable=True, comment="完成维修时间")
|
|
||||||
maintenance_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="维修人员ID")
|
|
||||||
maintenance_result = Column(Text, nullable=True, comment="维修结果描述")
|
|
||||||
replaced_parts = Column(Text, nullable=True, comment="更换的配件")
|
|
||||||
status = Column(String(20), default="pending", nullable=False, index=True, comment="状态")
|
|
||||||
images = Column(Text, nullable=True, comment="维修图片URL(多个逗号分隔)")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
asset = relationship("Asset")
|
|
||||||
vendor = relationship("Supplier")
|
|
||||||
report_user = relationship("User", foreign_keys=[report_user_id])
|
|
||||||
maintenance_user = relationship("User", foreign_keys=[maintenance_user_id])
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_maintenance_records_code", "record_code"),
|
|
||||||
Index("idx_maintenance_records_asset", "asset_id"),
|
|
||||||
Index("idx_maintenance_records_status", "status"),
|
|
||||||
Index("idx_maintenance_records_time", "report_time"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<MaintenanceRecord(id={self.id}, record_code={self.record_code}, asset_code={self.asset_code})>"
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""
|
|
||||||
消息通知数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Text, Integer, DateTime, Boolean, Index, ForeignKey
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Notification(Base):
|
|
||||||
"""消息通知表"""
|
|
||||||
|
|
||||||
__tablename__ = "notifications"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True, comment="接收人ID")
|
|
||||||
recipient_name = Column(String(100), nullable=False, comment="接收人姓名(冗余)")
|
|
||||||
title = Column(String(200), nullable=False, comment="通知标题")
|
|
||||||
content = Column(Text, nullable=False, comment="通知内容")
|
|
||||||
notification_type = Column(String(20), nullable=False, index=True, comment="通知类型: system/approval/maintenance/allocation等")
|
|
||||||
priority = Column(String(20), default="normal", nullable=False, comment="优先级: low/normal/high/urgent")
|
|
||||||
is_read = Column(Boolean, default=False, nullable=False, index=True, comment="是否已读")
|
|
||||||
read_at = Column(DateTime, nullable=True, comment="已读时间")
|
|
||||||
related_entity_type = Column(String(50), nullable=True, comment="关联实体类型")
|
|
||||||
related_entity_id = Column(BigInteger, nullable=True, comment="关联实体ID")
|
|
||||||
action_url = Column(String(500), nullable=True, comment="操作链接")
|
|
||||||
extra_data = Column(JSONB, nullable=True, comment="额外数据")
|
|
||||||
sent_via_email = Column(Boolean, default=False, nullable=False, comment="是否已发送邮件")
|
|
||||||
sent_via_sms = Column(Boolean, default=False, nullable=False, comment="是否已发送短信")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True, comment="创建时间")
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False, comment="更新时间")
|
|
||||||
expire_at = Column(DateTime, nullable=True, comment="过期时间")
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
recipient = relationship("User", foreign_keys=[recipient_id])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_notification_recipient", "recipient_id"),
|
|
||||||
Index("idx_notification_read", "is_read"),
|
|
||||||
Index("idx_notification_type", "notification_type"),
|
|
||||||
Index("idx_notification_time", "created_at"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Notification(id={self.id}, recipient={self.recipient_name}, title={self.title})>"
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplate(Base):
|
|
||||||
"""消息通知模板表"""
|
|
||||||
|
|
||||||
__tablename__ = "notification_templates"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
template_code = Column(String(50), unique=True, nullable=False, comment="模板编码")
|
|
||||||
template_name = Column(String(200), nullable=False, comment="模板名称")
|
|
||||||
notification_type = Column(String(20), nullable=False, comment="通知类型")
|
|
||||||
title_template = Column(String(200), nullable=False, comment="标题模板")
|
|
||||||
content_template = Column(Text, nullable=False, comment="内容模板")
|
|
||||||
variables = Column(JSONB, nullable=True, comment="变量说明")
|
|
||||||
priority = Column(String(20), default="normal", nullable=False, comment="默认优先级")
|
|
||||||
send_email = Column(Boolean, default=False, nullable=False, comment="是否发送邮件")
|
|
||||||
send_sms = Column(Boolean, default=False, nullable=False, comment="是否发送短信")
|
|
||||||
is_active = Column(Boolean, default=True, nullable=False, comment="是否启用")
|
|
||||||
description = Column(Text, nullable=True, comment="模板描述")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<NotificationTemplate(id={self.id}, code={self.template_code}, name={self.template_name})>"
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
"""
|
|
||||||
操作日志数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Text, Integer, DateTime, Index
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLog(Base):
|
|
||||||
"""操作日志表"""
|
|
||||||
|
|
||||||
__tablename__ = "operation_logs"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
operator_id = Column(BigInteger, nullable=False, index=True, comment="操作人ID")
|
|
||||||
operator_name = Column(String(100), nullable=False, comment="操作人姓名")
|
|
||||||
operator_ip = Column(String(50), nullable=True, comment="操作人IP")
|
|
||||||
module = Column(String(50), nullable=False, index=True, comment="模块名称")
|
|
||||||
operation_type = Column(String(50), nullable=False, index=True, comment="操作类型")
|
|
||||||
method = Column(String(10), nullable=False, comment="请求方法(GET/POST/PUT/DELETE等)")
|
|
||||||
url = Column(String(500), nullable=False, comment="请求URL")
|
|
||||||
params = Column(Text, nullable=True, comment="请求参数")
|
|
||||||
result = Column(String(20), default="success", nullable=False, comment="操作结果: success/failed")
|
|
||||||
error_msg = Column(Text, nullable=True, comment="错误信息")
|
|
||||||
duration = Column(Integer, nullable=True, comment="执行时长(毫秒)")
|
|
||||||
user_agent = Column(String(500), nullable=True, comment="用户代理")
|
|
||||||
extra_data = Column(JSONB, nullable=True, comment="额外数据")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_operation_log_operator", "operator_id"),
|
|
||||||
Index("idx_operation_log_module", "module"),
|
|
||||||
Index("idx_operation_log_time", "created_at"),
|
|
||||||
Index("idx_operation_log_result", "result"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<OperationLog(id={self.id}, operator={self.operator_name}, module={self.module}, operation={self.operation_type})>"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"""
|
|
||||||
机构网点相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Index
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Organization(Base):
|
|
||||||
"""机构/网点表"""
|
|
||||||
|
|
||||||
__tablename__ = "organizations"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
org_code = Column(String(50), unique=True, nullable=False, index=True, comment="机构代码")
|
|
||||||
org_name = Column(String(200), nullable=False, comment="机构名称")
|
|
||||||
org_type = Column(String(20), nullable=False, comment="机构类型: province, city, outlet")
|
|
||||||
parent_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=True, comment="父机构ID")
|
|
||||||
tree_path = Column(String(1000), nullable=True, comment="树形路径: /1/2/3/")
|
|
||||||
tree_level = Column(Integer, default=0, nullable=False, comment="层级")
|
|
||||||
address = Column(String(500), nullable=True, comment="地址")
|
|
||||||
contact_person = Column(String(100), nullable=True, comment="联系人")
|
|
||||||
contact_phone = Column(String(20), nullable=True, comment="联系电话")
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="状态: active, inactive")
|
|
||||||
sort_order = Column(Integer, default=0, nullable=False, comment="排序")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
parent = relationship("Organization", remote_side=[id], foreign_keys=[parent_id])
|
|
||||||
children = relationship("Organization", foreign_keys=[parent_id], backref="children_ref")
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Organization(id={self.id}, org_code={self.org_code}, org_name={self.org_name})>"
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""
|
|
||||||
资产回收相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime, date
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Date, Index
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrder(Base):
|
|
||||||
"""资产回收单表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_recovery_orders"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
order_code = Column(String(50), unique=True, nullable=False, index=True, comment="回收单号")
|
|
||||||
recovery_type = Column(String(20), nullable=False, index=True, comment="回收类型(user=使用人回收/org=机构回收/scrap=报废回收)")
|
|
||||||
title = Column(String(200), nullable=False, comment="标题")
|
|
||||||
asset_count = Column(Integer, default=0, nullable=False, comment="资产数量")
|
|
||||||
apply_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True, comment="申请人ID")
|
|
||||||
apply_time = Column(DateTime, nullable=False, comment="申请时间")
|
|
||||||
approval_status = Column(String(20), default="pending", nullable=False, index=True, comment="审批状态")
|
|
||||||
approval_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="审批人ID")
|
|
||||||
approval_time = Column(DateTime, nullable=True, comment="审批时间")
|
|
||||||
approval_remark = Column(Text, nullable=True, comment="审批备注")
|
|
||||||
execute_status = Column(String(20), default="pending", nullable=False, index=True, comment="执行状态")
|
|
||||||
execute_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="执行人ID")
|
|
||||||
execute_time = Column(DateTime, nullable=True, comment="执行时间")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
apply_user = relationship("User", foreign_keys=[apply_user_id])
|
|
||||||
approval_user = relationship("User", foreign_keys=[approval_user_id])
|
|
||||||
execute_user = relationship("User", foreign_keys=[execute_user_id])
|
|
||||||
items = relationship("AssetRecoveryItem", back_populates="order", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_recovery_orders_code", "order_code"),
|
|
||||||
Index("idx_recovery_orders_type", "recovery_type"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetRecoveryOrder(id={self.id}, order_code={self.order_code}, recovery_type={self.recovery_type})>"
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryItem(Base):
|
|
||||||
"""资产回收单明细表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_recovery_items"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
order_id = Column(BigInteger, ForeignKey("asset_recovery_orders.id", ondelete="CASCADE"), nullable=False, comment="回收单ID")
|
|
||||||
asset_id = Column(BigInteger, ForeignKey("assets.id"), nullable=False, comment="资产ID")
|
|
||||||
asset_code = Column(String(50), nullable=False, comment="资产编码")
|
|
||||||
recovery_status = Column(String(20), default="pending", nullable=False, index=True, comment="回收状态")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
order = relationship("AssetRecoveryOrder", back_populates="items")
|
|
||||||
asset = relationship("Asset")
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_recovery_items_order", "order_id"),
|
|
||||||
Index("idx_recovery_items_asset", "asset_id"),
|
|
||||||
Index("idx_recovery_items_status", "recovery_status"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetRecoveryItem(id={self.id}, order_id={self.order_id}, asset_code={self.asset_code})>"
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
"""
|
|
||||||
系统配置数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Text, Integer, DateTime, Boolean, Index
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfig(Base):
|
|
||||||
"""系统配置表"""
|
|
||||||
|
|
||||||
__tablename__ = "system_configs"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
config_key = Column(String(100), unique=True, nullable=False, index=True, comment="配置键")
|
|
||||||
config_name = Column(String(200), nullable=False, comment="配置名称")
|
|
||||||
config_value = Column(Text, nullable=True, comment="配置值")
|
|
||||||
value_type = Column(String(20), default="string", nullable=False, comment="值类型: string/number/boolean/json")
|
|
||||||
category = Column(String(50), nullable=False, index=True, comment="配置分类")
|
|
||||||
description = Column(Text, nullable=True, comment="配置描述")
|
|
||||||
is_system = Column(Boolean, default=False, nullable=False, comment="是否系统配置")
|
|
||||||
is_encrypted = Column(Boolean, default=False, nullable=False, comment="是否加密存储")
|
|
||||||
validation_rule = Column(Text, nullable=True, comment="验证规则(JSON)")
|
|
||||||
options = Column(JSONB, nullable=True, comment="可选值配置")
|
|
||||||
default_value = Column(Text, nullable=True, comment="默认值")
|
|
||||||
sort_order = Column(Integer, default=0, nullable=False, comment="排序序号")
|
|
||||||
is_active = Column(Boolean, default=True, nullable=False, comment="是否启用")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
updated_by = Column(BigInteger, nullable=True, comment="更新人ID")
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_system_config_category", "category"),
|
|
||||||
Index("idx_system_config_active", "is_active"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<SystemConfig(id={self.id}, config_key={self.config_key}, config_name={self.config_name})>"
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
"""
|
|
||||||
资产调拨相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime, date
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, ForeignKey, DateTime, Date, Index
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrder(Base):
|
|
||||||
"""资产调拨单表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_transfer_orders"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
order_code = Column(String(50), unique=True, nullable=False, index=True, comment="调拨单号")
|
|
||||||
source_org_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=False, comment="调出网点ID")
|
|
||||||
target_org_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=False, index=True, comment="调入网点ID")
|
|
||||||
transfer_type = Column(String(20), nullable=False, index=True, comment="调拨类型(internal/external)")
|
|
||||||
title = Column(String(200), nullable=False, comment="标题")
|
|
||||||
asset_count = Column(Integer, default=0, nullable=False, comment="资产数量")
|
|
||||||
apply_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True, comment="申请人ID")
|
|
||||||
apply_time = Column(DateTime, nullable=False, comment="申请时间")
|
|
||||||
approval_status = Column(String(20), default="pending", nullable=False, index=True, comment="审批状态")
|
|
||||||
approval_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="审批人ID")
|
|
||||||
approval_time = Column(DateTime, nullable=True, comment="审批时间")
|
|
||||||
approval_remark = Column(Text, nullable=True, comment="审批备注")
|
|
||||||
execute_status = Column(String(20), default="pending", nullable=False, index=True, comment="执行状态")
|
|
||||||
execute_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True, comment="执行人ID")
|
|
||||||
execute_time = Column(DateTime, nullable=True, comment="执行时间")
|
|
||||||
remark = Column(Text, nullable=True, comment="备注")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
source_organization = relationship("Organization", foreign_keys=[source_org_id])
|
|
||||||
target_organization = relationship("Organization", foreign_keys=[target_org_id])
|
|
||||||
apply_user = relationship("User", foreign_keys=[apply_user_id])
|
|
||||||
approval_user = relationship("User", foreign_keys=[approval_user_id])
|
|
||||||
execute_user = relationship("User", foreign_keys=[execute_user_id])
|
|
||||||
items = relationship("AssetTransferItem", back_populates="order", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_transfer_orders_code", "order_code"),
|
|
||||||
Index("idx_transfer_orders_source_org", "source_org_id"),
|
|
||||||
Index("idx_transfer_orders_target_org", "target_org_id"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetTransferOrder(id={self.id}, order_code={self.order_code}, transfer_type={self.transfer_type})>"
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferItem(Base):
|
|
||||||
"""资产调拨单明细表"""
|
|
||||||
|
|
||||||
__tablename__ = "asset_transfer_items"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
order_id = Column(BigInteger, ForeignKey("asset_transfer_orders.id", ondelete="CASCADE"), nullable=False, comment="调拨单ID")
|
|
||||||
asset_id = Column(BigInteger, ForeignKey("assets.id"), nullable=False, comment="资产ID")
|
|
||||||
asset_code = Column(String(50), nullable=False, comment="资产编码")
|
|
||||||
source_organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=False, comment="调出网点ID")
|
|
||||||
target_organization_id = Column(BigInteger, ForeignKey("organizations.id"), nullable=False, comment="调入网点ID")
|
|
||||||
transfer_status = Column(String(20), default="pending", nullable=False, index=True, comment="调拨状态")
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
order = relationship("AssetTransferOrder", back_populates="items")
|
|
||||||
asset = relationship("Asset")
|
|
||||||
source_organization = relationship("Organization", foreign_keys=[source_organization_id])
|
|
||||||
target_organization = relationship("Organization", foreign_keys=[target_organization_id])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_transfer_items_order", "order_id"),
|
|
||||||
Index("idx_transfer_items_asset", "asset_id"),
|
|
||||||
Index("idx_transfer_items_status", "transfer_status"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<AssetTransferItem(id={self.id}, order_id={self.order_id}, asset_code={self.asset_code})>"
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
"""
|
|
||||||
用户相关数据模型
|
|
||||||
"""
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import Column, BigInteger, String, Boolean, DateTime, Integer, ForeignKey, Text, Index
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
"""用户表"""
|
|
||||||
|
|
||||||
__tablename__ = "users"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
username = Column(String(50), unique=True, nullable=False, index=True)
|
|
||||||
password_hash = Column(String(255), nullable=False, comment="bcrypt哈希")
|
|
||||||
real_name = Column(String(100), nullable=False)
|
|
||||||
email = Column(String(255), unique=True, nullable=True)
|
|
||||||
phone = Column(String(20), nullable=True)
|
|
||||||
avatar_url = Column(String(500), nullable=True)
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="active, disabled, locked")
|
|
||||||
is_admin = Column(Boolean, default=False, nullable=False)
|
|
||||||
last_login_at = Column(DateTime, nullable=True)
|
|
||||||
login_fail_count = Column(Integer, default=0, nullable=False)
|
|
||||||
locked_until = Column(DateTime, nullable=True)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
created_by_user = relationship("User", remote_side=[id], foreign_keys=[created_by])
|
|
||||||
updated_by_user = relationship("User", remote_side=[id], foreign_keys=[updated_by])
|
|
||||||
deleted_by_user = relationship("User", remote_side=[id], foreign_keys=[deleted_by])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<User(id={self.id}, username={self.username}, real_name={self.real_name})>"
|
|
||||||
|
|
||||||
|
|
||||||
class Role(Base):
|
|
||||||
"""角色表"""
|
|
||||||
|
|
||||||
__tablename__ = "roles"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
role_name = Column(String(50), unique=True, nullable=False)
|
|
||||||
role_code = Column(String(50), unique=True, nullable=False)
|
|
||||||
description = Column(Text, nullable=True)
|
|
||||||
status = Column(String(20), default="active", nullable=False, comment="active, disabled")
|
|
||||||
sort_order = Column(Integer, default=0, nullable=False)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
deleted_at = Column(DateTime, nullable=True)
|
|
||||||
deleted_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
updated_user = relationship("User", foreign_keys=[updated_by])
|
|
||||||
deleted_user = relationship("User", foreign_keys=[deleted_by])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Role(id={self.id}, role_code={self.role_code}, role_name={self.role_name})>"
|
|
||||||
|
|
||||||
|
|
||||||
class UserRole(Base):
|
|
||||||
"""用户角色关联表"""
|
|
||||||
|
|
||||||
__tablename__ = "user_roles"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
user_id = Column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
role_id = Column(BigInteger, ForeignKey("roles.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
user = relationship("User", foreign_keys=[user_id])
|
|
||||||
role = relationship("Role", foreign_keys=[role_id])
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_user_roles_user", "user_id"),
|
|
||||||
Index("idx_user_roles_role", "role_id"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Permission(Base):
|
|
||||||
"""权限表"""
|
|
||||||
|
|
||||||
__tablename__ = "permissions"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
permission_name = Column(String(100), unique=True, nullable=False)
|
|
||||||
permission_code = Column(String(100), unique=True, nullable=False)
|
|
||||||
module = Column(String(50), nullable=False, comment="模块: asset, device_type, org, user, system")
|
|
||||||
resource = Column(String(50), nullable=True, comment="资源: asset, device_type, organization")
|
|
||||||
action = Column(String(50), nullable=True, comment="操作: create, read, update, delete, export, import")
|
|
||||||
description = Column(Text, nullable=True)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Permission(id={self.id}, permission_code={self.permission_code}, permission_name={self.permission_name})>"
|
|
||||||
|
|
||||||
|
|
||||||
class RolePermission(Base):
|
|
||||||
"""角色权限关联表"""
|
|
||||||
|
|
||||||
__tablename__ = "role_permissions"
|
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
|
||||||
role_id = Column(BigInteger, ForeignKey("roles.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
permission_id = Column(BigInteger, ForeignKey("permissions.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
created_by = Column(BigInteger, ForeignKey("users.id"), nullable=True)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
role = relationship("Role", foreign_keys=[role_id])
|
|
||||||
permission = relationship("Permission", foreign_keys=[permission_id])
|
|
||||||
created_user = relationship("User", foreign_keys=[created_by])
|
|
||||||
|
|
||||||
# 索引
|
|
||||||
__table_args__ = (
|
|
||||||
Index("idx_role_permissions_role", "role_id"),
|
|
||||||
Index("idx_role_permissions_permission", "permission_id"),
|
|
||||||
)
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
"""
|
|
||||||
资产分配相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime, date
|
|
||||||
from decimal import Decimal
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 分配单Schema =====
|
|
||||||
|
|
||||||
class AllocationOrderBase(BaseModel):
|
|
||||||
"""分配单基础Schema"""
|
|
||||||
order_type: str = Field(..., description="单据类型(allocation/transfer/recovery/maintenance/scrap)")
|
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="标题")
|
|
||||||
source_organization_id: Optional[int] = Field(None, gt=0, description="调出网点ID")
|
|
||||||
target_organization_id: int = Field(..., gt=0, description="调入网点ID")
|
|
||||||
expect_execute_date: Optional[date] = Field(None, description="预计执行日期")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderCreate(AllocationOrderBase):
|
|
||||||
"""创建分配单Schema"""
|
|
||||||
asset_ids: List[int] = Field(..., min_items=1, description="资产ID列表")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderUpdate(BaseModel):
|
|
||||||
"""更新分配单Schema"""
|
|
||||||
title: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
expect_execute_date: Optional[date] = None
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderApproval(BaseModel):
|
|
||||||
"""分配单审批Schema"""
|
|
||||||
approval_status: str = Field(..., description="审批状态(approved/rejected)")
|
|
||||||
approval_remark: Optional[str] = Field(None, description="审批备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderExecute(BaseModel):
|
|
||||||
"""分配单执行Schema"""
|
|
||||||
remark: Optional[str] = Field(None, description="执行备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderInDB(BaseModel):
|
|
||||||
"""数据库中的分配单Schema"""
|
|
||||||
id: int
|
|
||||||
order_code: str
|
|
||||||
order_type: str
|
|
||||||
title: str
|
|
||||||
source_organization_id: Optional[int]
|
|
||||||
target_organization_id: int
|
|
||||||
applicant_id: int
|
|
||||||
approver_id: Optional[int]
|
|
||||||
approval_status: str
|
|
||||||
approval_time: Optional[datetime]
|
|
||||||
approval_remark: Optional[str]
|
|
||||||
expect_execute_date: Optional[date]
|
|
||||||
actual_execute_date: Optional[date]
|
|
||||||
executor_id: Optional[int]
|
|
||||||
execute_status: str
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderResponse(AllocationOrderInDB):
|
|
||||||
"""分配单响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderWithRelations(AllocationOrderResponse):
|
|
||||||
"""带关联信息的分配单响应Schema"""
|
|
||||||
source_organization: Optional[Dict[str, Any]] = None
|
|
||||||
target_organization: Optional[Dict[str, Any]] = None
|
|
||||||
applicant: Optional[Dict[str, Any]] = None
|
|
||||||
approver: Optional[Dict[str, Any]] = None
|
|
||||||
executor: Optional[Dict[str, Any]] = None
|
|
||||||
items: Optional[List[Dict[str, Any]]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationOrderListResponse(BaseModel):
|
|
||||||
"""分配单列表响应Schema"""
|
|
||||||
total: int
|
|
||||||
page: int
|
|
||||||
page_size: int
|
|
||||||
total_pages: int
|
|
||||||
items: List[AllocationOrderWithRelations]
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 分配单明细Schema =====
|
|
||||||
|
|
||||||
class AllocationItemBase(BaseModel):
|
|
||||||
"""分配单明细基础Schema"""
|
|
||||||
asset_id: int = Field(..., gt=0, description="资产ID")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationItemInDB(BaseModel):
|
|
||||||
"""数据库中的分配单明细Schema"""
|
|
||||||
id: int
|
|
||||||
order_id: int
|
|
||||||
asset_id: int
|
|
||||||
asset_code: str
|
|
||||||
asset_name: str
|
|
||||||
from_organization_id: Optional[int]
|
|
||||||
to_organization_id: Optional[int]
|
|
||||||
from_status: Optional[str]
|
|
||||||
to_status: Optional[str]
|
|
||||||
execute_status: str
|
|
||||||
execute_time: Optional[datetime]
|
|
||||||
failure_reason: Optional[str]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationItemResponse(AllocationItemInDB):
|
|
||||||
"""分配单明细响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 查询参数Schema =====
|
|
||||||
|
|
||||||
class AllocationOrderQueryParams(BaseModel):
|
|
||||||
"""分配单查询参数"""
|
|
||||||
order_type: Optional[str] = Field(None, description="单据类型")
|
|
||||||
approval_status: Optional[str] = Field(None, description="审批状态")
|
|
||||||
execute_status: Optional[str] = Field(None, description="执行状态")
|
|
||||||
applicant_id: Optional[int] = Field(None, gt=0, description="申请人ID")
|
|
||||||
target_organization_id: Optional[int] = Field(None, gt=0, description="目标网点ID")
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 统计Schema =====
|
|
||||||
|
|
||||||
class AllocationOrderStatistics(BaseModel):
|
|
||||||
"""分配单统计Schema"""
|
|
||||||
total: int = Field(..., description="总数")
|
|
||||||
pending: int = Field(..., description="待审批数")
|
|
||||||
approved: int = Field(..., description="已审批数")
|
|
||||||
rejected: int = Field(..., description="已拒绝数")
|
|
||||||
executing: int = Field(..., description="执行中数")
|
|
||||||
completed: int = Field(..., description="已完成数")
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
"""
|
|
||||||
资产相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime, date
|
|
||||||
from decimal import Decimal
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 资产Schema =====
|
|
||||||
|
|
||||||
class AssetBase(BaseModel):
|
|
||||||
"""资产基础Schema"""
|
|
||||||
asset_name: str = Field(..., min_length=1, max_length=200, description="资产名称")
|
|
||||||
device_type_id: int = Field(..., gt=0, description="设备类型ID")
|
|
||||||
brand_id: Optional[int] = Field(None, gt=0, description="品牌ID")
|
|
||||||
model: Optional[str] = Field(None, max_length=200, description="规格型号")
|
|
||||||
serial_number: Optional[str] = Field(None, max_length=200, description="序列号")
|
|
||||||
supplier_id: Optional[int] = Field(None, gt=0, description="供应商ID")
|
|
||||||
purchase_date: Optional[date] = Field(None, description="采购日期")
|
|
||||||
purchase_price: Optional[Decimal] = Field(None, ge=0, description="采购价格")
|
|
||||||
warranty_period: Optional[int] = Field(None, ge=0, description="保修期(月)")
|
|
||||||
organization_id: int = Field(..., gt=0, description="所属网点ID")
|
|
||||||
location: Optional[str] = Field(None, max_length=500, description="存放位置")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetCreate(AssetBase):
|
|
||||||
"""创建资产Schema"""
|
|
||||||
dynamic_attributes: Dict[str, Any] = Field(default_factory=dict, description="动态字段值")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetUpdate(BaseModel):
|
|
||||||
"""更新资产Schema"""
|
|
||||||
asset_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
brand_id: Optional[int] = Field(None, gt=0)
|
|
||||||
model: Optional[str] = Field(None, max_length=200)
|
|
||||||
serial_number: Optional[str] = Field(None, max_length=200)
|
|
||||||
supplier_id: Optional[int] = Field(None, gt=0)
|
|
||||||
purchase_date: Optional[date] = None
|
|
||||||
purchase_price: Optional[Decimal] = Field(None, ge=0)
|
|
||||||
warranty_period: Optional[int] = Field(None, ge=0)
|
|
||||||
warranty_expire_date: Optional[date] = None
|
|
||||||
organization_id: Optional[int] = Field(None, gt=0)
|
|
||||||
location: Optional[str] = Field(None, max_length=500)
|
|
||||||
dynamic_attributes: Optional[Dict[str, Any]] = None
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AssetInDB(BaseModel):
|
|
||||||
"""数据库中的资产Schema"""
|
|
||||||
id: int
|
|
||||||
asset_code: str
|
|
||||||
asset_name: str
|
|
||||||
device_type_id: int
|
|
||||||
brand_id: Optional[int]
|
|
||||||
model: Optional[str]
|
|
||||||
serial_number: Optional[str]
|
|
||||||
supplier_id: Optional[int]
|
|
||||||
purchase_date: Optional[date]
|
|
||||||
purchase_price: Optional[Decimal]
|
|
||||||
warranty_period: Optional[int]
|
|
||||||
warranty_expire_date: Optional[date]
|
|
||||||
organization_id: int
|
|
||||||
location: Optional[str]
|
|
||||||
status: str
|
|
||||||
dynamic_attributes: Dict[str, Any]
|
|
||||||
qr_code_url: Optional[str]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AssetResponse(AssetInDB):
|
|
||||||
"""资产响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AssetWithRelations(AssetResponse):
|
|
||||||
"""带关联信息的资产响应Schema"""
|
|
||||||
device_type: Optional[Dict[str, Any]] = None
|
|
||||||
brand: Optional[Dict[str, Any]] = None
|
|
||||||
supplier: Optional[Dict[str, Any]] = None
|
|
||||||
organization: Optional[Dict[str, Any]] = None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 资产状态历史Schema =====
|
|
||||||
|
|
||||||
class AssetStatusHistoryBase(BaseModel):
|
|
||||||
"""资产状态历史基础Schema"""
|
|
||||||
old_status: Optional[str] = Field(None, description="原状态")
|
|
||||||
new_status: str = Field(..., description="新状态")
|
|
||||||
operation_type: str = Field(..., description="操作类型")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetStatusHistoryInDB(BaseModel):
|
|
||||||
"""数据库中的资产状态历史Schema"""
|
|
||||||
id: int
|
|
||||||
asset_id: int
|
|
||||||
old_status: Optional[str]
|
|
||||||
new_status: str
|
|
||||||
operation_type: str
|
|
||||||
operator_id: int
|
|
||||||
operator_name: Optional[str]
|
|
||||||
organization_id: Optional[int]
|
|
||||||
remark: Optional[str]
|
|
||||||
extra_data: Optional[Dict[str, Any]]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AssetStatusHistoryResponse(AssetStatusHistoryInDB):
|
|
||||||
"""资产状态历史响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 批量操作Schema =====
|
|
||||||
|
|
||||||
class AssetBatchImport(BaseModel):
|
|
||||||
"""批量导入Schema"""
|
|
||||||
file_path: str = Field(..., description="Excel文件路径")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetBatchImportResult(BaseModel):
|
|
||||||
"""批量导入结果Schema"""
|
|
||||||
total: int = Field(..., description="总数")
|
|
||||||
success: int = Field(..., description="成功数")
|
|
||||||
failed: int = Field(..., description="失败数")
|
|
||||||
errors: List[Dict[str, Any]] = Field(default_factory=list, description="错误列表")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetBatchDelete(BaseModel):
|
|
||||||
"""批量删除Schema"""
|
|
||||||
asset_ids: List[int] = Field(..., min_items=1, description="资产ID列表")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 查询参数Schema =====
|
|
||||||
|
|
||||||
class AssetQueryParams(BaseModel):
|
|
||||||
"""资产查询参数"""
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
device_type_id: Optional[int] = Field(None, gt=0, description="设备类型ID")
|
|
||||||
organization_id: Optional[int] = Field(None, gt=0, description="网点ID")
|
|
||||||
status: Optional[str] = Field(None, description="状态")
|
|
||||||
purchase_date_start: Optional[date] = Field(None, description="采购日期开始")
|
|
||||||
purchase_date_end: Optional[date] = Field(None, description="采购日期结束")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 状态转换Schema =====
|
|
||||||
|
|
||||||
class AssetStatusTransition(BaseModel):
|
|
||||||
"""资产状态转换Schema"""
|
|
||||||
new_status: str = Field(..., description="目标状态")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
extra_data: Optional[Dict[str, Any]] = Field(None, description="额外数据")
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
"""
|
|
||||||
品牌和供应商相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field, EmailStr
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 品牌Schema =====
|
|
||||||
|
|
||||||
class BrandBase(BaseModel):
|
|
||||||
"""品牌基础Schema"""
|
|
||||||
brand_code: str = Field(..., min_length=1, max_length=50, description="品牌代码")
|
|
||||||
brand_name: str = Field(..., min_length=1, max_length=200, description="品牌名称")
|
|
||||||
logo_url: Optional[str] = Field(None, max_length=500, description="Logo URL")
|
|
||||||
website: Optional[str] = Field(None, max_length=500, description="官网地址")
|
|
||||||
sort_order: int = Field(default=0, description="排序")
|
|
||||||
|
|
||||||
|
|
||||||
class BrandCreate(BrandBase):
|
|
||||||
"""创建品牌Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BrandUpdate(BaseModel):
|
|
||||||
"""更新品牌Schema"""
|
|
||||||
brand_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
logo_url: Optional[str] = Field(None, max_length=500)
|
|
||||||
website: Optional[str] = Field(None, max_length=500)
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|inactive)$")
|
|
||||||
sort_order: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class BrandInDB(BaseModel):
|
|
||||||
"""数据库中的品牌Schema"""
|
|
||||||
id: int
|
|
||||||
brand_code: str
|
|
||||||
brand_name: str
|
|
||||||
logo_url: Optional[str]
|
|
||||||
website: Optional[str]
|
|
||||||
status: str
|
|
||||||
sort_order: int
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class BrandResponse(BrandInDB):
|
|
||||||
"""品牌响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 供应商Schema =====
|
|
||||||
|
|
||||||
class SupplierBase(BaseModel):
|
|
||||||
"""供应商基础Schema"""
|
|
||||||
supplier_code: str = Field(..., min_length=1, max_length=50, description="供应商代码")
|
|
||||||
supplier_name: str = Field(..., min_length=1, max_length=200, description="供应商名称")
|
|
||||||
contact_person: Optional[str] = Field(None, max_length=100, description="联系人")
|
|
||||||
contact_phone: Optional[str] = Field(None, max_length=20, description="联系电话")
|
|
||||||
email: Optional[EmailStr] = Field(None, description="邮箱")
|
|
||||||
address: Optional[str] = Field(None, max_length=500, description="地址")
|
|
||||||
credit_code: Optional[str] = Field(None, max_length=50, description="统一社会信用代码")
|
|
||||||
bank_name: Optional[str] = Field(None, max_length=200, description="开户银行")
|
|
||||||
bank_account: Optional[str] = Field(None, max_length=100, description="银行账号")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierCreate(SupplierBase):
|
|
||||||
"""创建供应商Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierUpdate(BaseModel):
|
|
||||||
"""更新供应商Schema"""
|
|
||||||
supplier_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
contact_person: Optional[str] = Field(None, max_length=100)
|
|
||||||
contact_phone: Optional[str] = Field(None, max_length=20)
|
|
||||||
email: Optional[EmailStr] = None
|
|
||||||
address: Optional[str] = Field(None, max_length=500)
|
|
||||||
credit_code: Optional[str] = Field(None, max_length=50)
|
|
||||||
bank_name: Optional[str] = Field(None, max_length=200)
|
|
||||||
bank_account: Optional[str] = Field(None, max_length=100)
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|inactive)$")
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierInDB(BaseModel):
|
|
||||||
"""数据库中的供应商Schema"""
|
|
||||||
id: int
|
|
||||||
supplier_code: str
|
|
||||||
supplier_name: str
|
|
||||||
contact_person: Optional[str]
|
|
||||||
contact_phone: Optional[str]
|
|
||||||
email: Optional[str]
|
|
||||||
address: Optional[str]
|
|
||||||
credit_code: Optional[str]
|
|
||||||
bank_name: Optional[str]
|
|
||||||
bank_account: Optional[str]
|
|
||||||
status: str
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierResponse(SupplierInDB):
|
|
||||||
"""供应商响应Schema"""
|
|
||||||
pass
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
"""
|
|
||||||
设备类型相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 设备类型Schema =====
|
|
||||||
|
|
||||||
class DeviceTypeBase(BaseModel):
|
|
||||||
"""设备类型基础Schema"""
|
|
||||||
type_code: str = Field(..., min_length=1, max_length=50, description="设备类型代码")
|
|
||||||
type_name: str = Field(..., min_length=1, max_length=200, description="设备类型名称")
|
|
||||||
category: Optional[str] = Field(None, max_length=50, description="设备分类")
|
|
||||||
description: Optional[str] = Field(None, description="描述")
|
|
||||||
icon: Optional[str] = Field(None, max_length=100, description="图标名称")
|
|
||||||
sort_order: int = Field(default=0, description="排序")
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeCreate(DeviceTypeBase):
|
|
||||||
"""创建设备类型Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeUpdate(BaseModel):
|
|
||||||
"""更新设备类型Schema"""
|
|
||||||
type_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
category: Optional[str] = Field(None, max_length=50)
|
|
||||||
description: Optional[str] = None
|
|
||||||
icon: Optional[str] = Field(None, max_length=100)
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|inactive)$")
|
|
||||||
sort_order: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeInDB(BaseModel):
|
|
||||||
"""数据库中的设备类型Schema"""
|
|
||||||
id: int
|
|
||||||
type_code: str
|
|
||||||
type_name: str
|
|
||||||
category: Optional[str]
|
|
||||||
description: Optional[str]
|
|
||||||
icon: Optional[str]
|
|
||||||
status: str
|
|
||||||
sort_order: int
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeResponse(DeviceTypeInDB):
|
|
||||||
"""设备类型响应Schema"""
|
|
||||||
field_count: int = Field(default=0, description="字段数量")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeWithFields(DeviceTypeResponse):
|
|
||||||
"""带字段列表的设备类型响应Schema"""
|
|
||||||
fields: List["DeviceTypeFieldResponse"] = Field(default_factory=list, description="字段列表")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 设备类型字段Schema =====
|
|
||||||
|
|
||||||
class DeviceTypeFieldBase(BaseModel):
|
|
||||||
"""设备类型字段基础Schema"""
|
|
||||||
field_code: str = Field(..., min_length=1, max_length=50, description="字段代码")
|
|
||||||
field_name: str = Field(..., min_length=1, max_length=100, description="字段名称")
|
|
||||||
field_type: str = Field(..., pattern="^(text|number|date|select|multiselect|boolean|textarea)$", description="字段类型")
|
|
||||||
is_required: bool = Field(default=False, description="是否必填")
|
|
||||||
default_value: Optional[str] = Field(None, description="默认值")
|
|
||||||
placeholder: Optional[str] = Field(None, max_length=200, description="占位符")
|
|
||||||
help_text: Optional[str] = Field(None, description="帮助文本")
|
|
||||||
sort_order: int = Field(default=0, description="排序")
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFieldCreate(DeviceTypeFieldBase):
|
|
||||||
"""创建设备类型字段Schema"""
|
|
||||||
options: Optional[List[Dict[str, Any]]] = Field(None, description="选项列表(用于select/multiselect类型)")
|
|
||||||
validation_rules: Optional[Dict[str, Any]] = Field(None, description="验证规则")
|
|
||||||
|
|
||||||
@field_validator("field_type")
|
|
||||||
@classmethod
|
|
||||||
def validate_field_type(cls, v: str) -> str:
|
|
||||||
"""验证字段类型"""
|
|
||||||
valid_types = ["text", "number", "date", "select", "multiselect", "boolean", "textarea"]
|
|
||||||
if v not in valid_types:
|
|
||||||
raise ValueError(f"字段类型必须是以下之一: {', '.join(valid_types)}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFieldUpdate(BaseModel):
|
|
||||||
"""更新设备类型字段Schema"""
|
|
||||||
field_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
||||||
field_type: Optional[str] = Field(None, pattern="^(text|number|date|select|multiselect|boolean|textarea)$")
|
|
||||||
is_required: Optional[bool] = None
|
|
||||||
default_value: Optional[str] = None
|
|
||||||
options: Optional[List[Dict[str, Any]]] = None
|
|
||||||
validation_rules: Optional[Dict[str, Any]] = None
|
|
||||||
placeholder: Optional[str] = Field(None, max_length=200)
|
|
||||||
help_text: Optional[str] = None
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|inactive)$")
|
|
||||||
sort_order: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFieldInDB(BaseModel):
|
|
||||||
"""数据库中的设备类型字段Schema"""
|
|
||||||
id: int
|
|
||||||
device_type_id: int
|
|
||||||
field_code: str
|
|
||||||
field_name: str
|
|
||||||
field_type: str
|
|
||||||
is_required: bool
|
|
||||||
default_value: Optional[str]
|
|
||||||
options: Optional[List[Dict[str, Any]]]
|
|
||||||
validation_rules: Optional[Dict[str, Any]]
|
|
||||||
placeholder: Optional[str]
|
|
||||||
help_text: Optional[str]
|
|
||||||
sort_order: int
|
|
||||||
status: str
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFieldResponse(DeviceTypeFieldInDB):
|
|
||||||
"""设备类型字段响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 查询参数Schema =====
|
|
||||||
|
|
||||||
class DeviceTypeQueryParams(BaseModel):
|
|
||||||
"""设备类型查询参数"""
|
|
||||||
category: Optional[str] = Field(None, description="设备分类")
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|inactive)$", description="状态")
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
# 更新前向引用
|
|
||||||
DeviceTypeWithFields.model_rebuild()
|
|
||||||
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
"""
|
|
||||||
文件管理相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 文件Schema =====
|
|
||||||
|
|
||||||
class UploadedFileBase(BaseModel):
|
|
||||||
"""上传文件基础Schema"""
|
|
||||||
original_name: str = Field(..., min_length=1, max_length=255, description="原始文件名")
|
|
||||||
file_size: int = Field(..., gt=0, description="文件大小(字节)")
|
|
||||||
file_type: str = Field(..., description="文件类型(MIME)")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedFileCreate(UploadedFileBase):
|
|
||||||
"""创建文件记录Schema"""
|
|
||||||
file_name: str = Field(..., description="存储文件名")
|
|
||||||
file_path: str = Field(..., description="文件存储路径")
|
|
||||||
file_ext: str = Field(..., description="文件扩展名")
|
|
||||||
uploader_id: int = Field(..., gt=0, description="上传者ID")
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedFileUpdate(BaseModel):
|
|
||||||
"""更新文件记录Schema"""
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedFileInDB(BaseModel):
|
|
||||||
"""数据库中的文件Schema"""
|
|
||||||
id: int
|
|
||||||
file_name: str
|
|
||||||
original_name: str
|
|
||||||
file_path: str
|
|
||||||
file_size: int
|
|
||||||
file_type: str
|
|
||||||
file_ext: str
|
|
||||||
uploader_id: int
|
|
||||||
upload_time: datetime
|
|
||||||
thumbnail_path: Optional[str]
|
|
||||||
share_code: Optional[str]
|
|
||||||
share_expire_time: Optional[datetime]
|
|
||||||
download_count: int
|
|
||||||
is_deleted: int
|
|
||||||
deleted_at: Optional[datetime]
|
|
||||||
deleted_by: Optional[int]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedFileResponse(UploadedFileInDB):
|
|
||||||
"""文件响应Schema"""
|
|
||||||
uploader_name: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class UploadedFileWithUrl(UploadedFileResponse):
|
|
||||||
"""带访问URL的文件响应Schema"""
|
|
||||||
download_url: Optional[str] = None
|
|
||||||
preview_url: Optional[str] = None
|
|
||||||
share_url: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 文件上传Schema =====
|
|
||||||
|
|
||||||
class FileUploadResponse(BaseModel):
|
|
||||||
"""文件上传响应Schema"""
|
|
||||||
id: int
|
|
||||||
file_name: str
|
|
||||||
original_name: str
|
|
||||||
file_size: int
|
|
||||||
file_type: str
|
|
||||||
file_path: str
|
|
||||||
download_url: str
|
|
||||||
preview_url: Optional[str] = None
|
|
||||||
message: str = "上传成功"
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 文件分享Schema =====
|
|
||||||
|
|
||||||
class FileShareCreate(BaseModel):
|
|
||||||
"""创建文件分享Schema"""
|
|
||||||
expire_days: int = Field(default=7, ge=1, le=30, description="有效期(天)")
|
|
||||||
|
|
||||||
|
|
||||||
class FileShareResponse(BaseModel):
|
|
||||||
"""文件分享响应Schema"""
|
|
||||||
share_code: str
|
|
||||||
share_url: str
|
|
||||||
expire_time: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class FileShareVerify(BaseModel):
|
|
||||||
"""验证分享码Schema"""
|
|
||||||
share_code: str = Field(..., description="分享码")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 批量操作Schema =====
|
|
||||||
|
|
||||||
class FileBatchDelete(BaseModel):
|
|
||||||
"""批量删除文件Schema"""
|
|
||||||
file_ids: List[int] = Field(..., min_items=1, description="文件ID列表")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 查询参数Schema =====
|
|
||||||
|
|
||||||
class FileQueryParams(BaseModel):
|
|
||||||
"""文件查询参数"""
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
file_type: Optional[str] = Field(None, description="文件类型")
|
|
||||||
uploader_id: Optional[int] = Field(None, gt=0, description="上传者ID")
|
|
||||||
start_date: Optional[str] = Field(None, description="开始日期(YYYY-MM-DD)")
|
|
||||||
end_date: Optional[str] = Field(None, description="结束日期(YYYY-MM-DD)")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 统计Schema =====
|
|
||||||
|
|
||||||
class FileStatistics(BaseModel):
|
|
||||||
"""文件统计Schema"""
|
|
||||||
total_files: int = Field(..., description="总文件数")
|
|
||||||
total_size: int = Field(..., description="总大小(字节)")
|
|
||||||
total_size_human: str = Field(..., description="总大小(人类可读)")
|
|
||||||
type_distribution: Dict[str, int] = Field(default_factory=dict, description="文件类型分布")
|
|
||||||
upload_today: int = Field(..., description="今日上传数")
|
|
||||||
upload_this_week: int = Field(..., description="本周上传数")
|
|
||||||
upload_this_month: int = Field(..., description="本月上传数")
|
|
||||||
top_uploaders: List[Dict[str, Any]] = Field(default_factory=list, description="上传排行")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 分片上传Schema =====
|
|
||||||
|
|
||||||
class ChunkUploadInit(BaseModel):
|
|
||||||
"""初始化分片上传Schema"""
|
|
||||||
file_name: str = Field(..., description="文件名")
|
|
||||||
file_size: int = Field(..., gt=0, description="文件大小")
|
|
||||||
file_type: str = Field(..., description="文件类型")
|
|
||||||
total_chunks: int = Field(..., gt=0, description="总分片数")
|
|
||||||
file_hash: Optional[str] = Field(None, description="文件哈希(MD5/SHA256)")
|
|
||||||
|
|
||||||
|
|
||||||
class ChunkUploadInfo(BaseModel):
|
|
||||||
"""分片上传信息Schema"""
|
|
||||||
upload_id: str = Field(..., description="上传ID")
|
|
||||||
chunk_index: int = Field(..., ge=0, description="分片索引")
|
|
||||||
|
|
||||||
|
|
||||||
class ChunkUploadComplete(BaseModel):
|
|
||||||
"""完成分片上传Schema"""
|
|
||||||
upload_id: str = Field(..., description="上传ID")
|
|
||||||
file_name: str = Field(..., description="文件名")
|
|
||||||
file_hash: Optional[str] = Field(None, description="文件哈希")
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
"""
|
|
||||||
维修管理相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime, date
|
|
||||||
from decimal import Decimal
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 维修记录Schema =====
|
|
||||||
|
|
||||||
class MaintenanceRecordBase(BaseModel):
|
|
||||||
"""维修记录基础Schema"""
|
|
||||||
asset_id: int = Field(..., gt=0, description="资产ID")
|
|
||||||
fault_description: str = Field(..., min_length=1, description="故障描述")
|
|
||||||
fault_type: Optional[str] = Field(None, description="故障类型(hardware/software/network/other)")
|
|
||||||
priority: str = Field(default="normal", description="优先级(low/normal/high/urgent)")
|
|
||||||
maintenance_type: Optional[str] = Field(None, description="维修类型(self_repair/vendor_repair/warranty)")
|
|
||||||
vendor_id: Optional[int] = Field(None, gt=0, description="维修供应商ID")
|
|
||||||
maintenance_cost: Optional[Decimal] = Field(None, ge=0, description="维修费用")
|
|
||||||
maintenance_result: Optional[str] = Field(None, description="维修结果描述")
|
|
||||||
replaced_parts: Optional[str] = Field(None, description="更换的配件")
|
|
||||||
images: Optional[str] = Field(None, description="维修图片URL(多个逗号分隔)")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordCreate(MaintenanceRecordBase):
|
|
||||||
"""创建维修记录Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordUpdate(BaseModel):
|
|
||||||
"""更新维修记录Schema"""
|
|
||||||
fault_description: Optional[str] = Field(None, min_length=1)
|
|
||||||
fault_type: Optional[str] = None
|
|
||||||
priority: Optional[str] = None
|
|
||||||
maintenance_type: Optional[str] = None
|
|
||||||
vendor_id: Optional[int] = Field(None, gt=0)
|
|
||||||
maintenance_cost: Optional[Decimal] = Field(None, ge=0)
|
|
||||||
maintenance_result: Optional[str] = None
|
|
||||||
replaced_parts: Optional[str] = None
|
|
||||||
images: Optional[str] = None
|
|
||||||
remark: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordStart(BaseModel):
|
|
||||||
"""开始维修Schema"""
|
|
||||||
maintenance_type: str = Field(..., description="维修类型")
|
|
||||||
vendor_id: Optional[int] = Field(None, gt=0, description="维修供应商ID(vendor_repair时必填)")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordComplete(BaseModel):
|
|
||||||
"""完成维修Schema"""
|
|
||||||
maintenance_result: str = Field(..., description="维修结果描述")
|
|
||||||
maintenance_cost: Optional[Decimal] = Field(None, ge=0, description="维修费用")
|
|
||||||
replaced_parts: Optional[str] = Field(None, description="更换的配件")
|
|
||||||
images: Optional[str] = Field(None, description="维修图片URL")
|
|
||||||
asset_status: str = Field(default="in_stock", description="资产维修后状态(in_stock/in_use)")
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordInDB(BaseModel):
|
|
||||||
"""数据库中的维修记录Schema"""
|
|
||||||
id: int
|
|
||||||
record_code: str
|
|
||||||
asset_id: int
|
|
||||||
asset_code: str
|
|
||||||
fault_description: str
|
|
||||||
fault_type: Optional[str]
|
|
||||||
report_user_id: Optional[int]
|
|
||||||
report_time: datetime
|
|
||||||
priority: str
|
|
||||||
maintenance_type: Optional[str]
|
|
||||||
vendor_id: Optional[int]
|
|
||||||
maintenance_cost: Optional[Decimal]
|
|
||||||
start_time: Optional[datetime]
|
|
||||||
complete_time: Optional[datetime]
|
|
||||||
maintenance_user_id: Optional[int]
|
|
||||||
maintenance_result: Optional[str]
|
|
||||||
replaced_parts: Optional[str]
|
|
||||||
status: str
|
|
||||||
images: Optional[str]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordResponse(MaintenanceRecordInDB):
|
|
||||||
"""维修记录响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceRecordWithRelations(MaintenanceRecordResponse):
|
|
||||||
"""带关联信息的维修记录响应Schema"""
|
|
||||||
asset: Optional[Dict[str, Any]] = None
|
|
||||||
vendor: Optional[Dict[str, Any]] = None
|
|
||||||
report_user: Optional[Dict[str, Any]] = None
|
|
||||||
maintenance_user: Optional[Dict[str, Any]] = None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 查询参数Schema =====
|
|
||||||
|
|
||||||
class MaintenanceRecordQueryParams(BaseModel):
|
|
||||||
"""维修记录查询参数"""
|
|
||||||
asset_id: Optional[int] = Field(None, gt=0, description="资产ID")
|
|
||||||
status: Optional[str] = Field(None, description="状态")
|
|
||||||
fault_type: Optional[str] = Field(None, description="故障类型")
|
|
||||||
priority: Optional[str] = Field(None, description="优先级")
|
|
||||||
maintenance_type: Optional[str] = Field(None, description="维修类型")
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 统计Schema =====
|
|
||||||
|
|
||||||
class MaintenanceStatistics(BaseModel):
|
|
||||||
"""维修统计Schema"""
|
|
||||||
total: int = Field(..., description="总数")
|
|
||||||
pending: int = Field(..., description="待处理数")
|
|
||||||
in_progress: int = Field(..., description="维修中数")
|
|
||||||
completed: int = Field(..., description="已完成数")
|
|
||||||
cancelled: int = Field(..., description="已取消数")
|
|
||||||
total_cost: Decimal = Field(..., description="总维修费用")
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
"""
|
|
||||||
消息通知相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTypeEnum(str, Enum):
|
|
||||||
"""通知类型枚举"""
|
|
||||||
SYSTEM = "system" # 系统通知
|
|
||||||
APPROVAL = "approval" # 审批通知
|
|
||||||
MAINTENANCE = "maintenance" # 维修通知
|
|
||||||
ALLOCATION = "allocation" # 调拨通知
|
|
||||||
ASSET = "asset" # 资产通知
|
|
||||||
WARRANTY = "warranty" # 保修到期通知
|
|
||||||
REMINDER = "reminder" # 提醒通知
|
|
||||||
|
|
||||||
|
|
||||||
class PriorityEnum(str, Enum):
|
|
||||||
"""优先级枚举"""
|
|
||||||
LOW = "low"
|
|
||||||
NORMAL = "normal"
|
|
||||||
HIGH = "high"
|
|
||||||
URGENT = "urgent"
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationBase(BaseModel):
|
|
||||||
"""消息通知基础Schema"""
|
|
||||||
recipient_id: int = Field(..., description="接收人ID")
|
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="通知标题")
|
|
||||||
content: str = Field(..., min_length=1, description="通知内容")
|
|
||||||
notification_type: NotificationTypeEnum = Field(..., description="通知类型")
|
|
||||||
priority: PriorityEnum = Field(default=PriorityEnum.NORMAL, description="优先级")
|
|
||||||
related_entity_type: Optional[str] = Field(None, max_length=50, description="关联实体类型")
|
|
||||||
related_entity_id: Optional[int] = Field(None, description="关联实体ID")
|
|
||||||
action_url: Optional[str] = Field(None, max_length=500, description="操作链接")
|
|
||||||
extra_data: Optional[Dict[str, Any]] = Field(None, description="额外数据")
|
|
||||||
send_email: bool = Field(default=False, description="是否发送邮件")
|
|
||||||
send_sms: bool = Field(default=False, description="是否发送短信")
|
|
||||||
expire_at: Optional[datetime] = Field(None, description="过期时间")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationCreate(NotificationBase):
|
|
||||||
"""创建消息通知Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationUpdate(BaseModel):
|
|
||||||
"""更新消息通知Schema"""
|
|
||||||
is_read: Optional[bool] = Field(None, description="是否已读")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationInDB(BaseModel):
|
|
||||||
"""数据库中的消息通知Schema"""
|
|
||||||
id: int
|
|
||||||
recipient_id: int
|
|
||||||
recipient_name: str
|
|
||||||
title: str
|
|
||||||
content: str
|
|
||||||
notification_type: str
|
|
||||||
priority: str
|
|
||||||
is_read: bool
|
|
||||||
read_at: Optional[datetime]
|
|
||||||
related_entity_type: Optional[str]
|
|
||||||
related_entity_id: Optional[int]
|
|
||||||
action_url: Optional[str]
|
|
||||||
extra_data: Optional[Dict[str, Any]]
|
|
||||||
sent_via_email: bool
|
|
||||||
sent_via_sms: bool
|
|
||||||
created_at: datetime
|
|
||||||
expire_at: Optional[datetime]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationResponse(NotificationInDB):
|
|
||||||
"""消息通知响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationQueryParams(BaseModel):
|
|
||||||
"""消息通知查询参数"""
|
|
||||||
recipient_id: Optional[int] = Field(None, description="接收人ID")
|
|
||||||
notification_type: Optional[NotificationTypeEnum] = Field(None, description="通知类型")
|
|
||||||
priority: Optional[PriorityEnum] = Field(None, description="优先级")
|
|
||||||
is_read: Optional[bool] = Field(None, description="是否已读")
|
|
||||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
|
||||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
|
||||||
keyword: Optional[str] = Field(None, description="关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationBatchCreate(BaseModel):
|
|
||||||
"""批量创建通知Schema"""
|
|
||||||
recipient_ids: List[int] = Field(..., min_items=1, description="接收人ID列表")
|
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="通知标题")
|
|
||||||
content: str = Field(..., min_length=1, description="通知内容")
|
|
||||||
notification_type: NotificationTypeEnum = Field(..., description="通知类型")
|
|
||||||
priority: PriorityEnum = Field(default=PriorityEnum.NORMAL, description="优先级")
|
|
||||||
action_url: Optional[str] = Field(None, max_length=500, description="操作链接")
|
|
||||||
extra_data: Optional[Dict[str, Any]] = Field(None, description="额外数据")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationBatchUpdate(BaseModel):
|
|
||||||
"""批量更新通知Schema"""
|
|
||||||
notification_ids: List[int] = Field(..., min_items=1, description="通知ID列表")
|
|
||||||
is_read: bool = Field(..., description="是否已读")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationStatistics(BaseModel):
|
|
||||||
"""通知统计Schema"""
|
|
||||||
total_count: int = Field(..., description="总通知数")
|
|
||||||
unread_count: int = Field(..., description="未读数")
|
|
||||||
read_count: int = Field(..., description="已读数")
|
|
||||||
high_priority_count: int = Field(..., description="高优先级数")
|
|
||||||
urgent_count: int = Field(..., description="紧急通知数")
|
|
||||||
type_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="类型分布")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 通知模板Schema =====
|
|
||||||
|
|
||||||
class NotificationTemplateBase(BaseModel):
|
|
||||||
"""通知模板基础Schema"""
|
|
||||||
template_code: str = Field(..., min_length=1, max_length=50, description="模板编码")
|
|
||||||
template_name: str = Field(..., min_length=1, max_length=200, description="模板名称")
|
|
||||||
notification_type: NotificationTypeEnum = Field(..., description="通知类型")
|
|
||||||
title_template: str = Field(..., min_length=1, max_length=200, description="标题模板")
|
|
||||||
content_template: str = Field(..., min_length=1, description="内容模板")
|
|
||||||
variables: Optional[Dict[str, str]] = Field(None, description="变量说明")
|
|
||||||
priority: PriorityEnum = Field(default=PriorityEnum.NORMAL, description="默认优先级")
|
|
||||||
send_email: bool = Field(default=False, description="是否发送邮件")
|
|
||||||
send_sms: bool = Field(default=False, description="是否发送短信")
|
|
||||||
is_active: bool = Field(default=True, description="是否启用")
|
|
||||||
description: Optional[str] = Field(None, description="模板描述")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplateCreate(NotificationTemplateBase):
|
|
||||||
"""创建通知模板Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplateUpdate(BaseModel):
|
|
||||||
"""更新通知模板Schema"""
|
|
||||||
template_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
title_template: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
content_template: Optional[str] = Field(None, min_length=1)
|
|
||||||
variables: Optional[Dict[str, str]] = None
|
|
||||||
priority: Optional[PriorityEnum] = None
|
|
||||||
send_email: Optional[bool] = None
|
|
||||||
send_sms: Optional[bool] = None
|
|
||||||
is_active: Optional[bool] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplateInDB(BaseModel):
|
|
||||||
"""数据库中的通知模板Schema"""
|
|
||||||
id: int
|
|
||||||
template_code: str
|
|
||||||
template_name: str
|
|
||||||
notification_type: str
|
|
||||||
title_template: str
|
|
||||||
content_template: str
|
|
||||||
variables: Optional[Dict[str, str]]
|
|
||||||
priority: str
|
|
||||||
send_email: bool
|
|
||||||
send_sms: bool
|
|
||||||
is_active: bool
|
|
||||||
description: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplateResponse(NotificationTemplateInDB):
|
|
||||||
"""通知模板响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationSendFromTemplate(BaseModel):
|
|
||||||
"""从模板发送通知Schema"""
|
|
||||||
template_code: str = Field(..., description="模板编码")
|
|
||||||
recipient_ids: List[int] = Field(..., min_items=1, description="接收人ID列表")
|
|
||||||
variables: Dict[str, Any] = Field(default_factory=dict, description="模板变量")
|
|
||||||
related_entity_type: Optional[str] = Field(None, description="关联实体类型")
|
|
||||||
related_entity_id: Optional[int] = Field(None, description="关联实体ID")
|
|
||||||
action_url: Optional[str] = Field(None, description="操作链接")
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
"""
|
|
||||||
操作日志相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class OperationModuleEnum(str, Enum):
|
|
||||||
"""操作模块枚举"""
|
|
||||||
AUTH = "auth" # 认证模块
|
|
||||||
ASSET = "asset" # 资产模块
|
|
||||||
DEVICE_TYPE = "device_type" # 设备类型模块
|
|
||||||
ORGANIZATION = "organization" # 机构模块
|
|
||||||
BRAND_SUPPLIER = "brand_supplier" # 品牌供应商模块
|
|
||||||
ALLOCATION = "allocation" # 调拨模块
|
|
||||||
MAINTENANCE = "maintenance" # 维修模块
|
|
||||||
SYSTEM_CONFIG = "system_config" # 系统配置模块
|
|
||||||
USER = "user" # 用户模块
|
|
||||||
STATISTICS = "statistics" # 统计模块
|
|
||||||
|
|
||||||
|
|
||||||
class OperationTypeEnum(str, Enum):
|
|
||||||
"""操作类型枚举"""
|
|
||||||
CREATE = "create" # 创建
|
|
||||||
UPDATE = "update" # 更新
|
|
||||||
DELETE = "delete" # 删除
|
|
||||||
QUERY = "query" # 查询
|
|
||||||
EXPORT = "export" # 导出
|
|
||||||
IMPORT = "import" # 导入
|
|
||||||
LOGIN = "login" # 登录
|
|
||||||
LOGOUT = "logout" # 登出
|
|
||||||
APPROVE = "approve" # 审批
|
|
||||||
REJECT = "reject" # 拒绝
|
|
||||||
ASSIGN = "assign" # 分配
|
|
||||||
TRANSFER = "transfer" # 调拨
|
|
||||||
SCRAP = "scrap" # 报废
|
|
||||||
|
|
||||||
|
|
||||||
class OperationResultEnum(str, Enum):
|
|
||||||
"""操作结果枚举"""
|
|
||||||
SUCCESS = "success"
|
|
||||||
FAILED = "failed"
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogBase(BaseModel):
|
|
||||||
"""操作日志基础Schema"""
|
|
||||||
operator_id: int = Field(..., description="操作人ID")
|
|
||||||
operator_name: str = Field(..., min_length=1, max_length=100, description="操作人姓名")
|
|
||||||
operator_ip: Optional[str] = Field(None, max_length=50, description="操作人IP")
|
|
||||||
module: OperationModuleEnum = Field(..., description="模块名称")
|
|
||||||
operation_type: OperationTypeEnum = Field(..., description="操作类型")
|
|
||||||
method: str = Field(..., min_length=1, max_length=10, description="请求方法")
|
|
||||||
url: str = Field(..., min_length=1, max_length=500, description="请求URL")
|
|
||||||
params: Optional[str] = Field(None, description="请求参数")
|
|
||||||
result: OperationResultEnum = Field(default=OperationResultEnum.SUCCESS, description="操作结果")
|
|
||||||
error_msg: Optional[str] = Field(None, description="错误信息")
|
|
||||||
duration: Optional[int] = Field(None, ge=0, description="执行时长(毫秒)")
|
|
||||||
user_agent: Optional[str] = Field(None, max_length=500, description="用户代理")
|
|
||||||
extra_data: Optional[Dict[str, Any]] = Field(None, description="额外数据")
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogCreate(OperationLogBase):
|
|
||||||
"""创建操作日志Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogInDB(BaseModel):
|
|
||||||
"""数据库中的操作日志Schema"""
|
|
||||||
id: int
|
|
||||||
operator_id: int
|
|
||||||
operator_name: str
|
|
||||||
operator_ip: Optional[str]
|
|
||||||
module: str
|
|
||||||
operation_type: str
|
|
||||||
method: str
|
|
||||||
url: str
|
|
||||||
params: Optional[str]
|
|
||||||
result: str
|
|
||||||
error_msg: Optional[str]
|
|
||||||
duration: Optional[int]
|
|
||||||
user_agent: Optional[str]
|
|
||||||
extra_data: Optional[Dict[str, Any]]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogResponse(OperationLogInDB):
|
|
||||||
"""操作日志响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogQueryParams(BaseModel):
|
|
||||||
"""操作日志查询参数"""
|
|
||||||
operator_id: Optional[int] = Field(None, description="操作人ID")
|
|
||||||
operator_name: Optional[str] = Field(None, description="操作人姓名")
|
|
||||||
module: Optional[OperationModuleEnum] = Field(None, description="模块名称")
|
|
||||||
operation_type: Optional[OperationTypeEnum] = Field(None, description="操作类型")
|
|
||||||
result: Optional[OperationResultEnum] = Field(None, description="操作结果")
|
|
||||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
|
||||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
|
||||||
keyword: Optional[str] = Field(None, description="关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogStatistics(BaseModel):
|
|
||||||
"""操作日志统计Schema"""
|
|
||||||
total_count: int = Field(..., description="总操作次数")
|
|
||||||
success_count: int = Field(..., description="成功次数")
|
|
||||||
failed_count: int = Field(..., description="失败次数")
|
|
||||||
today_count: int = Field(..., description="今日操作次数")
|
|
||||||
module_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="模块分布")
|
|
||||||
operation_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="操作类型分布")
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogExport(BaseModel):
|
|
||||||
"""操作日志导出Schema"""
|
|
||||||
start_time: Optional[datetime] = Field(None, description="开始时间")
|
|
||||||
end_time: Optional[datetime] = Field(None, description="结束时间")
|
|
||||||
operator_id: Optional[int] = Field(None, description="操作人ID")
|
|
||||||
module: Optional[str] = Field(None, description="模块名称")
|
|
||||||
operation_type: Optional[str] = Field(None, description="操作类型")
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"""
|
|
||||||
机构网点相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 机构网点Schema =====
|
|
||||||
|
|
||||||
class OrganizationBase(BaseModel):
|
|
||||||
"""机构基础Schema"""
|
|
||||||
org_code: str = Field(..., min_length=1, max_length=50, description="机构代码")
|
|
||||||
org_name: str = Field(..., min_length=1, max_length=200, description="机构名称")
|
|
||||||
org_type: str = Field(..., pattern="^(province|city|outlet)$", description="机构类型")
|
|
||||||
parent_id: Optional[int] = Field(None, description="父机构ID")
|
|
||||||
address: Optional[str] = Field(None, max_length=500, description="地址")
|
|
||||||
contact_person: Optional[str] = Field(None, max_length=100, description="联系人")
|
|
||||||
contact_phone: Optional[str] = Field(None, max_length=20, description="联系电话")
|
|
||||||
sort_order: int = Field(default=0, description="排序")
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationCreate(OrganizationBase):
|
|
||||||
"""创建机构Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationUpdate(BaseModel):
|
|
||||||
"""更新机构Schema"""
|
|
||||||
org_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
org_type: Optional[str] = Field(None, pattern="^(province|city|outlet)$")
|
|
||||||
parent_id: Optional[int] = None
|
|
||||||
address: Optional[str] = Field(None, max_length=500)
|
|
||||||
contact_person: Optional[str] = Field(None, max_length=100)
|
|
||||||
contact_phone: Optional[str] = Field(None, max_length=20)
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|inactive)$")
|
|
||||||
sort_order: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationInDB(BaseModel):
|
|
||||||
"""数据库中的机构Schema"""
|
|
||||||
id: int
|
|
||||||
org_code: str
|
|
||||||
org_name: str
|
|
||||||
org_type: str
|
|
||||||
parent_id: Optional[int]
|
|
||||||
tree_path: Optional[str]
|
|
||||||
tree_level: int
|
|
||||||
address: Optional[str]
|
|
||||||
contact_person: Optional[str]
|
|
||||||
contact_phone: Optional[str]
|
|
||||||
status: str
|
|
||||||
sort_order: int
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationResponse(OrganizationInDB):
|
|
||||||
"""机构响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationTreeNode(OrganizationResponse):
|
|
||||||
"""机构树节点Schema"""
|
|
||||||
children: List["OrganizationTreeNode"] = []
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationWithParent(OrganizationResponse):
|
|
||||||
"""带父机构信息的Schema"""
|
|
||||||
parent: Optional[OrganizationResponse] = None
|
|
||||||
|
|
||||||
|
|
||||||
# 更新前向引用
|
|
||||||
OrganizationTreeNode.model_rebuild()
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
"""
|
|
||||||
资产回收相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 回收单Schema =====
|
|
||||||
|
|
||||||
class AssetRecoveryOrderBase(BaseModel):
|
|
||||||
"""回收单基础Schema"""
|
|
||||||
recovery_type: str = Field(..., description="回收类型(user=使用人回收/org=机构回收/scrap=报废回收)")
|
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="标题")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderCreate(AssetRecoveryOrderBase):
|
|
||||||
"""创建回收单Schema"""
|
|
||||||
asset_ids: List[int] = Field(..., min_items=1, description="资产ID列表")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderUpdate(BaseModel):
|
|
||||||
"""更新回收单Schema"""
|
|
||||||
title: Optional[str] = Field(None, min_length=1, max_length=200, description="标题")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderInDB(BaseModel):
|
|
||||||
"""数据库中的回收单Schema"""
|
|
||||||
id: int
|
|
||||||
order_code: str
|
|
||||||
recovery_type: str
|
|
||||||
title: str
|
|
||||||
asset_count: int
|
|
||||||
apply_user_id: int
|
|
||||||
apply_time: datetime
|
|
||||||
approval_status: str
|
|
||||||
approval_user_id: Optional[int]
|
|
||||||
approval_time: Optional[datetime]
|
|
||||||
approval_remark: Optional[str]
|
|
||||||
execute_status: str
|
|
||||||
execute_user_id: Optional[int]
|
|
||||||
execute_time: Optional[datetime]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderResponse(AssetRecoveryOrderInDB):
|
|
||||||
"""回收单响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderWithRelations(AssetRecoveryOrderResponse):
|
|
||||||
"""带关联信息的回收单响应Schema"""
|
|
||||||
apply_user: Optional[Dict[str, Any]] = None
|
|
||||||
approval_user: Optional[Dict[str, Any]] = None
|
|
||||||
execute_user: Optional[Dict[str, Any]] = None
|
|
||||||
items: Optional[List[Dict[str, Any]]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderQueryParams(BaseModel):
|
|
||||||
"""回收单查询参数"""
|
|
||||||
recovery_type: Optional[str] = Field(None, description="回收类型")
|
|
||||||
approval_status: Optional[str] = Field(None, description="审批状态")
|
|
||||||
execute_status: Optional[str] = Field(None, description="执行状态")
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryOrderListResponse(BaseModel):
|
|
||||||
"""回收单列表响应Schema"""
|
|
||||||
total: int
|
|
||||||
page: int
|
|
||||||
page_size: int
|
|
||||||
total_pages: int
|
|
||||||
items: List[AssetRecoveryOrderWithRelations]
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryStatistics(BaseModel):
|
|
||||||
"""回收单统计Schema"""
|
|
||||||
total: int = Field(..., description="总数")
|
|
||||||
pending: int = Field(..., description="待审批数")
|
|
||||||
approved: int = Field(..., description="已审批数")
|
|
||||||
rejected: int = Field(..., description="已拒绝数")
|
|
||||||
executing: int = Field(..., description="执行中数")
|
|
||||||
completed: int = Field(..., description="已完成数")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 回收单明细Schema =====
|
|
||||||
|
|
||||||
class AssetRecoveryItemBase(BaseModel):
|
|
||||||
"""回收单明细基础Schema"""
|
|
||||||
asset_id: int = Field(..., gt=0, description="资产ID")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryItemInDB(BaseModel):
|
|
||||||
"""数据库中的回收单明细Schema"""
|
|
||||||
id: int
|
|
||||||
order_id: int
|
|
||||||
asset_id: int
|
|
||||||
asset_code: str
|
|
||||||
recovery_status: str
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AssetRecoveryItemResponse(AssetRecoveryItemInDB):
|
|
||||||
"""回收单明细响应Schema"""
|
|
||||||
pass
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
"""
|
|
||||||
统计分析相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime, date
|
|
||||||
from decimal import Decimal
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticsOverview(BaseModel):
|
|
||||||
"""总览统计Schema"""
|
|
||||||
total_assets: int = Field(..., description="资产总数")
|
|
||||||
total_value: Decimal = Field(..., description="资产总价值")
|
|
||||||
in_stock_count: int = Field(..., description="库存中数量")
|
|
||||||
in_use_count: int = Field(..., description="使用中数量")
|
|
||||||
maintenance_count: int = Field(..., description="维修中数量")
|
|
||||||
scrapped_count: int = Field(..., description="已报废数量")
|
|
||||||
today_purchase_count: int = Field(..., description="今日采购数量")
|
|
||||||
this_month_purchase_count: int = Field(..., description="本月采购数量")
|
|
||||||
organization_count: int = Field(..., description="机构网点数")
|
|
||||||
supplier_count: int = Field(..., description="供应商数")
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseStatistics(BaseModel):
|
|
||||||
"""采购统计Schema"""
|
|
||||||
total_purchase_count: int = Field(..., description="总采购数量")
|
|
||||||
total_purchase_value: Decimal = Field(..., description="总采购金额")
|
|
||||||
monthly_trend: List[Dict[str, Any]] = Field(default_factory=list, description="月度趋势")
|
|
||||||
supplier_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="供应商分布")
|
|
||||||
category_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="分类分布")
|
|
||||||
|
|
||||||
|
|
||||||
class DepreciationStatistics(BaseModel):
|
|
||||||
"""折旧统计Schema"""
|
|
||||||
total_depreciation_value: Decimal = Field(..., description="总折旧金额")
|
|
||||||
average_depreciation_rate: Decimal = Field(..., description="平均折旧率")
|
|
||||||
depreciation_by_category: List[Dict[str, Any]] = Field(default_factory=list, description="分类折旧")
|
|
||||||
assets_near_end_life: List[Dict[str, Any]] = Field(default_factory=list, description="接近使用年限的资产")
|
|
||||||
|
|
||||||
|
|
||||||
class ValueStatistics(BaseModel):
|
|
||||||
"""价值统计Schema"""
|
|
||||||
total_value: Decimal = Field(..., description="资产总价值")
|
|
||||||
net_value: Decimal = Field(..., description="资产净值")
|
|
||||||
depreciation_value: Decimal = Field(..., description="累计折旧")
|
|
||||||
value_by_category: List[Dict[str, Any]] = Field(default_factory=list, description="分类价值")
|
|
||||||
value_by_organization: List[Dict[str, Any]] = Field(default_factory=list, description="网点价值")
|
|
||||||
high_value_assets: List[Dict[str, Any]] = Field(default_factory=list, description="高价值资产")
|
|
||||||
|
|
||||||
|
|
||||||
class TrendAnalysis(BaseModel):
|
|
||||||
"""趋势分析Schema"""
|
|
||||||
asset_trend: List[Dict[str, Any]] = Field(default_factory=list, description="资产数量趋势")
|
|
||||||
value_trend: List[Dict[str, Any]] = Field(default_factory=list, description="资产价值趋势")
|
|
||||||
purchase_trend: List[Dict[str, Any]] = Field(default_factory=list, description="采购趋势")
|
|
||||||
maintenance_trend: List[Dict[str, Any]] = Field(default_factory=list, description="维修趋势")
|
|
||||||
allocation_trend: List[Dict[str, Any]] = Field(default_factory=list, description="调拨趋势")
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceStatistics(BaseModel):
|
|
||||||
"""维修统计Schema"""
|
|
||||||
total_maintenance_count: int = Field(..., description="总维修次数")
|
|
||||||
total_maintenance_cost: Decimal = Field(..., description="总维修费用")
|
|
||||||
pending_count: int = Field(..., description="待维修数量")
|
|
||||||
in_progress_count: int = Field(..., description="维修中数量")
|
|
||||||
completed_count: int = Field(..., description="已完成数量")
|
|
||||||
monthly_trend: List[Dict[str, Any]] = Field(default_factory=list, description="月度趋势")
|
|
||||||
type_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="维修类型分布")
|
|
||||||
cost_by_category: List[Dict[str, Any]] = Field(default_factory=list, description="分类维修费用")
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationStatistics(BaseModel):
|
|
||||||
"""分配统计Schema"""
|
|
||||||
total_allocation_count: int = Field(..., description="总分配次数")
|
|
||||||
pending_count: int = Field(..., description="待审批数量")
|
|
||||||
approved_count: int = Field(..., description="已批准数量")
|
|
||||||
rejected_count: int = Field(..., description="已拒绝数量")
|
|
||||||
monthly_trend: List[Dict[str, Any]] = Field(default_factory=list, description="月度趋势")
|
|
||||||
by_organization: List[Dict[str, Any]] = Field(default_factory=list, description="网点分配统计")
|
|
||||||
transfer_statistics: List[Dict[str, Any]] = Field(default_factory=list, description="调拨统计")
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticsQueryParams(BaseModel):
|
|
||||||
"""统计查询参数"""
|
|
||||||
start_date: Optional[date] = Field(None, description="开始日期")
|
|
||||||
end_date: Optional[date] = Field(None, description="结束日期")
|
|
||||||
organization_id: Optional[int] = Field(None, description="网点ID")
|
|
||||||
device_type_id: Optional[int] = Field(None, description="设备类型ID")
|
|
||||||
group_by: Optional[str] = Field(None, description="分组字段")
|
|
||||||
|
|
||||||
|
|
||||||
class ExportStatisticsRequest(BaseModel):
|
|
||||||
"""导出统计请求"""
|
|
||||||
report_type: str = Field(..., description="报表类型")
|
|
||||||
start_date: Optional[date] = Field(None, description="开始日期")
|
|
||||||
end_date: Optional[date] = Field(None, description="结束日期")
|
|
||||||
organization_id: Optional[int] = Field(None, description="网点ID")
|
|
||||||
device_type_id: Optional[int] = Field(None, description="设备类型ID")
|
|
||||||
format: str = Field(default="xlsx", description="导出格式")
|
|
||||||
include_charts: bool = Field(default=False, description="是否包含图表")
|
|
||||||
|
|
||||||
|
|
||||||
class ExportStatisticsResponse(BaseModel):
|
|
||||||
"""导出统计响应"""
|
|
||||||
file_url: str = Field(..., description="文件URL")
|
|
||||||
file_name: str = Field(..., description="文件名")
|
|
||||||
file_size: int = Field(..., description="文件大小(字节)")
|
|
||||||
record_count: int = Field(..., description="记录数量")
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
"""
|
|
||||||
系统配置相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class ValueTypeEnum(str, Enum):
|
|
||||||
"""配置值类型枚举"""
|
|
||||||
STRING = "string"
|
|
||||||
NUMBER = "number"
|
|
||||||
BOOLEAN = "boolean"
|
|
||||||
JSON = "json"
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigBase(BaseModel):
|
|
||||||
"""系统配置基础Schema"""
|
|
||||||
config_key: str = Field(..., min_length=1, max_length=100, description="配置键")
|
|
||||||
config_name: str = Field(..., min_length=1, max_length=200, description="配置名称")
|
|
||||||
config_value: Optional[str] = Field(None, description="配置值")
|
|
||||||
value_type: ValueTypeEnum = Field(default=ValueTypeEnum.STRING, description="值类型")
|
|
||||||
category: str = Field(..., min_length=1, max_length=50, description="配置分类")
|
|
||||||
description: Optional[str] = Field(None, description="配置描述")
|
|
||||||
is_system: bool = Field(default=False, description="是否系统配置")
|
|
||||||
is_encrypted: bool = Field(default=False, description="是否加密存储")
|
|
||||||
validation_rule: Optional[str] = Field(None, description="验证规则")
|
|
||||||
options: Optional[Dict[str, Any]] = Field(None, description="可选值配置")
|
|
||||||
default_value: Optional[str] = Field(None, description="默认值")
|
|
||||||
sort_order: int = Field(default=0, description="排序序号")
|
|
||||||
is_active: bool = Field(default=True, description="是否启用")
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigCreate(SystemConfigBase):
|
|
||||||
"""创建系统配置Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigUpdate(BaseModel):
|
|
||||||
"""更新系统配置Schema"""
|
|
||||||
config_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
||||||
config_value: Optional[str] = None
|
|
||||||
value_type: Optional[ValueTypeEnum] = None
|
|
||||||
category: Optional[str] = Field(None, min_length=1, max_length=50)
|
|
||||||
description: Optional[str] = None
|
|
||||||
validation_rule: Optional[str] = None
|
|
||||||
options: Optional[Dict[str, Any]] = None
|
|
||||||
default_value: Optional[str] = None
|
|
||||||
sort_order: Optional[int] = None
|
|
||||||
is_active: Optional[bool] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigInDB(BaseModel):
|
|
||||||
"""数据库中的系统配置Schema"""
|
|
||||||
id: int
|
|
||||||
config_key: str
|
|
||||||
config_name: str
|
|
||||||
config_value: Optional[str]
|
|
||||||
value_type: str
|
|
||||||
category: str
|
|
||||||
description: Optional[str]
|
|
||||||
is_system: bool
|
|
||||||
is_encrypted: bool
|
|
||||||
validation_rule: Optional[str]
|
|
||||||
options: Optional[Dict[str, Any]]
|
|
||||||
default_value: Optional[str]
|
|
||||||
sort_order: int
|
|
||||||
is_active: bool
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
updated_by: Optional[int]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigResponse(SystemConfigInDB):
|
|
||||||
"""系统配置响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigBatchUpdate(BaseModel):
|
|
||||||
"""批量更新配置Schema"""
|
|
||||||
configs: Dict[str, Any] = Field(..., description="配置键值对")
|
|
||||||
|
|
||||||
|
|
||||||
class SystemConfigQueryParams(BaseModel):
|
|
||||||
"""系统配置查询参数"""
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
category: Optional[str] = Field(None, description="配置分类")
|
|
||||||
is_active: Optional[bool] = Field(None, description="是否启用")
|
|
||||||
is_system: Optional[bool] = Field(None, description="是否系统配置")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigCategoryResponse(BaseModel):
|
|
||||||
"""配置分类响应Schema"""
|
|
||||||
category: str = Field(..., description="分类名称")
|
|
||||||
count: int = Field(..., description="配置数量")
|
|
||||||
description: Optional[str] = Field(None, description="分类描述")
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
"""
|
|
||||||
资产调拨相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 调拨单Schema =====
|
|
||||||
|
|
||||||
class AssetTransferOrderBase(BaseModel):
|
|
||||||
"""调拨单基础Schema"""
|
|
||||||
source_org_id: int = Field(..., gt=0, description="调出网点ID")
|
|
||||||
target_org_id: int = Field(..., gt=0, description="调入网点ID")
|
|
||||||
transfer_type: str = Field(..., description="调拨类型(internal=内部调拨/external=跨机构调拨)")
|
|
||||||
title: str = Field(..., min_length=1, max_length=200, description="标题")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderCreate(AssetTransferOrderBase):
|
|
||||||
"""创建调拨单Schema"""
|
|
||||||
asset_ids: List[int] = Field(..., min_items=1, description="资产ID列表")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderUpdate(BaseModel):
|
|
||||||
"""更新调拨单Schema"""
|
|
||||||
title: Optional[str] = Field(None, min_length=1, max_length=200, description="标题")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderStart(BaseModel):
|
|
||||||
"""开始调拨Schema"""
|
|
||||||
remark: Optional[str] = Field(None, description="开始备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderComplete(BaseModel):
|
|
||||||
"""完成调拨Schema"""
|
|
||||||
remark: Optional[str] = Field(None, description="完成备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderInDB(BaseModel):
|
|
||||||
"""数据库中的调拨单Schema"""
|
|
||||||
id: int
|
|
||||||
order_code: str
|
|
||||||
source_org_id: int
|
|
||||||
target_org_id: int
|
|
||||||
transfer_type: str
|
|
||||||
title: str
|
|
||||||
asset_count: int
|
|
||||||
apply_user_id: int
|
|
||||||
apply_time: datetime
|
|
||||||
approval_status: str
|
|
||||||
approval_user_id: Optional[int]
|
|
||||||
approval_time: Optional[datetime]
|
|
||||||
approval_remark: Optional[str]
|
|
||||||
execute_status: str
|
|
||||||
execute_user_id: Optional[int]
|
|
||||||
execute_time: Optional[datetime]
|
|
||||||
remark: Optional[str]
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderResponse(AssetTransferOrderInDB):
|
|
||||||
"""调拨单响应Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderWithRelations(AssetTransferOrderResponse):
|
|
||||||
"""带关联信息的调拨单响应Schema"""
|
|
||||||
source_organization: Optional[Dict[str, Any]] = None
|
|
||||||
target_organization: Optional[Dict[str, Any]] = None
|
|
||||||
apply_user: Optional[Dict[str, Any]] = None
|
|
||||||
approval_user: Optional[Dict[str, Any]] = None
|
|
||||||
execute_user: Optional[Dict[str, Any]] = None
|
|
||||||
items: Optional[List[Dict[str, Any]]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderQueryParams(BaseModel):
|
|
||||||
"""调拨单查询参数"""
|
|
||||||
transfer_type: Optional[str] = Field(None, description="调拨类型")
|
|
||||||
approval_status: Optional[str] = Field(None, description="审批状态")
|
|
||||||
execute_status: Optional[str] = Field(None, description="执行状态")
|
|
||||||
source_org_id: Optional[int] = Field(None, gt=0, description="调出网点ID")
|
|
||||||
target_org_id: Optional[int] = Field(None, gt=0, description="调入网点ID")
|
|
||||||
keyword: Optional[str] = Field(None, description="搜索关键词")
|
|
||||||
page: int = Field(default=1, ge=1, description="页码")
|
|
||||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferOrderListResponse(BaseModel):
|
|
||||||
"""调拨单列表响应Schema"""
|
|
||||||
total: int
|
|
||||||
page: int
|
|
||||||
page_size: int
|
|
||||||
total_pages: int
|
|
||||||
items: List[AssetTransferOrderWithRelations]
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferStatistics(BaseModel):
|
|
||||||
"""调拨单统计Schema"""
|
|
||||||
total: int = Field(..., description="总数")
|
|
||||||
pending: int = Field(..., description="待审批数")
|
|
||||||
approved: int = Field(..., description="已审批数")
|
|
||||||
rejected: int = Field(..., description="已拒绝数")
|
|
||||||
executing: int = Field(..., description="执行中数")
|
|
||||||
completed: int = Field(..., description="已完成数")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 调拨单明细Schema =====
|
|
||||||
|
|
||||||
class AssetTransferItemBase(BaseModel):
|
|
||||||
"""调拨单明细基础Schema"""
|
|
||||||
asset_id: int = Field(..., gt=0, description="资产ID")
|
|
||||||
remark: Optional[str] = Field(None, description="备注")
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferItemInDB(BaseModel):
|
|
||||||
"""数据库中的调拨单明细Schema"""
|
|
||||||
id: int
|
|
||||||
order_id: int
|
|
||||||
asset_id: int
|
|
||||||
asset_code: str
|
|
||||||
source_organization_id: int
|
|
||||||
target_organization_id: int
|
|
||||||
transfer_status: str
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class AssetTransferItemResponse(AssetTransferItemInDB):
|
|
||||||
"""调拨单明细响应Schema"""
|
|
||||||
pass
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
"""
|
|
||||||
用户相关的Pydantic Schema
|
|
||||||
"""
|
|
||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field, EmailStr, field_validator
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 用户Schema =====
|
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
|
||||||
"""用户基础Schema"""
|
|
||||||
real_name: str = Field(..., min_length=1, max_length=100, description="真实姓名")
|
|
||||||
email: Optional[EmailStr] = Field(None, description="邮箱")
|
|
||||||
phone: Optional[str] = Field(None, max_length=20, description="手机号")
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
|
||||||
"""创建用户Schema"""
|
|
||||||
username: str = Field(..., min_length=4, max_length=50, description="用户名")
|
|
||||||
password: str = Field(..., min_length=8, max_length=100, description="密码")
|
|
||||||
role_ids: List[int] = Field(..., min_items=1, description="角色ID列表")
|
|
||||||
|
|
||||||
@field_validator("username")
|
|
||||||
@classmethod
|
|
||||||
def validate_username(cls, v: str) -> str:
|
|
||||||
"""验证用户名格式"""
|
|
||||||
if not v.replace("_", "").isalnum():
|
|
||||||
raise ValueError("用户名只能包含字母、数字和下划线")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator("password")
|
|
||||||
@classmethod
|
|
||||||
def validate_password(cls, v: str) -> str:
|
|
||||||
"""验证密码强度"""
|
|
||||||
if not any(c.isupper() for c in v):
|
|
||||||
raise ValueError("密码必须包含至少一个大写字母")
|
|
||||||
if not any(c.islower() for c in v):
|
|
||||||
raise ValueError("密码必须包含至少一个小写字母")
|
|
||||||
if not any(c.isdigit() for c in v):
|
|
||||||
raise ValueError("密码必须包含至少一个数字")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(BaseModel):
|
|
||||||
"""更新用户Schema"""
|
|
||||||
real_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
||||||
email: Optional[EmailStr] = None
|
|
||||||
phone: Optional[str] = Field(None, max_length=20)
|
|
||||||
status: Optional[str] = Field(None, pattern="^(active|disabled|locked)$")
|
|
||||||
role_ids: Optional[List[int]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class UserInDB(BaseModel):
|
|
||||||
"""数据库中的用户Schema"""
|
|
||||||
id: int
|
|
||||||
username: str
|
|
||||||
real_name: str
|
|
||||||
email: Optional[str]
|
|
||||||
phone: Optional[str]
|
|
||||||
avatar_url: Optional[str]
|
|
||||||
status: str
|
|
||||||
is_admin: bool
|
|
||||||
last_login_at: Optional[datetime]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(UserInDB):
|
|
||||||
"""用户响应Schema"""
|
|
||||||
roles: List["RoleResponse"] = []
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class UserInfo(BaseModel):
|
|
||||||
"""用户信息Schema(不含敏感信息)"""
|
|
||||||
id: int
|
|
||||||
username: str
|
|
||||||
real_name: str
|
|
||||||
email: Optional[str]
|
|
||||||
avatar_url: Optional[str]
|
|
||||||
is_admin: bool
|
|
||||||
status: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 登录认证Schema =====
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
|
||||||
"""登录请求Schema"""
|
|
||||||
username: str = Field(..., min_length=1, description="用户名")
|
|
||||||
password: str = Field(..., min_length=1, description="密码")
|
|
||||||
captcha: str = Field(..., min_length=4, description="验证码")
|
|
||||||
captcha_key: str = Field(..., description="验证码UUID")
|
|
||||||
|
|
||||||
|
|
||||||
class LoginResponse(BaseModel):
|
|
||||||
"""登录响应Schema"""
|
|
||||||
access_token: str = Field(..., description="访问令牌")
|
|
||||||
refresh_token: str = Field(..., description="刷新令牌")
|
|
||||||
token_type: str = Field(default="Bearer", description="令牌类型")
|
|
||||||
expires_in: int = Field(..., description="过期时间(秒)")
|
|
||||||
user: UserInfo = Field(..., description="用户信息")
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenRequest(BaseModel):
|
|
||||||
"""刷新令牌请求Schema"""
|
|
||||||
refresh_token: str = Field(..., description="刷新令牌")
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenResponse(BaseModel):
|
|
||||||
"""刷新令牌响应Schema"""
|
|
||||||
access_token: str = Field(..., description="新的访问令牌")
|
|
||||||
expires_in: int = Field(..., description="过期时间(秒)")
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordRequest(BaseModel):
|
|
||||||
"""修改密码请求Schema"""
|
|
||||||
old_password: str = Field(..., min_length=1, description="旧密码")
|
|
||||||
new_password: str = Field(..., min_length=8, max_length=100, description="新密码")
|
|
||||||
confirm_password: str = Field(..., min_length=8, max_length=100, description="确认密码")
|
|
||||||
|
|
||||||
@field_validator("confirm_password")
|
|
||||||
@classmethod
|
|
||||||
def validate_passwords_match(cls, v: str, info) -> str:
|
|
||||||
"""验证两次密码是否一致"""
|
|
||||||
if "new_password" in info.data and v != info.data["new_password"]:
|
|
||||||
raise ValueError("两次输入的密码不一致")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordRequest(BaseModel):
|
|
||||||
"""重置密码请求Schema"""
|
|
||||||
new_password: str = Field(..., min_length=8, max_length=100, description="新密码")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 角色Schema =====
|
|
||||||
|
|
||||||
class RoleBase(BaseModel):
|
|
||||||
"""角色基础Schema"""
|
|
||||||
role_name: str = Field(..., min_length=1, max_length=50, description="角色名称")
|
|
||||||
role_code: str = Field(..., min_length=1, max_length=50, description="角色代码")
|
|
||||||
description: Optional[str] = Field(None, description="角色描述")
|
|
||||||
|
|
||||||
|
|
||||||
class RoleCreate(RoleBase):
|
|
||||||
"""创建角色Schema"""
|
|
||||||
permission_ids: List[int] = Field(default_factory=list, description="权限ID列表")
|
|
||||||
|
|
||||||
|
|
||||||
class RoleUpdate(BaseModel):
|
|
||||||
"""更新角色Schema"""
|
|
||||||
role_name: Optional[str] = Field(None, min_length=1, max_length=50)
|
|
||||||
description: Optional[str] = None
|
|
||||||
permission_ids: Optional[List[int]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class RoleInDB(BaseModel):
|
|
||||||
"""数据库中的角色Schema"""
|
|
||||||
id: int
|
|
||||||
role_name: str
|
|
||||||
role_code: str
|
|
||||||
description: Optional[str]
|
|
||||||
status: str
|
|
||||||
sort_order: int
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class RoleResponse(RoleInDB):
|
|
||||||
"""角色响应Schema"""
|
|
||||||
permissions: List["PermissionResponse"] = []
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class RoleWithUserCount(RoleResponse):
|
|
||||||
"""带用户数量的角色响应Schema"""
|
|
||||||
user_count: int = Field(..., description="用户数量")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== 权限Schema =====
|
|
||||||
|
|
||||||
class PermissionBase(BaseModel):
|
|
||||||
"""权限基础Schema"""
|
|
||||||
permission_name: str = Field(..., min_length=1, max_length=100)
|
|
||||||
permission_code: str = Field(..., min_length=1, max_length=100)
|
|
||||||
module: str = Field(..., min_length=1, max_length=50)
|
|
||||||
resource: Optional[str] = Field(None, max_length=50)
|
|
||||||
action: Optional[str] = Field(None, max_length=50)
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionCreate(PermissionBase):
|
|
||||||
"""创建权限Schema"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionUpdate(BaseModel):
|
|
||||||
"""更新权限Schema"""
|
|
||||||
permission_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionResponse(PermissionBase):
|
|
||||||
"""权限响应Schema"""
|
|
||||||
id: int
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionTreeNode(PermissionResponse):
|
|
||||||
"""权限树节点Schema"""
|
|
||||||
children: List["PermissionTreeNode"] = []
|
|
||||||
|
|
||||||
|
|
||||||
# 更新前向引用
|
|
||||||
UserResponse.model_rebuild()
|
|
||||||
RoleResponse.model_rebuild()
|
|
||||||
PermissionTreeNode.model_rebuild()
|
|
||||||
@@ -1,469 +0,0 @@
|
|||||||
"""
|
|
||||||
资产分配业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
from sqlalchemy.orm import Session, selectinload
|
|
||||||
from app.crud.allocation import allocation_order, allocation_item
|
|
||||||
from app.crud.asset import asset
|
|
||||||
from app.schemas.allocation import (
|
|
||||||
AllocationOrderCreate,
|
|
||||||
AllocationOrderUpdate,
|
|
||||||
AllocationOrderApproval
|
|
||||||
)
|
|
||||||
from app.core.exceptions import NotFoundException, BusinessException
|
|
||||||
|
|
||||||
|
|
||||||
class AllocationService:
|
|
||||||
"""资产分配服务类"""
|
|
||||||
|
|
||||||
async def get_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""获取分配单详情"""
|
|
||||||
# 使用selectinload预加载关联数据,避免N+1查询
|
|
||||||
from app.models.allocation import AllocationOrder
|
|
||||||
from app.models.organization import Organization
|
|
||||||
from app.models.user import User
|
|
||||||
from app.models.allocation import AllocationItem
|
|
||||||
|
|
||||||
obj = db.query(
|
|
||||||
AllocationOrder
|
|
||||||
).options(
|
|
||||||
selectinload(AllocationOrder.items),
|
|
||||||
selectinload(AllocationOrder.source_organization.of_type(Organization)),
|
|
||||||
selectinload(AllocationOrder.target_organization.of_type(Organization)),
|
|
||||||
selectinload(AllocationOrder.applicant.of_type(User)),
|
|
||||||
selectinload(AllocationOrder.approver.of_type(User)),
|
|
||||||
selectinload(AllocationOrder.executor.of_type(User))
|
|
||||||
).filter(
|
|
||||||
AllocationOrder.id == order_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
return self._load_order_relations(db, obj)
|
|
||||||
|
|
||||||
def get_orders(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
order_type: Optional[str] = None,
|
|
||||||
approval_status: Optional[str] = None,
|
|
||||||
execute_status: Optional[str] = None,
|
|
||||||
applicant_id: Optional[int] = None,
|
|
||||||
target_organization_id: Optional[int] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> tuple:
|
|
||||||
"""获取分配单列表"""
|
|
||||||
items, total = allocation_order.get_multi(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
order_type=order_type,
|
|
||||||
approval_status=approval_status,
|
|
||||||
execute_status=execute_status,
|
|
||||||
applicant_id=applicant_id,
|
|
||||||
target_organization_id=target_organization_id,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
items_with_relations = [self._load_order_relations(db, item) for item in items]
|
|
||||||
|
|
||||||
return items_with_relations, total
|
|
||||||
|
|
||||||
async def create_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AllocationOrderCreate,
|
|
||||||
applicant_id: int
|
|
||||||
):
|
|
||||||
"""创建分配单"""
|
|
||||||
# 验证资产存在性和状态
|
|
||||||
assets = []
|
|
||||||
for asset_id in obj_in.asset_ids:
|
|
||||||
asset_obj = asset.get(db, asset_id)
|
|
||||||
if not asset_obj:
|
|
||||||
raise NotFoundException(f"资产ID {asset_id}")
|
|
||||||
assets.append(asset_obj)
|
|
||||||
|
|
||||||
# 验证资产状态是否允许分配
|
|
||||||
for asset_obj in assets:
|
|
||||||
if not self._can_allocate(asset_obj.status, obj_in.order_type):
|
|
||||||
raise BusinessException(
|
|
||||||
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许{self._get_order_type_name(obj_in.order_type)}操作"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成分配单号
|
|
||||||
order_code = await self._generate_order_code(db, obj_in.order_type)
|
|
||||||
|
|
||||||
# 创建分配单
|
|
||||||
db_obj = allocation_order.create(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
order_code=order_code,
|
|
||||||
applicant_id=applicant_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
def update_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
obj_in: AllocationOrderUpdate,
|
|
||||||
updater_id: int
|
|
||||||
):
|
|
||||||
"""更新分配单"""
|
|
||||||
db_obj = allocation_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
# 只有草稿或待审批状态可以更新
|
|
||||||
if db_obj.approval_status not in ["pending", "draft"]:
|
|
||||||
raise BusinessException("只有待审批状态的分配单可以更新")
|
|
||||||
|
|
||||||
return allocation_order.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
async def approve_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
approval_in: AllocationOrderApproval,
|
|
||||||
approver_id: int
|
|
||||||
):
|
|
||||||
"""审批分配单"""
|
|
||||||
db_obj = allocation_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.approval_status != "pending":
|
|
||||||
raise BusinessException("该分配单已审批,无法重复审批")
|
|
||||||
|
|
||||||
# 审批
|
|
||||||
db_obj = allocation_order.approve(
|
|
||||||
db=db,
|
|
||||||
db_obj=db_obj,
|
|
||||||
approval_status=approval_in.approval_status,
|
|
||||||
approver_id=approver_id,
|
|
||||||
approval_remark=approval_in.approval_remark
|
|
||||||
)
|
|
||||||
|
|
||||||
# 如果审批通过,执行分配逻辑
|
|
||||||
if approval_in.approval_status == "approved":
|
|
||||||
await self._execute_allocation_logic(db, db_obj)
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
async def execute_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
executor_id: int
|
|
||||||
):
|
|
||||||
"""执行分配单"""
|
|
||||||
db_obj = allocation_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.approval_status != "approved":
|
|
||||||
raise BusinessException("该分配单未审批通过,无法执行")
|
|
||||||
if db_obj.execute_status == "completed":
|
|
||||||
raise BusinessException("该分配单已执行完成")
|
|
||||||
|
|
||||||
# 执行分配单
|
|
||||||
db_obj = allocation_order.execute(db, db_obj, executor_id)
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
def cancel_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> bool:
|
|
||||||
"""取消分配单"""
|
|
||||||
db_obj = allocation_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.execute_status == "completed":
|
|
||||||
raise BusinessException("已完成的分配单无法取消")
|
|
||||||
|
|
||||||
allocation_order.cancel(db, db_obj)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def delete_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> bool:
|
|
||||||
"""删除分配单"""
|
|
||||||
db_obj = allocation_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
# 只有草稿或已取消的可以删除
|
|
||||||
if db_obj.approval_status not in ["draft", "rejected", "cancelled"]:
|
|
||||||
raise BusinessException("只能删除草稿、已拒绝或已取消的分配单")
|
|
||||||
|
|
||||||
return allocation_order.delete(db, order_id)
|
|
||||||
|
|
||||||
def get_order_items(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> List:
|
|
||||||
"""获取分配单明细"""
|
|
||||||
# 验证分配单存在
|
|
||||||
if not allocation_order.get(db, order_id):
|
|
||||||
raise NotFoundException("分配单")
|
|
||||||
|
|
||||||
return allocation_item.get_by_order(db, order_id)
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
applicant_id: Optional[int] = None
|
|
||||||
) -> Dict[str, int]:
|
|
||||||
"""获取分配单统计信息"""
|
|
||||||
return allocation_order.get_statistics(db, applicant_id)
|
|
||||||
|
|
||||||
async def _execute_allocation_logic(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_obj
|
|
||||||
):
|
|
||||||
"""执行分配逻辑(审批通过后自动执行)"""
|
|
||||||
# 根据单据类型执行不同的逻辑
|
|
||||||
if order_obj.order_type == "allocation":
|
|
||||||
await self._execute_allocation(db, order_obj)
|
|
||||||
elif order_obj.order_type == "transfer":
|
|
||||||
await self._execute_transfer(db, order_obj)
|
|
||||||
elif order_obj.order_type == "recovery":
|
|
||||||
await self._execute_recovery(db, order_obj)
|
|
||||||
elif order_obj.order_type == "maintenance":
|
|
||||||
await self._execute_maintenance_allocation(db, order_obj)
|
|
||||||
elif order_obj.order_type == "scrap":
|
|
||||||
await self._execute_scrap_allocation(db, order_obj)
|
|
||||||
|
|
||||||
async def _execute_allocation(self, db: Session, order_obj):
|
|
||||||
"""执行资产分配"""
|
|
||||||
# 更新明细状态为执行中
|
|
||||||
allocation_item.batch_update_execute_status(db, order_obj.id, "executing")
|
|
||||||
|
|
||||||
# 获取明细
|
|
||||||
items = allocation_item.get_by_order(db, order_obj.id)
|
|
||||||
|
|
||||||
# 更新资产状态
|
|
||||||
from app.services.asset_service import asset_service
|
|
||||||
from app.schemas.asset import AssetStatusTransition
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
try:
|
|
||||||
# 变更资产状态
|
|
||||||
await asset_service.change_asset_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=item.asset_id,
|
|
||||||
status_transition=AssetStatusTransition(
|
|
||||||
new_status=item.to_status,
|
|
||||||
remark=f"分配单: {order_obj.order_code}"
|
|
||||||
),
|
|
||||||
operator_id=order_obj.applicant_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新明细状态为完成
|
|
||||||
allocation_item.update_execute_status(db, item.id, "completed")
|
|
||||||
except Exception as e:
|
|
||||||
# 更新明细状态为失败
|
|
||||||
allocation_item.update_execute_status(
|
|
||||||
db,
|
|
||||||
item.id,
|
|
||||||
"failed",
|
|
||||||
failure_reason=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _execute_transfer(self, db: Session, order_obj):
|
|
||||||
"""执行资产调拨"""
|
|
||||||
# 调拨逻辑与分配类似,但需要记录调出和调入网点
|
|
||||||
await self._execute_allocation(db, order_obj)
|
|
||||||
|
|
||||||
async def _execute_recovery(self, db: Session, order_obj):
|
|
||||||
"""执行资产回收"""
|
|
||||||
# 回收逻辑
|
|
||||||
await self._execute_allocation(db, order_obj)
|
|
||||||
|
|
||||||
async def _execute_maintenance_allocation(self, db: Session, order_obj):
|
|
||||||
"""执行维修分配"""
|
|
||||||
# 维修分配逻辑
|
|
||||||
await self._execute_allocation(db, order_obj)
|
|
||||||
|
|
||||||
async def _execute_scrap_allocation(self, db: Session, order_obj):
|
|
||||||
"""执行报废分配"""
|
|
||||||
# 报废分配逻辑
|
|
||||||
await self._execute_allocation(db, order_obj)
|
|
||||||
|
|
||||||
def _load_order_relations(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""加载分配单关联信息"""
|
|
||||||
from app.models.user import User
|
|
||||||
from app.models.organization import Organization
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"id": obj.id,
|
|
||||||
"order_code": obj.order_code,
|
|
||||||
"order_type": obj.order_type,
|
|
||||||
"title": obj.title,
|
|
||||||
"source_organization_id": obj.source_organization_id,
|
|
||||||
"target_organization_id": obj.target_organization_id,
|
|
||||||
"applicant_id": obj.applicant_id,
|
|
||||||
"approver_id": obj.approver_id,
|
|
||||||
"approval_status": obj.approval_status,
|
|
||||||
"approval_time": obj.approval_time,
|
|
||||||
"approval_remark": obj.approval_remark,
|
|
||||||
"expect_execute_date": obj.expect_execute_date,
|
|
||||||
"actual_execute_date": obj.actual_execute_date,
|
|
||||||
"executor_id": obj.executor_id,
|
|
||||||
"execute_status": obj.execute_status,
|
|
||||||
"remark": obj.remark,
|
|
||||||
"created_at": obj.created_at,
|
|
||||||
"updated_at": obj.updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
if obj.source_organization_id:
|
|
||||||
source_org = db.query(Organization).filter(
|
|
||||||
Organization.id == obj.source_organization_id
|
|
||||||
).first()
|
|
||||||
if source_org:
|
|
||||||
result["source_organization"] = {
|
|
||||||
"id": source_org.id,
|
|
||||||
"org_name": source_org.org_name,
|
|
||||||
"org_type": source_org.org_type
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.target_organization_id:
|
|
||||||
target_org = db.query(Organization).filter(
|
|
||||||
Organization.id == obj.target_organization_id
|
|
||||||
).first()
|
|
||||||
if target_org:
|
|
||||||
result["target_organization"] = {
|
|
||||||
"id": target_org.id,
|
|
||||||
"org_name": target_org.org_name,
|
|
||||||
"org_type": target_org.org_type
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.applicant_id:
|
|
||||||
applicant = db.query(User).filter(User.id == obj.applicant_id).first()
|
|
||||||
if applicant:
|
|
||||||
result["applicant"] = {
|
|
||||||
"id": applicant.id,
|
|
||||||
"real_name": applicant.real_name,
|
|
||||||
"username": applicant.username
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.approver_id:
|
|
||||||
approver = db.query(User).filter(User.id == obj.approver_id).first()
|
|
||||||
if approver:
|
|
||||||
result["approver"] = {
|
|
||||||
"id": approver.id,
|
|
||||||
"real_name": approver.real_name,
|
|
||||||
"username": approver.username
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.executor_id:
|
|
||||||
executor = db.query(User).filter(User.id == obj.executor_id).first()
|
|
||||||
if executor:
|
|
||||||
result["executor"] = {
|
|
||||||
"id": executor.id,
|
|
||||||
"real_name": executor.real_name,
|
|
||||||
"username": executor.username
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载明细
|
|
||||||
items = allocation_item.get_by_order(db, obj.id)
|
|
||||||
result["items"] = [
|
|
||||||
{
|
|
||||||
"id": item.id,
|
|
||||||
"asset_id": item.asset_id,
|
|
||||||
"asset_code": item.asset_code,
|
|
||||||
"asset_name": item.asset_name,
|
|
||||||
"from_status": item.from_status,
|
|
||||||
"to_status": item.to_status,
|
|
||||||
"execute_status": item.execute_status,
|
|
||||||
"failure_reason": item.failure_reason
|
|
||||||
}
|
|
||||||
for item in items
|
|
||||||
]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _can_allocate(self, asset_status: str, order_type: str) -> bool:
|
|
||||||
"""判断资产是否可以分配"""
|
|
||||||
# 库存中或使用中的资产可以分配
|
|
||||||
if order_type in ["allocation", "transfer"]:
|
|
||||||
return asset_status in ["in_stock", "in_use"]
|
|
||||||
elif order_type == "recovery":
|
|
||||||
return asset_status == "in_use"
|
|
||||||
elif order_type == "maintenance":
|
|
||||||
return asset_status in ["in_stock", "in_use"]
|
|
||||||
elif order_type == "scrap":
|
|
||||||
return asset_status in ["in_stock", "in_use", "maintenance"]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_order_type_name(self, order_type: str) -> str:
|
|
||||||
"""获取单据类型中文名"""
|
|
||||||
type_names = {
|
|
||||||
"allocation": "分配",
|
|
||||||
"transfer": "调拨",
|
|
||||||
"recovery": "回收",
|
|
||||||
"maintenance": "维修",
|
|
||||||
"scrap": "报废"
|
|
||||||
}
|
|
||||||
return type_names.get(order_type, "操作")
|
|
||||||
|
|
||||||
async def _generate_order_code(self, db: Session, order_type: str) -> str:
|
|
||||||
"""生成分配单号"""
|
|
||||||
from datetime import datetime
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
# 单据类型前缀
|
|
||||||
prefix_map = {
|
|
||||||
"allocation": "AL",
|
|
||||||
"transfer": "TF",
|
|
||||||
"recovery": "RC",
|
|
||||||
"maintenance": "MT",
|
|
||||||
"scrap": "SC"
|
|
||||||
}
|
|
||||||
prefix = prefix_map.get(order_type, "AL")
|
|
||||||
|
|
||||||
# 日期部分
|
|
||||||
date_str = datetime.now().strftime("%Y%m%d")
|
|
||||||
|
|
||||||
# 序号部分(4位随机数)
|
|
||||||
sequence = "".join(random.choices(string.digits, k=4))
|
|
||||||
|
|
||||||
# 组合单号: AL202501240001
|
|
||||||
order_code = f"{prefix}{date_str}{sequence}"
|
|
||||||
|
|
||||||
# 检查是否重复,如果重复则重新生成
|
|
||||||
while allocation_order.get_by_code(db, order_code):
|
|
||||||
sequence = "".join(random.choices(string.digits, k=4))
|
|
||||||
order_code = f"{prefix}{date_str}{sequence}"
|
|
||||||
|
|
||||||
return order_code
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
allocation_service = AllocationService()
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
"""
|
|
||||||
资产管理业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple, Dict, Any
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.crud.asset import asset, asset_status_history
|
|
||||||
from app.schemas.asset import (
|
|
||||||
AssetCreate,
|
|
||||||
AssetUpdate,
|
|
||||||
AssetStatusTransition
|
|
||||||
)
|
|
||||||
from app.services.state_machine_service import state_machine_service
|
|
||||||
from app.utils.asset_code import generate_asset_code
|
|
||||||
from app.utils.qrcode import generate_qr_code, delete_qr_code
|
|
||||||
from app.core.exceptions import NotFoundException, AlreadyExistsException, StateTransitionException
|
|
||||||
|
|
||||||
|
|
||||||
class AssetService:
|
|
||||||
"""资产服务类"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.state_machine = state_machine_service
|
|
||||||
|
|
||||||
async def get_asset(self, db: Session, asset_id: int):
|
|
||||||
"""获取资产详情"""
|
|
||||||
obj = asset.get(db, asset_id)
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
return self._load_relations(db, obj)
|
|
||||||
|
|
||||||
def get_assets(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
keyword: Optional[str] = None,
|
|
||||||
device_type_id: Optional[int] = None,
|
|
||||||
organization_id: Optional[int] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
purchase_date_start: Optional[Any] = None,
|
|
||||||
purchase_date_end: Optional[Any] = None
|
|
||||||
) -> Tuple[List, int]:
|
|
||||||
"""获取资产列表"""
|
|
||||||
return asset.get_multi(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
keyword=keyword,
|
|
||||||
device_type_id=device_type_id,
|
|
||||||
organization_id=organization_id,
|
|
||||||
status=status,
|
|
||||||
purchase_date_start=purchase_date_start,
|
|
||||||
purchase_date_end=purchase_date_end
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_asset(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AssetCreate,
|
|
||||||
creator_id: int
|
|
||||||
):
|
|
||||||
"""创建资产"""
|
|
||||||
# 检查序列号是否已存在
|
|
||||||
if obj_in.serial_number:
|
|
||||||
existing = asset.get_by_serial_number(db, obj_in.serial_number)
|
|
||||||
if existing:
|
|
||||||
raise AlreadyExistsException("该序列号已被使用")
|
|
||||||
|
|
||||||
# 生成资产编码
|
|
||||||
asset_code = await generate_asset_code(db)
|
|
||||||
|
|
||||||
# 创建资产
|
|
||||||
db_obj = asset.create(db, obj_in, asset_code, creator_id)
|
|
||||||
|
|
||||||
# 生成二维码
|
|
||||||
try:
|
|
||||||
qr_code_url = generate_qr_code(asset_code)
|
|
||||||
db_obj.qr_code_url = qr_code_url
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
except Exception as e:
|
|
||||||
# 二维码生成失败不影响资产创建
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 记录状态历史
|
|
||||||
await self._record_status_change(
|
|
||||||
db=db,
|
|
||||||
asset_id=db_obj.id,
|
|
||||||
old_status=None,
|
|
||||||
new_status="pending",
|
|
||||||
operation_type="create",
|
|
||||||
operator_id=creator_id,
|
|
||||||
operator_name=None, # 可以从用户表获取
|
|
||||||
remark="资产创建"
|
|
||||||
)
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def update_asset(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
obj_in: AssetUpdate,
|
|
||||||
updater_id: int
|
|
||||||
):
|
|
||||||
"""更新资产"""
|
|
||||||
db_obj = asset.get(db, asset_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
# 如果更新序列号,检查是否重复
|
|
||||||
if obj_in.serial_number and obj_in.serial_number != db_obj.serial_number:
|
|
||||||
existing = asset.get_by_serial_number(db, obj_in.serial_number)
|
|
||||||
if existing:
|
|
||||||
raise AlreadyExistsException("该序列号已被使用")
|
|
||||||
|
|
||||||
return asset.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
def delete_asset(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
deleter_id: int
|
|
||||||
) -> bool:
|
|
||||||
"""删除资产"""
|
|
||||||
if not asset.get(db, asset_id):
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
return asset.delete(db, asset_id, deleter_id)
|
|
||||||
|
|
||||||
async def change_asset_status(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
status_transition: AssetStatusTransition,
|
|
||||||
operator_id: int,
|
|
||||||
operator_name: Optional[str] = None
|
|
||||||
):
|
|
||||||
"""变更资产状态"""
|
|
||||||
db_obj = asset.get(db, asset_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
# 验证状态转换
|
|
||||||
error = self.state_machine.validate_transition(
|
|
||||||
db_obj.status,
|
|
||||||
status_transition.new_status
|
|
||||||
)
|
|
||||||
if error:
|
|
||||||
raise StateTransitionException(db_obj.status, status_transition.new_status)
|
|
||||||
|
|
||||||
# 更新状态
|
|
||||||
old_status = db_obj.status
|
|
||||||
asset.update_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_id,
|
|
||||||
new_status=status_transition.new_status,
|
|
||||||
updater_id=operator_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 记录状态历史
|
|
||||||
await self._record_status_change(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_id,
|
|
||||||
old_status=old_status,
|
|
||||||
new_status=status_transition.new_status,
|
|
||||||
operation_type=self._get_operation_type(old_status, status_transition.new_status),
|
|
||||||
operator_id=operator_id,
|
|
||||||
operator_name=operator_name,
|
|
||||||
remark=status_transition.remark,
|
|
||||||
extra_data=status_transition.extra_data
|
|
||||||
)
|
|
||||||
|
|
||||||
# 刷新对象
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def get_asset_status_history(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50
|
|
||||||
) -> List:
|
|
||||||
"""获取资产状态历史"""
|
|
||||||
if not asset.get(db, asset_id):
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
return asset_status_history.get_by_asset(db, asset_id, skip, limit)
|
|
||||||
|
|
||||||
def scan_asset_by_code(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_code: str
|
|
||||||
):
|
|
||||||
"""扫码查询资产"""
|
|
||||||
obj = asset.get_by_code(db, asset_code)
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
return self._load_relations(db, obj)
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
organization_id: Optional[int] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""获取资产统计信息"""
|
|
||||||
query = db.query(
|
|
||||||
func.count(Asset.id).label("total"),
|
|
||||||
func.sum(Asset.purchase_price).label("total_value")
|
|
||||||
).filter(Asset.deleted_at.is_(None))
|
|
||||||
|
|
||||||
if organization_id:
|
|
||||||
query = query.filter(Asset.organization_id == organization_id)
|
|
||||||
|
|
||||||
result = query.first()
|
|
||||||
|
|
||||||
# 按状态统计
|
|
||||||
status_query = db.query(
|
|
||||||
Asset.status,
|
|
||||||
func.count(Asset.id).label("count")
|
|
||||||
).filter(
|
|
||||||
Asset.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
|
|
||||||
if organization_id:
|
|
||||||
status_query = status_query.filter(Asset.organization_id == organization_id)
|
|
||||||
|
|
||||||
status_query = status_query.group_by(Asset.status)
|
|
||||||
status_distribution = {row.status: row.count for row in status_query.all()}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total": result.total or 0,
|
|
||||||
"total_value": float(result.total_value or 0),
|
|
||||||
"status_distribution": status_distribution
|
|
||||||
}
|
|
||||||
|
|
||||||
def _load_relations(self, db: Session, obj):
|
|
||||||
"""加载关联信息"""
|
|
||||||
# 这里可以预加载关联对象
|
|
||||||
# 例如: obj.device_type, obj.brand, obj.organization等
|
|
||||||
return obj
|
|
||||||
|
|
||||||
async def _record_status_change(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
old_status: Optional[str],
|
|
||||||
new_status: str,
|
|
||||||
operation_type: str,
|
|
||||||
operator_id: int,
|
|
||||||
operator_name: Optional[str] = None,
|
|
||||||
organization_id: Optional[int] = None,
|
|
||||||
remark: Optional[str] = None,
|
|
||||||
extra_data: Optional[Dict[str, Any]] = None
|
|
||||||
):
|
|
||||||
"""记录状态变更历史"""
|
|
||||||
asset_status_history.create(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_id,
|
|
||||||
old_status=old_status,
|
|
||||||
new_status=new_status,
|
|
||||||
operation_type=operation_type,
|
|
||||||
operator_id=operator_id,
|
|
||||||
operator_name=operator_name,
|
|
||||||
organization_id=organization_id,
|
|
||||||
remark=remark,
|
|
||||||
extra_data=extra_data
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_operation_type(self, old_status: str, new_status: str) -> str:
|
|
||||||
"""根据状态转换获取操作类型"""
|
|
||||||
operation_map = {
|
|
||||||
("pending", "in_stock"): "in_stock",
|
|
||||||
("in_stock", "in_use"): "allocate",
|
|
||||||
("in_use", "in_stock"): "recover",
|
|
||||||
("in_stock", "transferring"): "transfer",
|
|
||||||
("in_use", "transferring"): "transfer",
|
|
||||||
("transferring", "in_use"): "transfer_complete",
|
|
||||||
("in_stock", "maintenance"): "maintenance",
|
|
||||||
("in_use", "maintenance"): "maintenance",
|
|
||||||
("maintenance", "in_stock"): "maintenance_complete",
|
|
||||||
("maintenance", "in_use"): "maintenance_complete",
|
|
||||||
("in_stock", "pending_scrap"): "pending_scrap",
|
|
||||||
("in_use", "pending_scrap"): "pending_scrap",
|
|
||||||
("pending_scrap", "scrapped"): "scrap",
|
|
||||||
("pending_scrap", "in_stock"): "cancel_scrap",
|
|
||||||
}
|
|
||||||
return operation_map.get((old_status, new_status), "status_change")
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
asset_service = AssetService()
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
"""
|
|
||||||
认证服务
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional
|
|
||||||
from fastapi import status
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.core.security import security_manager
|
|
||||||
from app.core.exceptions import (
|
|
||||||
InvalidCredentialsException,
|
|
||||||
UserLockedException,
|
|
||||||
UserDisabledException,
|
|
||||||
CaptchaException
|
|
||||||
)
|
|
||||||
from app.crud.user import user_crud
|
|
||||||
from app.models.user import User
|
|
||||||
from app.schemas.user import LoginResponse, UserInfo
|
|
||||||
from app.core.config import settings
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
|
||||||
"""认证服务类"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.max_login_failures = 5
|
|
||||||
self.lock_duration_minutes = 30
|
|
||||||
|
|
||||||
async def login(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
username: str,
|
|
||||||
password: str,
|
|
||||||
captcha: str,
|
|
||||||
captcha_key: str
|
|
||||||
) -> LoginResponse:
|
|
||||||
"""
|
|
||||||
用户登录
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
username: 用户名
|
|
||||||
password: 密码
|
|
||||||
captcha: 验证码
|
|
||||||
captcha_key: 验证码UUID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
LoginResponse: 登录响应
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
InvalidCredentialsException: 认证失败
|
|
||||||
UserLockedException: 用户被锁定
|
|
||||||
UserDisabledException: 用户被禁用
|
|
||||||
"""
|
|
||||||
# 验证验证码
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.info(f"开始验证验证码 - captcha_key: {captcha_key}, captcha: {captcha}")
|
|
||||||
|
|
||||||
captcha_valid = await self._verify_captcha(captcha_key, captcha)
|
|
||||||
logger.info(f"验证码验证结果: {captcha_valid}")
|
|
||||||
|
|
||||||
if not captcha_valid:
|
|
||||||
logger.warning(f"验证码验证失败 - captcha_key: {captcha_key}")
|
|
||||||
raise CaptchaException()
|
|
||||||
|
|
||||||
# 获取用户
|
|
||||||
user = await user_crud.get_by_username(db, username)
|
|
||||||
if not user:
|
|
||||||
raise InvalidCredentialsException("用户名或密码错误")
|
|
||||||
|
|
||||||
# 检查用户状态
|
|
||||||
if user.status == "disabled":
|
|
||||||
raise UserDisabledException()
|
|
||||||
|
|
||||||
if user.status == "locked":
|
|
||||||
# 检查是否已过锁定时间
|
|
||||||
if user.locked_until and user.locked_until > datetime.utcnow():
|
|
||||||
raise UserLockedException(f"账户已被锁定,请在 {user.locked_until.strftime('%Y-%m-%d %H:%M:%S')} 后重试")
|
|
||||||
else:
|
|
||||||
# 解锁用户
|
|
||||||
user.status = "active"
|
|
||||||
user.locked_until = None
|
|
||||||
user.login_fail_count = 0
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
# 验证密码
|
|
||||||
if not security_manager.verify_password(password, user.password_hash):
|
|
||||||
# 增加失败次数
|
|
||||||
user.login_fail_count += 1
|
|
||||||
|
|
||||||
# 检查是否需要锁定
|
|
||||||
if user.login_fail_count >= self.max_login_failures:
|
|
||||||
user.status = "locked"
|
|
||||||
user.locked_until = datetime.utcnow() + timedelta(minutes=self.lock_duration_minutes)
|
|
||||||
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
if user.status == "locked":
|
|
||||||
raise UserLockedException(f"密码错误次数过多,账户已被锁定 {self.lock_duration_minutes} 分钟")
|
|
||||||
|
|
||||||
raise InvalidCredentialsException("用户名或密码错误")
|
|
||||||
|
|
||||||
# 登录成功,重置失败次数
|
|
||||||
await user_crud.update_last_login(db, user)
|
|
||||||
|
|
||||||
# 生成Token
|
|
||||||
access_token = security_manager.create_access_token(
|
|
||||||
data={"sub": str(user.id), "username": user.username}
|
|
||||||
)
|
|
||||||
refresh_token = security_manager.create_refresh_token(
|
|
||||||
data={"sub": str(user.id), "username": user.username}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取用户角色和权限
|
|
||||||
user_info = await self._build_user_info(db, user)
|
|
||||||
|
|
||||||
return LoginResponse(
|
|
||||||
access_token=access_token,
|
|
||||||
refresh_token=refresh_token,
|
|
||||||
token_type="Bearer",
|
|
||||||
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
||||||
user=user_info
|
|
||||||
)
|
|
||||||
|
|
||||||
async def refresh_token(self, db: AsyncSession, refresh_token: str) -> dict:
|
|
||||||
"""
|
|
||||||
刷新访问令牌
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
refresh_token: 刷新令牌
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含新的访问令牌
|
|
||||||
"""
|
|
||||||
payload = security_manager.verify_token(refresh_token, token_type="refresh")
|
|
||||||
user_id = int(payload.get("sub"))
|
|
||||||
|
|
||||||
user = await user_crud.get(db, user_id)
|
|
||||||
if not user or user.status != "active":
|
|
||||||
raise InvalidCredentialsException("用户不存在或已被禁用")
|
|
||||||
|
|
||||||
# 生成新的访问令牌
|
|
||||||
access_token = security_manager.create_access_token(
|
|
||||||
data={"sub": str(user.id), "username": user.username}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"access_token": access_token,
|
|
||||||
"expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
|
||||||
}
|
|
||||||
|
|
||||||
async def change_password(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
user: User,
|
|
||||||
old_password: str,
|
|
||||||
new_password: str
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
修改密码
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
user: 当前用户
|
|
||||||
old_password: 旧密码
|
|
||||||
new_password: 新密码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否修改成功
|
|
||||||
"""
|
|
||||||
# 验证旧密码
|
|
||||||
if not security_manager.verify_password(old_password, user.password_hash):
|
|
||||||
raise InvalidCredentialsException("旧密码错误")
|
|
||||||
|
|
||||||
# 更新密码
|
|
||||||
return await user_crud.update_password(db, user, new_password)
|
|
||||||
|
|
||||||
async def reset_password(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
user_id: int,
|
|
||||||
new_password: str
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
重置用户密码(管理员功能)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
user_id: 用户ID
|
|
||||||
new_password: 新密码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否重置成功
|
|
||||||
"""
|
|
||||||
user = await user_crud.get(db, user_id)
|
|
||||||
if not user:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return await user_crud.update_password(db, user, new_password)
|
|
||||||
|
|
||||||
async def _generate_captcha(self) -> dict:
|
|
||||||
"""
|
|
||||||
生成验证码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含captcha_key和captcha_base64的字典
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from app.utils.redis_client import redis_client
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import base64
|
|
||||||
from io import BytesIO
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 生成4位随机验证码,使用更清晰的字符组合(排除易混淆的字符)
|
|
||||||
captcha_text = ''.join(random.choices('23456789', k=4)) # 排除0、1
|
|
||||||
logger.info(f"生成验证码: {captcha_text}")
|
|
||||||
|
|
||||||
# 生成验证码图片
|
|
||||||
width, height = 200, 80 # 增大图片尺寸
|
|
||||||
# 使用浅色背景而不是纯白
|
|
||||||
background_color = (245, 245, 250) # 浅蓝灰色
|
|
||||||
image = Image.new('RGB', (width, height), color=background_color)
|
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
|
|
||||||
# 尝试使用更大的字体
|
|
||||||
try:
|
|
||||||
# 优先使用系统大字体
|
|
||||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
font = ImageFont.truetype("arial.ttf", 48) # 增大到48
|
|
||||||
except:
|
|
||||||
font = ImageFont.load_default()
|
|
||||||
# 如果使用默认字体,尝试放大
|
|
||||||
font = font.font_variant(size=48)
|
|
||||||
|
|
||||||
# 绘制验证码
|
|
||||||
draw.text((10, 5), captcha_text, fill='black', font=font)
|
|
||||||
|
|
||||||
# 减少干扰线数量(从5条减少到3条)
|
|
||||||
for _ in range(3):
|
|
||||||
x1 = random.randint(0, width)
|
|
||||||
y1 = random.randint(0, height)
|
|
||||||
x2 = random.randint(0, width)
|
|
||||||
y2 = random.randint(0, height)
|
|
||||||
draw.line([(x1, y1), (x2, y2)], fill='gray', width=1)
|
|
||||||
|
|
||||||
# 添加噪点(可选)
|
|
||||||
for _ in range(50):
|
|
||||||
x = random.randint(0, width - 1)
|
|
||||||
y = random.randint(0, height - 1)
|
|
||||||
draw.point((x, y), fill='lightgray')
|
|
||||||
|
|
||||||
# 转换为base64
|
|
||||||
buffer = BytesIO()
|
|
||||||
image.save(buffer, format='PNG')
|
|
||||||
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
|
||||||
|
|
||||||
# 生成captcha_key
|
|
||||||
captcha_key = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
|
||||||
logger.info(f"生成验证码Key: {captcha_key}")
|
|
||||||
|
|
||||||
# 存储到Redis,5分钟过期
|
|
||||||
await redis_client.setex(
|
|
||||||
f"captcha:{captcha_key}",
|
|
||||||
300,
|
|
||||||
captcha_text
|
|
||||||
)
|
|
||||||
logger.info(f"验证码已存储到Redis: captcha:{captcha_key}, 值: {captcha_text}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"captcha_key": captcha_key,
|
|
||||||
"captcha_base64": f"data:image/png;base64,{image_base64}"
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"生成验证码失败: {str(e)}", exc_info=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _verify_captcha(self, captcha_key: str, captcha: str) -> bool:
|
|
||||||
"""
|
|
||||||
验证验证码
|
|
||||||
|
|
||||||
Args:
|
|
||||||
captcha_key: 验证码密钥
|
|
||||||
captcha: 用户输入的验证码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
验证是否成功
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from app.utils.redis_client import redis_client
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 从Redis获取存储的验证码
|
|
||||||
stored_captcha = await redis_client.get(f"captcha:{captcha_key}")
|
|
||||||
logger.info(f"Redis中存储的验证码: {stored_captcha}, 用户输入: {captcha}")
|
|
||||||
|
|
||||||
if not stored_captcha:
|
|
||||||
logger.warning(f"验证码已过期或不存在 - captcha_key: {captcha_key}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 验证码不区分大小写
|
|
||||||
is_valid = stored_captcha.lower() == captcha.lower()
|
|
||||||
logger.info(f"验证码匹配结果: {is_valid}")
|
|
||||||
|
|
||||||
return is_valid
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"验证码验证异常: {str(e)}", exc_info=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _build_user_info(self, db: AsyncSession, user: User) -> UserInfo:
|
|
||||||
"""
|
|
||||||
构建用户信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
user: 用户对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
UserInfo: 用户信息
|
|
||||||
"""
|
|
||||||
# 获取用户角色代码列表
|
|
||||||
role_codes = [role.role_code for role in user.roles]
|
|
||||||
|
|
||||||
# 获取用户权限代码列表
|
|
||||||
permissions = []
|
|
||||||
for role in user.roles:
|
|
||||||
for perm in role.permissions:
|
|
||||||
permissions.append(perm.permission_code)
|
|
||||||
|
|
||||||
# 如果是超级管理员,给予所有权限
|
|
||||||
if user.is_admin:
|
|
||||||
permissions = ["*:*:*"]
|
|
||||||
|
|
||||||
return UserInfo(
|
|
||||||
id=user.id,
|
|
||||||
username=user.username,
|
|
||||||
real_name=user.real_name,
|
|
||||||
email=user.email,
|
|
||||||
avatar_url=user.avatar_url,
|
|
||||||
is_admin=user.is_admin,
|
|
||||||
status=user.status
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# 创建服务实例
|
|
||||||
auth_service = AuthService()
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"""
|
|
||||||
品牌和供应商业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.crud.brand_supplier import brand, supplier
|
|
||||||
from app.schemas.brand_supplier import (
|
|
||||||
BrandCreate,
|
|
||||||
BrandUpdate,
|
|
||||||
SupplierCreate,
|
|
||||||
SupplierUpdate
|
|
||||||
)
|
|
||||||
from app.core.exceptions import NotFoundException, AlreadyExistsException
|
|
||||||
|
|
||||||
|
|
||||||
class BrandService:
|
|
||||||
"""品牌服务类"""
|
|
||||||
|
|
||||||
def get_brand(self, db: Session, brand_id: int):
|
|
||||||
"""获取品牌详情"""
|
|
||||||
obj = brand.get(db, brand_id)
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("品牌")
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def get_brands(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List, int]:
|
|
||||||
"""获取品牌列表"""
|
|
||||||
return brand.get_multi(db, skip, limit, status, keyword)
|
|
||||||
|
|
||||||
def create_brand(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: BrandCreate,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""创建品牌"""
|
|
||||||
try:
|
|
||||||
return brand.create(db, obj_in, creator_id)
|
|
||||||
except ValueError as e:
|
|
||||||
raise AlreadyExistsException("品牌") from e
|
|
||||||
|
|
||||||
def update_brand(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
brand_id: int,
|
|
||||||
obj_in: BrandUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""更新品牌"""
|
|
||||||
db_obj = brand.get(db, brand_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("品牌")
|
|
||||||
return brand.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
def delete_brand(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
brand_id: int,
|
|
||||||
deleter_id: Optional[int] = None
|
|
||||||
) -> bool:
|
|
||||||
"""删除品牌"""
|
|
||||||
if not brand.get(db, brand_id):
|
|
||||||
raise NotFoundException("品牌")
|
|
||||||
return brand.delete(db, brand_id, deleter_id)
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierService:
|
|
||||||
"""供应商服务类"""
|
|
||||||
|
|
||||||
def get_supplier(self, db: Session, supplier_id: int):
|
|
||||||
"""获取供应商详情"""
|
|
||||||
obj = supplier.get(db, supplier_id)
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("供应商")
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def get_suppliers(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List, int]:
|
|
||||||
"""获取供应商列表"""
|
|
||||||
return supplier.get_multi(db, skip, limit, status, keyword)
|
|
||||||
|
|
||||||
def create_supplier(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: SupplierCreate,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""创建供应商"""
|
|
||||||
try:
|
|
||||||
return supplier.create(db, obj_in, creator_id)
|
|
||||||
except ValueError as e:
|
|
||||||
raise AlreadyExistsException("供应商") from e
|
|
||||||
|
|
||||||
def update_supplier(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
supplier_id: int,
|
|
||||||
obj_in: SupplierUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""更新供应商"""
|
|
||||||
db_obj = supplier.get(db, supplier_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("供应商")
|
|
||||||
return supplier.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
def delete_supplier(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
supplier_id: int,
|
|
||||||
deleter_id: Optional[int] = None
|
|
||||||
) -> bool:
|
|
||||||
"""删除供应商"""
|
|
||||||
if not supplier.get(db, supplier_id):
|
|
||||||
raise NotFoundException("供应商")
|
|
||||||
return supplier.delete(db, supplier_id, deleter_id)
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
brand_service = BrandService()
|
|
||||||
supplier_service = SupplierService()
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
"""
|
|
||||||
设备类型业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.crud.device_type import device_type, device_type_field
|
|
||||||
from app.schemas.device_type import (
|
|
||||||
DeviceTypeCreate,
|
|
||||||
DeviceTypeUpdate,
|
|
||||||
DeviceTypeFieldCreate,
|
|
||||||
DeviceTypeFieldUpdate
|
|
||||||
)
|
|
||||||
from app.core.exceptions import NotFoundException, AlreadyExistsException
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeService:
|
|
||||||
"""设备类型服务类"""
|
|
||||||
|
|
||||||
def get_device_type(self, db: Session, device_type_id: int, include_fields: bool = False):
|
|
||||||
"""
|
|
||||||
获取设备类型详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
include_fields: 是否包含字段列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
设备类型对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 设备类型不存在
|
|
||||||
"""
|
|
||||||
obj = device_type.get(db, device_type_id)
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("设备类型")
|
|
||||||
|
|
||||||
# 计算字段数量
|
|
||||||
field_count = device_type_field.get_by_device_type(db, device_type_id)
|
|
||||||
obj.field_count = len(field_count)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def get_device_types(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
category: Optional[str] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List, int]:
|
|
||||||
"""
|
|
||||||
获取设备类型列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
category: 设备分类
|
|
||||||
status: 状态
|
|
||||||
keyword: 搜索关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(设备类型列表, 总数)
|
|
||||||
"""
|
|
||||||
items, total = device_type.get_multi(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
category=category,
|
|
||||||
status=status,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
# 为每个项目添加字段数量
|
|
||||||
for item in items:
|
|
||||||
fields = device_type_field.get_by_device_type(db, item.id)
|
|
||||||
item.field_count = len(fields)
|
|
||||||
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def create_device_type(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: DeviceTypeCreate,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的设备类型对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AlreadyExistsException: 设备类型代码已存在
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return device_type.create(db, obj_in, creator_id)
|
|
||||||
except ValueError as e:
|
|
||||||
raise AlreadyExistsException("设备类型") from e
|
|
||||||
|
|
||||||
def update_device_type(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
device_type_id: int,
|
|
||||||
obj_in: DeviceTypeUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新后的设备类型对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 设备类型不存在
|
|
||||||
"""
|
|
||||||
db_obj = device_type.get(db, device_type_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("设备类型")
|
|
||||||
|
|
||||||
return device_type.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
def delete_device_type(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
device_type_id: int,
|
|
||||||
deleter_id: Optional[int] = None
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
删除设备类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否删除成功
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 设备类型不存在
|
|
||||||
"""
|
|
||||||
if not device_type.get(db, device_type_id):
|
|
||||||
raise NotFoundException("设备类型")
|
|
||||||
|
|
||||||
return device_type.delete(db, device_type_id, deleter_id)
|
|
||||||
|
|
||||||
def get_device_type_fields(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
device_type_id: int,
|
|
||||||
status: Optional[str] = None
|
|
||||||
) -> List:
|
|
||||||
"""
|
|
||||||
获取设备类型的字段列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
status: 状态筛选
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
字段列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 设备类型不存在
|
|
||||||
"""
|
|
||||||
# 验证设备类型存在
|
|
||||||
if not device_type.get(db, device_type_id):
|
|
||||||
raise NotFoundException("设备类型")
|
|
||||||
|
|
||||||
return device_type_field.get_by_device_type(db, device_type_id, status)
|
|
||||||
|
|
||||||
def create_device_type_field(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
device_type_id: int,
|
|
||||||
obj_in: DeviceTypeFieldCreate,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建设备类型字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
device_type_id: 设备类型ID
|
|
||||||
obj_in: 创建数据
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的字段对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 设备类型不存在
|
|
||||||
AlreadyExistsException: 字段代码已存在
|
|
||||||
"""
|
|
||||||
# 验证设备类型存在
|
|
||||||
if not device_type.get(db, device_type_id):
|
|
||||||
raise NotFoundException("设备类型")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return device_type_field.create(db, obj_in, device_type_id, creator_id)
|
|
||||||
except ValueError as e:
|
|
||||||
raise AlreadyExistsException("字段") from e
|
|
||||||
|
|
||||||
def update_device_type_field(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
field_id: int,
|
|
||||||
obj_in: DeviceTypeFieldUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新设备类型字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
field_id: 字段ID
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新后的字段对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 字段不存在
|
|
||||||
"""
|
|
||||||
db_obj = device_type_field.get(db, field_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("字段")
|
|
||||||
|
|
||||||
return device_type_field.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
def delete_device_type_field(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
field_id: int,
|
|
||||||
deleter_id: Optional[int] = None
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
删除设备类型字段
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
field_id: 字段ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否删除成功
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 字段不存在
|
|
||||||
"""
|
|
||||||
if not device_type_field.get(db, field_id):
|
|
||||||
raise NotFoundException("字段")
|
|
||||||
|
|
||||||
return device_type_field.delete(db, field_id, deleter_id)
|
|
||||||
|
|
||||||
def get_all_categories(self, db: Session) -> List[str]:
|
|
||||||
"""
|
|
||||||
获取所有设备分类
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
设备分类列表
|
|
||||||
"""
|
|
||||||
return device_type.get_all_categories(db)
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
device_type_service = DeviceTypeService()
|
|
||||||
@@ -1,508 +0,0 @@
|
|||||||
"""
|
|
||||||
文件存储服务
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import secrets
|
|
||||||
import mimetypes
|
|
||||||
from typing import Optional, Dict, Any, List, Tuple
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from fastapi import UploadFile, HTTPException, status
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from PIL import Image
|
|
||||||
import io
|
|
||||||
|
|
||||||
from app.models.file_management import UploadedFile
|
|
||||||
from app.schemas.file_management import (
|
|
||||||
UploadedFileCreate,
|
|
||||||
FileUploadResponse,
|
|
||||||
FileShareResponse,
|
|
||||||
FileStatistics
|
|
||||||
)
|
|
||||||
from app.crud.file_management import uploaded_file as crud_uploaded_file
|
|
||||||
|
|
||||||
|
|
||||||
class FileService:
|
|
||||||
"""文件存储服务"""
|
|
||||||
|
|
||||||
# 允许的文件类型白名单
|
|
||||||
ALLOWED_MIME_TYPES = {
|
|
||||||
# 图片
|
|
||||||
'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', 'image/svg+xml',
|
|
||||||
# 文档
|
|
||||||
'application/pdf', 'application/msword',
|
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
||||||
'application/vnd.ms-excel',
|
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
'application/vnd.ms-powerpoint',
|
|
||||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
||||||
'text/plain', 'text/csv',
|
|
||||||
# 压缩包
|
|
||||||
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
|
||||||
# 其他
|
|
||||||
'application/json', 'application/xml', 'text/xml'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 文件大小限制(字节)- 默认100MB
|
|
||||||
MAX_FILE_SIZE = 100 * 1024 * 1024
|
|
||||||
|
|
||||||
# 图片文件大小限制 - 默认10MB
|
|
||||||
MAX_IMAGE_SIZE = 10 * 1024 * 1024
|
|
||||||
|
|
||||||
# Magic Numbers for file validation
|
|
||||||
MAGIC_NUMBERS = {
|
|
||||||
b'\xFF\xD8\xFF': 'image/jpeg',
|
|
||||||
b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A': 'image/png',
|
|
||||||
b'GIF87a': 'image/gif',
|
|
||||||
b'GIF89a': 'image/gif',
|
|
||||||
b'%PDF': 'application/pdf',
|
|
||||||
b'PK\x03\x04': 'application/zip',
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, base_upload_dir: str = "uploads"):
|
|
||||||
self.base_upload_dir = Path(base_upload_dir)
|
|
||||||
self.ensure_upload_dirs()
|
|
||||||
|
|
||||||
def ensure_upload_dirs(self):
|
|
||||||
"""确保上传目录存在"""
|
|
||||||
directories = [
|
|
||||||
self.base_upload_dir,
|
|
||||||
self.base_upload_dir / "images",
|
|
||||||
self.base_upload_dir / "documents",
|
|
||||||
self.base_upload_dir / "thumbnails",
|
|
||||||
self.base_upload_dir / "temp",
|
|
||||||
]
|
|
||||||
for directory in directories:
|
|
||||||
directory.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
def validate_file_type(self, file: UploadFile) -> bool:
|
|
||||||
"""验证文件类型"""
|
|
||||||
# 检查MIME类型
|
|
||||||
if file.content_type not in self.ALLOWED_MIME_TYPES:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=f"不支持的文件类型: {file.content_type}"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate_file_size(self, file: UploadFile) -> bool:
|
|
||||||
"""验证文件大小"""
|
|
||||||
# 先检查是否是图片
|
|
||||||
if file.content_type and file.content_type.startswith('image/'):
|
|
||||||
max_size = self.MAX_IMAGE_SIZE
|
|
||||||
else:
|
|
||||||
max_size = self.MAX_FILE_SIZE
|
|
||||||
|
|
||||||
# 读取文件内容检查大小
|
|
||||||
content = file.file.read()
|
|
||||||
file.file.seek(0) # 重置文件指针
|
|
||||||
|
|
||||||
if len(content) > max_size:
|
|
||||||
# 转换为MB
|
|
||||||
size_mb = max_size / (1024 * 1024)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=f"文件大小超过限制: {size_mb:.0f}MB"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate_file_content(self, content: bytes) -> str:
|
|
||||||
"""验证文件内容(Magic Number)"""
|
|
||||||
for magic, mime_type in self.MAGIC_NUMBERS.items():
|
|
||||||
if content.startswith(magic):
|
|
||||||
return mime_type
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def upload_file(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
file: UploadFile,
|
|
||||||
uploader_id: int,
|
|
||||||
remark: Optional[str] = None
|
|
||||||
) -> UploadedFile:
|
|
||||||
"""
|
|
||||||
上传文件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
file: 上传的文件
|
|
||||||
uploader_id: 上传者ID
|
|
||||||
remark: 备注
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
UploadedFile: 创建的文件记录
|
|
||||||
"""
|
|
||||||
# 验证文件类型
|
|
||||||
self.validate_file_type(file)
|
|
||||||
|
|
||||||
# 验证文件大小
|
|
||||||
self.validate_file_size(file)
|
|
||||||
|
|
||||||
# 读取文件内容
|
|
||||||
content = await file.read()
|
|
||||||
|
|
||||||
# 验证文件内容
|
|
||||||
detected_mime = self.validate_file_content(content)
|
|
||||||
if detected_mime and detected_mime != file.content_type:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=f"文件内容与扩展名不匹配"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成文件名
|
|
||||||
file_ext = self.get_file_extension(file.filename)
|
|
||||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
|
||||||
|
|
||||||
# 确定存储路径
|
|
||||||
upload_date = datetime.utcnow()
|
|
||||||
date_dir = upload_date.strftime("%Y/%m/%d")
|
|
||||||
save_dir = self.base_upload_dir / date_dir
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
file_path = save_dir / unique_filename
|
|
||||||
|
|
||||||
# 保存文件
|
|
||||||
with open(file_path, "wb") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
# 生成缩略图(如果是图片)
|
|
||||||
thumbnail_path = None
|
|
||||||
if file.content_type and file.content_type.startswith('image/'):
|
|
||||||
thumbnail_path = self.generate_thumbnail(content, unique_filename, date_dir)
|
|
||||||
|
|
||||||
# 创建数据库记录
|
|
||||||
file_create = UploadedFileCreate(
|
|
||||||
file_name=unique_filename,
|
|
||||||
original_name=file.filename,
|
|
||||||
file_path=str(file_path),
|
|
||||||
file_size=len(content),
|
|
||||||
file_type=file.content_type,
|
|
||||||
file_ext=file_ext.lstrip('.'),
|
|
||||||
uploader_id=uploader_id
|
|
||||||
)
|
|
||||||
|
|
||||||
db_obj = crud_uploaded_file.create(db, obj_in=file_create.dict())
|
|
||||||
|
|
||||||
# 更新缩略图路径
|
|
||||||
if thumbnail_path:
|
|
||||||
crud_uploaded_file.update(db, db_obj=db_obj, obj_in={"thumbnail_path": thumbnail_path})
|
|
||||||
|
|
||||||
# 模拟病毒扫描
|
|
||||||
self._scan_virus(file_path)
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def generate_thumbnail(
|
|
||||||
self,
|
|
||||||
content: bytes,
|
|
||||||
filename: str,
|
|
||||||
date_dir: str
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""生成缩略图"""
|
|
||||||
try:
|
|
||||||
# 打开图片
|
|
||||||
image = Image.open(io.BytesIO(content))
|
|
||||||
|
|
||||||
# 转换为RGB(如果是RGBA)
|
|
||||||
if image.mode in ('RGBA', 'P'):
|
|
||||||
image = image.convert('RGB')
|
|
||||||
|
|
||||||
# 创建缩略图
|
|
||||||
thumbnail_size = (200, 200)
|
|
||||||
image.thumbnail(thumbnail_size, Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
# 保存缩略图
|
|
||||||
thumbnail_dir = self.base_upload_dir / "thumbnails" / date_dir
|
|
||||||
thumbnail_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
thumbnail_name = f"thumb_{filename}"
|
|
||||||
thumbnail_path = thumbnail_dir / thumbnail_name
|
|
||||||
image.save(thumbnail_path, 'JPEG', quality=85)
|
|
||||||
|
|
||||||
return str(thumbnail_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"生成缩略图失败: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_file_path(self, file_obj: UploadedFile) -> Path:
|
|
||||||
"""获取文件路径"""
|
|
||||||
return Path(file_obj.file_path)
|
|
||||||
|
|
||||||
def file_exists(self, file_obj: UploadedFile) -> bool:
|
|
||||||
"""检查文件是否存在"""
|
|
||||||
file_path = self.get_file_path(file_obj)
|
|
||||||
return file_path.exists() and file_path.is_file()
|
|
||||||
|
|
||||||
def delete_file_from_disk(self, file_obj: UploadedFile) -> bool:
|
|
||||||
"""从磁盘删除文件"""
|
|
||||||
try:
|
|
||||||
file_path = self.get_file_path(file_obj)
|
|
||||||
if file_path.exists():
|
|
||||||
file_path.unlink()
|
|
||||||
|
|
||||||
# 删除缩略图
|
|
||||||
if file_obj.thumbnail_path:
|
|
||||||
thumbnail_path = Path(file_obj.thumbnail_path)
|
|
||||||
if thumbnail_path.exists():
|
|
||||||
thumbnail_path.unlink()
|
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"删除文件失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def generate_share_link(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
file_id: int,
|
|
||||||
expire_days: int = 7,
|
|
||||||
base_url: str = "http://localhost:8000"
|
|
||||||
) -> FileShareResponse:
|
|
||||||
"""
|
|
||||||
生成分享链接
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
file_id: 文件ID
|
|
||||||
expire_days: 有效期(天)
|
|
||||||
base_url: 基础URL
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
FileShareResponse: 分享链接信息
|
|
||||||
"""
|
|
||||||
# 生成分享码
|
|
||||||
share_code = crud_uploaded_file.generate_share_code(
|
|
||||||
db,
|
|
||||||
file_id=file_id,
|
|
||||||
expire_days=expire_days
|
|
||||||
)
|
|
||||||
|
|
||||||
if not share_code:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="文件不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取文件信息
|
|
||||||
file_obj = crud_uploaded_file.get(db, file_id)
|
|
||||||
expire_time = file_obj.share_expire_time
|
|
||||||
|
|
||||||
# 生成分享URL
|
|
||||||
share_url = f"{base_url}/api/v1/files/share/{share_code}"
|
|
||||||
|
|
||||||
return FileShareResponse(
|
|
||||||
share_code=share_code,
|
|
||||||
share_url=share_url,
|
|
||||||
expire_time=expire_time
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_shared_file(self, db: Session, share_code: str) -> Optional[UploadedFile]:
|
|
||||||
"""通过分享码获取文件"""
|
|
||||||
return crud_uploaded_file.get_by_share_code(db, share_code)
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
uploader_id: Optional[int] = None
|
|
||||||
) -> FileStatistics:
|
|
||||||
"""获取文件统计信息"""
|
|
||||||
stats = crud_uploaded_file.get_statistics(db, uploader_id=uploader_id)
|
|
||||||
return FileStatistics(**stats)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_file_extension(filename: str) -> str:
|
|
||||||
"""获取文件扩展名"""
|
|
||||||
return os.path.splitext(filename)[1]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_mime_type(filename: str) -> str:
|
|
||||||
"""获取MIME类型"""
|
|
||||||
mime_type, _ = mimetypes.guess_type(filename)
|
|
||||||
return mime_type or 'application/octet-stream'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _scan_virus(file_path: Path) -> bool:
|
|
||||||
"""
|
|
||||||
模拟病毒扫描
|
|
||||||
|
|
||||||
实际生产环境应集成专业杀毒软件如:
|
|
||||||
- ClamAV
|
|
||||||
- VirusTotal API
|
|
||||||
- Windows Defender
|
|
||||||
"""
|
|
||||||
# 模拟扫描
|
|
||||||
import time
|
|
||||||
time.sleep(0.1) # 模拟扫描时间
|
|
||||||
return True # 假设文件安全
|
|
||||||
|
|
||||||
|
|
||||||
# 分片上传管理
|
|
||||||
class ChunkUploadManager:
|
|
||||||
"""分片上传管理器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.uploads: Dict[str, Dict[str, Any]] = {}
|
|
||||||
|
|
||||||
def init_upload(
|
|
||||||
self,
|
|
||||||
file_name: str,
|
|
||||||
file_size: int,
|
|
||||||
file_type: str,
|
|
||||||
total_chunks: int,
|
|
||||||
file_hash: Optional[str] = None
|
|
||||||
) -> str:
|
|
||||||
"""初始化分片上传"""
|
|
||||||
upload_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
self.uploads[upload_id] = {
|
|
||||||
"file_name": file_name,
|
|
||||||
"file_size": file_size,
|
|
||||||
"file_type": file_type,
|
|
||||||
"total_chunks": total_chunks,
|
|
||||||
"file_hash": file_hash,
|
|
||||||
"uploaded_chunks": [],
|
|
||||||
"created_at": datetime.utcnow()
|
|
||||||
}
|
|
||||||
|
|
||||||
return upload_id
|
|
||||||
|
|
||||||
def save_chunk(
|
|
||||||
self,
|
|
||||||
upload_id: str,
|
|
||||||
chunk_index: int,
|
|
||||||
chunk_data: bytes
|
|
||||||
) -> bool:
|
|
||||||
"""保存分片"""
|
|
||||||
if upload_id not in self.uploads:
|
|
||||||
return False
|
|
||||||
|
|
||||||
upload_info = self.uploads[upload_id]
|
|
||||||
|
|
||||||
# 保存分片到临时文件
|
|
||||||
temp_dir = Path("uploads/temp")
|
|
||||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
chunk_filename = f"{upload_id}_chunk_{chunk_index}"
|
|
||||||
chunk_path = temp_dir / chunk_filename
|
|
||||||
|
|
||||||
with open(chunk_path, "wb") as f:
|
|
||||||
f.write(chunk_data)
|
|
||||||
|
|
||||||
# 记录已上传的分片
|
|
||||||
if chunk_index not in upload_info["uploaded_chunks"]:
|
|
||||||
upload_info["uploaded_chunks"].append(chunk_index)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def is_complete(self, upload_id: str) -> bool:
|
|
||||||
"""检查是否所有分片都已上传"""
|
|
||||||
if upload_id not in self.uploads:
|
|
||||||
return False
|
|
||||||
|
|
||||||
upload_info = self.uploads[upload_id]
|
|
||||||
return len(upload_info["uploaded_chunks"]) == upload_info["total_chunks"]
|
|
||||||
|
|
||||||
def merge_chunks(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
upload_id: str,
|
|
||||||
uploader_id: int,
|
|
||||||
file_service: FileService
|
|
||||||
) -> UploadedFile:
|
|
||||||
"""合并分片"""
|
|
||||||
if upload_id not in self.uploads:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="上传会话不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.is_complete(upload_id):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="分片未全部上传"
|
|
||||||
)
|
|
||||||
|
|
||||||
upload_info = self.uploads[upload_id]
|
|
||||||
|
|
||||||
# 合并分片
|
|
||||||
temp_dir = Path("uploads/temp")
|
|
||||||
merged_content = b""
|
|
||||||
|
|
||||||
for i in range(upload_info["total_chunks"]):
|
|
||||||
chunk_filename = f"{upload_id}_chunk_{i}"
|
|
||||||
chunk_path = temp_dir / chunk_filename
|
|
||||||
|
|
||||||
if not chunk_path.exists():
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=f"分片 {i} 不存在"
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(chunk_path, "rb") as f:
|
|
||||||
merged_content += f.read()
|
|
||||||
|
|
||||||
# 验证文件大小
|
|
||||||
if len(merged_content) != upload_info["file_size"]:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="文件大小不匹配"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证文件哈希(如果提供)
|
|
||||||
if upload_info["file_hash"]:
|
|
||||||
import hashlib
|
|
||||||
file_hash = hashlib.md5(merged_content).hexdigest()
|
|
||||||
if file_hash != upload_info["file_hash"]:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="文件哈希不匹配"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 保存文件
|
|
||||||
file_ext = Path(upload_info["file_name"]).suffix
|
|
||||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
|
||||||
upload_date = datetime.utcnow()
|
|
||||||
date_dir = upload_date.strftime("%Y/%m/%d")
|
|
||||||
save_dir = Path("uploads") / date_dir
|
|
||||||
save_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
file_path = save_dir / unique_filename
|
|
||||||
|
|
||||||
with open(file_path, "wb") as f:
|
|
||||||
f.write(merged_content)
|
|
||||||
|
|
||||||
# 清理临时文件
|
|
||||||
self.cleanup_upload(upload_id)
|
|
||||||
|
|
||||||
# 创建数据库记录
|
|
||||||
from app.schemas.file_management import UploadedFileCreate
|
|
||||||
file_create = UploadedFileCreate(
|
|
||||||
file_name=unique_filename,
|
|
||||||
original_name=upload_info["file_name"],
|
|
||||||
file_path=str(file_path),
|
|
||||||
file_size=upload_info["file_size"],
|
|
||||||
file_type=upload_info["file_type"],
|
|
||||||
file_ext=file_ext.lstrip('.'),
|
|
||||||
uploader_id=uploader_id
|
|
||||||
)
|
|
||||||
|
|
||||||
db_obj = crud_uploaded_file.create(db, obj_in=file_create.dict())
|
|
||||||
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def cleanup_upload(self, upload_id: str):
|
|
||||||
"""清理上传会话"""
|
|
||||||
if upload_id in self.uploads:
|
|
||||||
del self.uploads[upload_id]
|
|
||||||
|
|
||||||
# 清理临时分片文件
|
|
||||||
temp_dir = Path("uploads/temp")
|
|
||||||
for chunk_file in temp_dir.glob(f"{upload_id}_chunk_*"):
|
|
||||||
chunk_file.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
# 创建服务实例
|
|
||||||
file_service = FileService()
|
|
||||||
chunk_upload_manager = ChunkUploadManager()
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
"""
|
|
||||||
维修管理业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
from sqlalchemy.orm import Session, selectinload
|
|
||||||
from app.crud.maintenance import maintenance_record
|
|
||||||
from app.crud.asset import asset
|
|
||||||
from app.schemas.maintenance import (
|
|
||||||
MaintenanceRecordCreate,
|
|
||||||
MaintenanceRecordUpdate,
|
|
||||||
MaintenanceRecordStart,
|
|
||||||
MaintenanceRecordComplete
|
|
||||||
)
|
|
||||||
from app.core.exceptions import NotFoundException, BusinessException
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceService:
|
|
||||||
"""维修管理服务类"""
|
|
||||||
|
|
||||||
async def get_record(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
record_id: int
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""获取维修记录详情"""
|
|
||||||
# 使用selectinload预加载关联数据,避免N+1查询
|
|
||||||
from app.models.maintenance import MaintenanceRecord
|
|
||||||
from app.models.asset import Asset
|
|
||||||
from app.models.user import User
|
|
||||||
from app.models.brand_supplier import Supplier
|
|
||||||
|
|
||||||
obj = db.query(
|
|
||||||
MaintenanceRecord
|
|
||||||
).options(
|
|
||||||
selectinload(MaintenanceRecord.asset.of_type(Asset)),
|
|
||||||
selectinload(MaintenanceRecord.report_user.of_type(User)),
|
|
||||||
selectinload(MaintenanceRecord.maintenance_user.of_type(User)),
|
|
||||||
selectinload(MaintenanceRecord.vendor.of_type(Supplier))
|
|
||||||
).filter(
|
|
||||||
MaintenanceRecord.id == record_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("维修记录")
|
|
||||||
|
|
||||||
return self._load_relations(db, obj)
|
|
||||||
|
|
||||||
def get_records(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
asset_id: Optional[int] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
fault_type: Optional[str] = None,
|
|
||||||
priority: Optional[str] = None,
|
|
||||||
maintenance_type: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> tuple:
|
|
||||||
"""获取维修记录列表"""
|
|
||||||
items, total = maintenance_record.get_multi(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
asset_id=asset_id,
|
|
||||||
status=status,
|
|
||||||
fault_type=fault_type,
|
|
||||||
priority=priority,
|
|
||||||
maintenance_type=maintenance_type,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
items_with_relations = [self._load_relations(db, item) for item in items]
|
|
||||||
|
|
||||||
return items_with_relations, total
|
|
||||||
|
|
||||||
async def create_record(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: MaintenanceRecordCreate,
|
|
||||||
report_user_id: int,
|
|
||||||
creator_id: int
|
|
||||||
):
|
|
||||||
"""创建维修记录"""
|
|
||||||
# 验证资产存在
|
|
||||||
asset_obj = asset.get(db, obj_in.asset_id)
|
|
||||||
if not asset_obj:
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
# 生成维修单号
|
|
||||||
record_code = await self._generate_record_code(db)
|
|
||||||
|
|
||||||
# 创建维修记录
|
|
||||||
db_obj = maintenance_record.create(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
record_code=record_code,
|
|
||||||
asset_code=asset_obj.asset_code,
|
|
||||||
report_user_id=report_user_id,
|
|
||||||
creator_id=creator_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 如果资产状态不是维修中,则更新状态
|
|
||||||
if asset_obj.status != "maintenance":
|
|
||||||
from app.services.asset_service import asset_service
|
|
||||||
from app.schemas.asset import AssetStatusTransition
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asset_service.change_asset_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_obj.id,
|
|
||||||
status_transition=AssetStatusTransition(
|
|
||||||
new_status="maintenance",
|
|
||||||
remark=f"报修: {record_code}"
|
|
||||||
),
|
|
||||||
operator_id=report_user_id
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# 状态更新失败不影响维修记录创建
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self._load_relations(db, db_obj)
|
|
||||||
|
|
||||||
def update_record(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
record_id: int,
|
|
||||||
obj_in: MaintenanceRecordUpdate,
|
|
||||||
updater_id: int
|
|
||||||
):
|
|
||||||
"""更新维修记录"""
|
|
||||||
db_obj = maintenance_record.get(db, record_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("维修记录")
|
|
||||||
|
|
||||||
# 已完成的维修记录不能更新
|
|
||||||
if db_obj.status == "completed":
|
|
||||||
raise BusinessException("已完成的维修记录不能更新")
|
|
||||||
|
|
||||||
return maintenance_record.update(db, db_obj, obj_in, updater_id)
|
|
||||||
|
|
||||||
async def start_maintenance(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
record_id: int,
|
|
||||||
start_in: MaintenanceRecordStart,
|
|
||||||
maintenance_user_id: int
|
|
||||||
):
|
|
||||||
"""开始维修"""
|
|
||||||
db_obj = maintenance_record.get(db, record_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("维修记录")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.status != "pending":
|
|
||||||
raise BusinessException("只有待处理状态的维修记录可以开始维修")
|
|
||||||
|
|
||||||
# 验证维修类型
|
|
||||||
if start_in.maintenance_type == "vendor_repair" and not start_in.vendor_id:
|
|
||||||
raise BusinessException("外部维修必须指定维修供应商")
|
|
||||||
|
|
||||||
# 开始维修
|
|
||||||
db_obj = maintenance_record.start_maintenance(
|
|
||||||
db=db,
|
|
||||||
db_obj=db_obj,
|
|
||||||
maintenance_type=start_in.maintenance_type,
|
|
||||||
maintenance_user_id=maintenance_user_id,
|
|
||||||
vendor_id=start_in.vendor_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._load_relations(db, db_obj)
|
|
||||||
|
|
||||||
async def complete_maintenance(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
record_id: int,
|
|
||||||
complete_in: MaintenanceRecordComplete,
|
|
||||||
maintenance_user_id: int
|
|
||||||
):
|
|
||||||
"""完成维修"""
|
|
||||||
db_obj = maintenance_record.get(db, record_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("维修记录")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.status != "in_progress":
|
|
||||||
raise BusinessException("只有维修中的记录可以完成")
|
|
||||||
|
|
||||||
# 完成维修
|
|
||||||
db_obj = maintenance_record.complete_maintenance(
|
|
||||||
db=db,
|
|
||||||
db_obj=db_obj,
|
|
||||||
maintenance_result=complete_in.maintenance_result,
|
|
||||||
maintenance_cost=complete_in.maintenance_cost,
|
|
||||||
replaced_parts=complete_in.replaced_parts,
|
|
||||||
images=complete_in.images
|
|
||||||
)
|
|
||||||
|
|
||||||
# 恢复资产状态
|
|
||||||
from app.services.asset_service import asset_service
|
|
||||||
from app.schemas.asset import AssetStatusTransition
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asset_service.change_asset_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=db_obj.asset_id,
|
|
||||||
status_transition=AssetStatusTransition(
|
|
||||||
new_status=complete_in.asset_status,
|
|
||||||
remark=f"维修完成: {db_obj.record_code}"
|
|
||||||
),
|
|
||||||
operator_id=maintenance_user_id
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# 状态更新失败不影响维修记录完成
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self._load_relations(db, db_obj)
|
|
||||||
|
|
||||||
def cancel_maintenance(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
record_id: int
|
|
||||||
):
|
|
||||||
"""取消维修"""
|
|
||||||
db_obj = maintenance_record.get(db, record_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("维修记录")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.status == "completed":
|
|
||||||
raise BusinessException("已完成的维修记录不能取消")
|
|
||||||
|
|
||||||
# 取消维修
|
|
||||||
db_obj = maintenance_record.cancel_maintenance(db, db_obj)
|
|
||||||
|
|
||||||
# 恢复资产状态
|
|
||||||
asset_obj = asset.get(db, db_obj.asset_id)
|
|
||||||
if asset_obj and asset_obj.status == "maintenance":
|
|
||||||
from app.services.asset_service import asset_service
|
|
||||||
from app.schemas.asset import AssetStatusTransition
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 根据维修前的状态恢复
|
|
||||||
target_status = "in_stock" # 默认恢复为库存中
|
|
||||||
asset_service.change_asset_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=asset_obj.id,
|
|
||||||
status_transition=AssetStatusTransition(
|
|
||||||
new_status=target_status,
|
|
||||||
remark=f"取消维修: {db_obj.record_code}"
|
|
||||||
),
|
|
||||||
operator_id=db_obj.report_user_id or 0
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# 状态更新失败不影响维修记录取消
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self._load_relations(db, db_obj)
|
|
||||||
|
|
||||||
def delete_record(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
record_id: int
|
|
||||||
) -> bool:
|
|
||||||
"""删除维修记录"""
|
|
||||||
db_obj = maintenance_record.get(db, record_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("维修记录")
|
|
||||||
|
|
||||||
# 只能删除待处理或已取消的记录
|
|
||||||
if db_obj.status not in ["pending", "cancelled"]:
|
|
||||||
raise BusinessException("只能删除待处理或已取消的维修记录")
|
|
||||||
|
|
||||||
return maintenance_record.delete(db, record_id)
|
|
||||||
|
|
||||||
def get_asset_records(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: int,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50
|
|
||||||
) -> List:
|
|
||||||
"""获取资产的维修记录"""
|
|
||||||
# 验证资产存在
|
|
||||||
if not asset.get(db, asset_id):
|
|
||||||
raise NotFoundException("资产")
|
|
||||||
|
|
||||||
records = maintenance_record.get_by_asset(db, asset_id, skip, limit)
|
|
||||||
return [self._load_relations(db, record) for record in records]
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
asset_id: Optional[int] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""获取维修统计信息"""
|
|
||||||
return maintenance_record.get_statistics(db, asset_id)
|
|
||||||
|
|
||||||
def _load_relations(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""加载维修记录关联信息"""
|
|
||||||
from app.models.asset import Asset
|
|
||||||
from app.models.user import User
|
|
||||||
from app.models.brand_supplier import Supplier
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"id": obj.id,
|
|
||||||
"record_code": obj.record_code,
|
|
||||||
"asset_id": obj.asset_id,
|
|
||||||
"asset_code": obj.asset_code,
|
|
||||||
"fault_description": obj.fault_description,
|
|
||||||
"fault_type": obj.fault_type,
|
|
||||||
"report_user_id": obj.report_user_id,
|
|
||||||
"report_time": obj.report_time,
|
|
||||||
"priority": obj.priority,
|
|
||||||
"maintenance_type": obj.maintenance_type,
|
|
||||||
"vendor_id": obj.vendor_id,
|
|
||||||
"maintenance_cost": float(obj.maintenance_cost) if obj.maintenance_cost else None,
|
|
||||||
"start_time": obj.start_time,
|
|
||||||
"complete_time": obj.complete_time,
|
|
||||||
"maintenance_user_id": obj.maintenance_user_id,
|
|
||||||
"maintenance_result": obj.maintenance_result,
|
|
||||||
"replaced_parts": obj.replaced_parts,
|
|
||||||
"status": obj.status,
|
|
||||||
"images": obj.images,
|
|
||||||
"remark": obj.remark,
|
|
||||||
"created_at": obj.created_at,
|
|
||||||
"updated_at": obj.updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载资产信息
|
|
||||||
if obj.asset_id:
|
|
||||||
asset_obj = db.query(Asset).filter(Asset.id == obj.asset_id).first()
|
|
||||||
if asset_obj:
|
|
||||||
result["asset"] = {
|
|
||||||
"id": asset_obj.id,
|
|
||||||
"asset_code": asset_obj.asset_code,
|
|
||||||
"asset_name": asset_obj.asset_name,
|
|
||||||
"status": asset_obj.status
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载报修人信息
|
|
||||||
if obj.report_user_id:
|
|
||||||
report_user = db.query(User).filter(User.id == obj.report_user_id).first()
|
|
||||||
if report_user:
|
|
||||||
result["report_user"] = {
|
|
||||||
"id": report_user.id,
|
|
||||||
"real_name": report_user.real_name,
|
|
||||||
"username": report_user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载维修人员信息
|
|
||||||
if obj.maintenance_user_id:
|
|
||||||
maintenance_user = db.query(User).filter(User.id == obj.maintenance_user_id).first()
|
|
||||||
if maintenance_user:
|
|
||||||
result["maintenance_user"] = {
|
|
||||||
"id": maintenance_user.id,
|
|
||||||
"real_name": maintenance_user.real_name,
|
|
||||||
"username": maintenance_user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载供应商信息
|
|
||||||
if obj.vendor_id:
|
|
||||||
vendor = db.query(Supplier).filter(Supplier.id == obj.vendor_id).first()
|
|
||||||
if vendor:
|
|
||||||
result["vendor"] = {
|
|
||||||
"id": vendor.id,
|
|
||||||
"supplier_name": vendor.supplier_name,
|
|
||||||
"contact_person": vendor.contact_person,
|
|
||||||
"contact_phone": vendor.contact_phone
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def _generate_record_code(self, db: Session) -> str:
|
|
||||||
"""生成维修单号"""
|
|
||||||
from datetime import datetime
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
# 日期部分
|
|
||||||
date_str = datetime.now().strftime("%Y%m%d")
|
|
||||||
|
|
||||||
# 序号部分(4位随机数)
|
|
||||||
sequence = "".join(random.choices(string.digits, k=4))
|
|
||||||
|
|
||||||
# 组合单号: MT202501240001
|
|
||||||
record_code = f"MT{date_str}{sequence}"
|
|
||||||
|
|
||||||
# 检查是否重复,如果重复则重新生成
|
|
||||||
while maintenance_record.get_by_code(db, record_code):
|
|
||||||
sequence = "".join(random.choices(string.digits, k=4))
|
|
||||||
record_code = f"MT{date_str}{sequence}"
|
|
||||||
|
|
||||||
return record_code
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
maintenance_service = MaintenanceService()
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
"""
|
|
||||||
消息通知服务层
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy import select
|
|
||||||
from app.crud.notification import notification_crud
|
|
||||||
from app.models.notification import NotificationTemplate
|
|
||||||
from app.models.user import User
|
|
||||||
from app.schemas.notification import (
|
|
||||||
NotificationCreate,
|
|
||||||
NotificationBatchCreate,
|
|
||||||
NotificationSendFromTemplate
|
|
||||||
)
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationService:
|
|
||||||
"""消息通知服务类"""
|
|
||||||
|
|
||||||
async def get_notification(self, db: AsyncSession, notification_id: int) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取消息通知详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_id: 通知ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
通知信息
|
|
||||||
"""
|
|
||||||
notification = await notification_crud.get(db, notification_id)
|
|
||||||
if not notification:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": notification.id,
|
|
||||||
"recipient_id": notification.recipient_id,
|
|
||||||
"recipient_name": notification.recipient_name,
|
|
||||||
"title": notification.title,
|
|
||||||
"content": notification.content,
|
|
||||||
"notification_type": notification.notification_type,
|
|
||||||
"priority": notification.priority,
|
|
||||||
"is_read": notification.is_read,
|
|
||||||
"read_at": notification.read_at,
|
|
||||||
"related_entity_type": notification.related_entity_type,
|
|
||||||
"related_entity_id": notification.related_entity_id,
|
|
||||||
"action_url": notification.action_url,
|
|
||||||
"extra_data": notification.extra_data,
|
|
||||||
"sent_via_email": notification.sent_via_email,
|
|
||||||
"sent_via_sms": notification.sent_via_sms,
|
|
||||||
"created_at": notification.created_at,
|
|
||||||
"expire_at": notification.expire_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_notifications(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
recipient_id: Optional[int] = None,
|
|
||||||
notification_type: Optional[str] = None,
|
|
||||||
priority: Optional[str] = None,
|
|
||||||
is_read: Optional[bool] = None,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取消息通知列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
notification_type: 通知类型
|
|
||||||
priority: 优先级
|
|
||||||
is_read: 是否已读
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
keyword: 关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
通知列表和总数
|
|
||||||
"""
|
|
||||||
items, total = await notification_crud.get_multi(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
recipient_id=recipient_id,
|
|
||||||
notification_type=notification_type,
|
|
||||||
priority=priority,
|
|
||||||
is_read=is_read,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": item.id,
|
|
||||||
"recipient_id": item.recipient_id,
|
|
||||||
"recipient_name": item.recipient_name,
|
|
||||||
"title": item.title,
|
|
||||||
"content": item.content,
|
|
||||||
"notification_type": item.notification_type,
|
|
||||||
"priority": item.priority,
|
|
||||||
"is_read": item.is_read,
|
|
||||||
"read_at": item.read_at,
|
|
||||||
"action_url": item.action_url,
|
|
||||||
"created_at": item.created_at,
|
|
||||||
}
|
|
||||||
for item in items
|
|
||||||
],
|
|
||||||
"total": total
|
|
||||||
}
|
|
||||||
|
|
||||||
async def create_notification(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
obj_in: NotificationCreate
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
创建消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的通知信息
|
|
||||||
"""
|
|
||||||
# 获取接收人信息
|
|
||||||
user_result = await db.execute(
|
|
||||||
select(User).where(User.id == obj_in.recipient_id)
|
|
||||||
)
|
|
||||||
user = user_result.scalar_one_or_none()
|
|
||||||
if not user:
|
|
||||||
raise ValueError("接收人不存在")
|
|
||||||
|
|
||||||
# 转换为字典
|
|
||||||
obj_in_data = obj_in.model_dump()
|
|
||||||
obj_in_data["recipient_name"] = user.real_name
|
|
||||||
|
|
||||||
# 处理复杂类型
|
|
||||||
if obj_in_data.get("extra_data"):
|
|
||||||
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
|
|
||||||
|
|
||||||
# 设置邮件和短信发送标记
|
|
||||||
obj_in_data["sent_via_email"] = obj_in_data.pop("send_email", False)
|
|
||||||
obj_in_data["sent_via_sms"] = obj_in_data.pop("send_sms", False)
|
|
||||||
|
|
||||||
notification = await notification_crud.create(db, obj_in=obj_in_data)
|
|
||||||
|
|
||||||
# TODO: 发送邮件和短信
|
|
||||||
# if notification.sent_via_email:
|
|
||||||
# await self._send_email(notification)
|
|
||||||
# if notification.sent_via_sms:
|
|
||||||
# await self._send_sms(notification)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": notification.id,
|
|
||||||
"recipient_id": notification.recipient_id,
|
|
||||||
"title": notification.title,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def batch_create_notifications(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
batch_in: NotificationBatchCreate
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
批量创建消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
batch_in: 批量创建数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建结果
|
|
||||||
"""
|
|
||||||
# 获取接收人信息
|
|
||||||
user_results = await db.execute(
|
|
||||||
select(User).where(User.id.in_(batch_in.recipient_ids))
|
|
||||||
)
|
|
||||||
users = {user.id: user.real_name for user in user_results.scalars()}
|
|
||||||
|
|
||||||
# 准备通知数据
|
|
||||||
notification_data = {
|
|
||||||
"title": batch_in.title,
|
|
||||||
"content": batch_in.content,
|
|
||||||
"notification_type": batch_in.notification_type.value,
|
|
||||||
"priority": batch_in.priority.value,
|
|
||||||
"action_url": batch_in.action_url,
|
|
||||||
"extra_data": json.loads(batch_in.extra_data.model_dump_json()) if batch_in.extra_data else {},
|
|
||||||
}
|
|
||||||
|
|
||||||
# 批量创建
|
|
||||||
notifications = await notification_crud.batch_create(
|
|
||||||
db,
|
|
||||||
recipient_ids=batch_in.recipient_ids,
|
|
||||||
notification_data=notification_data
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新接收人姓名
|
|
||||||
for notification in notifications:
|
|
||||||
notification.recipient_name = users.get(notification.recipient_id, "")
|
|
||||||
|
|
||||||
await db.flush()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"count": len(notifications),
|
|
||||||
"notification_ids": [n.id for n in notifications]
|
|
||||||
}
|
|
||||||
|
|
||||||
async def mark_as_read(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
notification_id: int
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
标记为已读
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_id: 通知ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新结果
|
|
||||||
"""
|
|
||||||
notification = await notification_crud.mark_as_read(
|
|
||||||
db,
|
|
||||||
notification_id=notification_id,
|
|
||||||
read_at=datetime.utcnow()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not notification:
|
|
||||||
raise ValueError("通知不存在")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": notification.id,
|
|
||||||
"is_read": notification.is_read,
|
|
||||||
"read_at": notification.read_at
|
|
||||||
}
|
|
||||||
|
|
||||||
async def mark_all_as_read(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
recipient_id: int
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
标记所有未读为已读
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新结果
|
|
||||||
"""
|
|
||||||
count = await notification_crud.mark_all_as_read(
|
|
||||||
db,
|
|
||||||
recipient_id=recipient_id,
|
|
||||||
read_at=datetime.utcnow()
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"count": count,
|
|
||||||
"message": f"已标记 {count} 条通知为已读"
|
|
||||||
}
|
|
||||||
|
|
||||||
async def delete_notification(self, db: AsyncSession, notification_id: int) -> None:
|
|
||||||
"""
|
|
||||||
删除消息通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_id: 通知ID
|
|
||||||
"""
|
|
||||||
await notification_crud.delete(db, notification_id=notification_id)
|
|
||||||
|
|
||||||
async def batch_delete_notifications(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
notification_ids: List[int]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
批量删除通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
notification_ids: 通知ID列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除结果
|
|
||||||
"""
|
|
||||||
count = await notification_crud.batch_delete(db, notification_ids=notification_ids)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"count": count,
|
|
||||||
"message": f"已删除 {count} 条通知"
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_unread_count(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取未读通知数量
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
未读数量
|
|
||||||
"""
|
|
||||||
count = await notification_crud.get_unread_count(db, recipient_id)
|
|
||||||
|
|
||||||
return {"unread_count": count}
|
|
||||||
|
|
||||||
async def get_statistics(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取通知统计信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
recipient_id: 接收人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
统计信息
|
|
||||||
"""
|
|
||||||
return await notification_crud.get_statistics(db, recipient_id)
|
|
||||||
|
|
||||||
async def send_from_template(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
template_in: NotificationSendFromTemplate
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
从模板发送通知
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
template_in: 模板发送数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
发送结果
|
|
||||||
"""
|
|
||||||
# 获取模板
|
|
||||||
result = await db.execute(
|
|
||||||
select(NotificationTemplate).where(
|
|
||||||
and_(
|
|
||||||
NotificationTemplate.template_code == template_in.template_code,
|
|
||||||
NotificationTemplate.is_active == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
template = result.scalar_one_or_none()
|
|
||||||
if not template:
|
|
||||||
raise ValueError(f"通知模板 {template_in.template_code} 不存在或未启用")
|
|
||||||
|
|
||||||
# 渲染标题和内容
|
|
||||||
title = self._render_template(template.title_template, template_in.variables)
|
|
||||||
content = self._render_template(template.content_template, template_in.variables)
|
|
||||||
|
|
||||||
# 创建批量通知数据
|
|
||||||
batch_data = NotificationBatchCreate(
|
|
||||||
recipient_ids=template_in.recipient_ids,
|
|
||||||
title=title,
|
|
||||||
content=content,
|
|
||||||
notification_type=template.notification_type,
|
|
||||||
priority=template.priority,
|
|
||||||
action_url=template_in.action_url,
|
|
||||||
extra_data={
|
|
||||||
"template_code": template.template_code,
|
|
||||||
"variables": template_in.variables
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return await self.batch_create_notifications(db, batch_data)
|
|
||||||
|
|
||||||
def _render_template(self, template: str, variables: Dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
渲染模板
|
|
||||||
|
|
||||||
Args:
|
|
||||||
template: 模板字符串
|
|
||||||
variables: 变量字典
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
渲染后的字符串
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return template.format(**variables)
|
|
||||||
except KeyError as e:
|
|
||||||
raise ValueError(f"模板变量缺失: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
notification_service = NotificationService()
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
"""
|
|
||||||
操作日志服务层
|
|
||||||
"""
|
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from app.crud.operation_log import operation_log_crud
|
|
||||||
from app.schemas.operation_log import OperationLogCreate
|
|
||||||
|
|
||||||
|
|
||||||
class OperationLogService:
|
|
||||||
"""操作日志服务类"""
|
|
||||||
|
|
||||||
async def get_log(self, db: AsyncSession, log_id: int) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取操作日志详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
log_id: 日志ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
日志信息
|
|
||||||
"""
|
|
||||||
log = await operation_log_crud.get(db, log_id)
|
|
||||||
if not log:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": log.id,
|
|
||||||
"operator_id": log.operator_id,
|
|
||||||
"operator_name": log.operator_name,
|
|
||||||
"operator_ip": log.operator_ip,
|
|
||||||
"module": log.module,
|
|
||||||
"operation_type": log.operation_type,
|
|
||||||
"method": log.method,
|
|
||||||
"url": log.url,
|
|
||||||
"params": log.params,
|
|
||||||
"result": log.result,
|
|
||||||
"error_msg": log.error_msg,
|
|
||||||
"duration": log.duration,
|
|
||||||
"user_agent": log.user_agent,
|
|
||||||
"extra_data": log.extra_data,
|
|
||||||
"created_at": log.created_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_logs(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
operator_id: Optional[int] = None,
|
|
||||||
operator_name: Optional[str] = None,
|
|
||||||
module: Optional[str] = None,
|
|
||||||
operation_type: Optional[str] = None,
|
|
||||||
result: Optional[str] = None,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取操作日志列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
operator_id: 操作人ID
|
|
||||||
operator_name: 操作人姓名
|
|
||||||
module: 模块名称
|
|
||||||
operation_type: 操作类型
|
|
||||||
result: 操作结果
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
keyword: 关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
日志列表和总数
|
|
||||||
"""
|
|
||||||
items, total = await operation_log_crud.get_multi(
|
|
||||||
db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
operator_id=operator_id,
|
|
||||||
operator_name=operator_name,
|
|
||||||
module=module,
|
|
||||||
operation_type=operation_type,
|
|
||||||
result=result,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": item.id,
|
|
||||||
"operator_id": item.operator_id,
|
|
||||||
"operator_name": item.operator_name,
|
|
||||||
"operator_ip": item.operator_ip,
|
|
||||||
"module": item.module,
|
|
||||||
"operation_type": item.operation_type,
|
|
||||||
"method": item.method,
|
|
||||||
"url": item.url,
|
|
||||||
"result": item.result,
|
|
||||||
"error_msg": item.error_msg,
|
|
||||||
"duration": item.duration,
|
|
||||||
"created_at": item.created_at,
|
|
||||||
}
|
|
||||||
for item in items
|
|
||||||
],
|
|
||||||
"total": total
|
|
||||||
}
|
|
||||||
|
|
||||||
async def create_log(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
obj_in: OperationLogCreate
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
创建操作日志
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的日志信息
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
# 转换为字典
|
|
||||||
obj_in_data = obj_in.model_dump()
|
|
||||||
|
|
||||||
# 处理复杂类型
|
|
||||||
if obj_in_data.get("extra_data"):
|
|
||||||
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
|
|
||||||
|
|
||||||
log = await operation_log_crud.create(db, obj_in=obj_in_data)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": log.id,
|
|
||||||
"operator_name": log.operator_name,
|
|
||||||
"module": log.module,
|
|
||||||
"operation_type": log.operation_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_statistics(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取操作日志统计信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
统计信息
|
|
||||||
"""
|
|
||||||
return await operation_log_crud.get_statistics(
|
|
||||||
db,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_operator_top(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
limit: int = 10,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取操作排行榜
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
limit: 返回条数
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
操作排行列表
|
|
||||||
"""
|
|
||||||
return await operation_log_crud.get_operator_top(
|
|
||||||
db,
|
|
||||||
limit=limit,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_old_logs(self, db: AsyncSession, *, days: int = 90) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
删除旧日志
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
days: 保留天数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
删除结果
|
|
||||||
"""
|
|
||||||
count = await operation_log_crud.delete_old_logs(db, days=days)
|
|
||||||
return {
|
|
||||||
"deleted_count": count,
|
|
||||||
"message": f"已删除 {count} 条 {days} 天前的日志"
|
|
||||||
}
|
|
||||||
|
|
||||||
async def export_logs(
|
|
||||||
self,
|
|
||||||
db: AsyncSession,
|
|
||||||
*,
|
|
||||||
start_time: Optional[datetime] = None,
|
|
||||||
end_time: Optional[datetime] = None,
|
|
||||||
operator_id: Optional[int] = None,
|
|
||||||
module: Optional[str] = None,
|
|
||||||
operation_type: Optional[str] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
导出操作日志
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
start_time: 开始时间
|
|
||||||
end_time: 结束时间
|
|
||||||
operator_id: 操作人ID
|
|
||||||
module: 模块名称
|
|
||||||
operation_type: 操作类型
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
日志列表
|
|
||||||
"""
|
|
||||||
items, total = await operation_log_crud.get_multi(
|
|
||||||
db,
|
|
||||||
skip=0,
|
|
||||||
limit=10000, # 导出限制
|
|
||||||
operator_id=operator_id,
|
|
||||||
module=module,
|
|
||||||
operation_type=operation_type,
|
|
||||||
start_time=start_time,
|
|
||||||
end_time=end_time
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"操作人": item.operator_name,
|
|
||||||
"模块": item.module,
|
|
||||||
"操作类型": item.operation_type,
|
|
||||||
"请求方法": item.method,
|
|
||||||
"请求URL": item.url,
|
|
||||||
"操作结果": item.result,
|
|
||||||
"错误信息": item.error_msg or "",
|
|
||||||
"执行时长(毫秒)": item.duration or 0,
|
|
||||||
"操作时间": item.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"操作IP": item.operator_ip or "",
|
|
||||||
}
|
|
||||||
for item in items
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
operation_log_service = OperationLogService()
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
"""
|
|
||||||
机构网点业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from app.crud.organization import organization
|
|
||||||
from app.schemas.organization import OrganizationCreate, OrganizationUpdate
|
|
||||||
from app.core.exceptions import NotFoundException, AlreadyExistsException
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationService:
|
|
||||||
"""机构网点服务类"""
|
|
||||||
|
|
||||||
def get_organization(self, db: Session, org_id: int):
|
|
||||||
"""
|
|
||||||
获取机构详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
org_id: 机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
机构对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 机构不存在
|
|
||||||
"""
|
|
||||||
obj = organization.get(db, org_id)
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("机构")
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def get_organizations(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
org_type: Optional[str] = None,
|
|
||||||
status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> Tuple[List, int]:
|
|
||||||
"""
|
|
||||||
获取机构列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
skip: 跳过条数
|
|
||||||
limit: 返回条数
|
|
||||||
org_type: 机构类型
|
|
||||||
status: 状态
|
|
||||||
keyword: 搜索关键词
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(机构列表, 总数)
|
|
||||||
"""
|
|
||||||
return organization.get_multi(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
org_type=org_type,
|
|
||||||
status=status,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_organization_tree(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
status: Optional[str] = None
|
|
||||||
) -> List:
|
|
||||||
"""
|
|
||||||
获取机构树
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
status: 状态筛选
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
机构树列表
|
|
||||||
"""
|
|
||||||
return organization.get_tree(db, status)
|
|
||||||
|
|
||||||
def get_organization_children(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
parent_id: int
|
|
||||||
) -> List:
|
|
||||||
"""
|
|
||||||
获取直接子机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
parent_id: 父机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
子机构列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 父机构不存在
|
|
||||||
"""
|
|
||||||
if parent_id > 0 and not organization.get(db, parent_id):
|
|
||||||
raise NotFoundException("父机构")
|
|
||||||
|
|
||||||
return organization.get_children(db, parent_id)
|
|
||||||
|
|
||||||
def get_all_children(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
parent_id: int
|
|
||||||
) -> List:
|
|
||||||
"""
|
|
||||||
递归获取所有子机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
parent_id: 父机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
所有子机构列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 父机构不存在
|
|
||||||
"""
|
|
||||||
if not organization.get(db, parent_id):
|
|
||||||
raise NotFoundException("机构")
|
|
||||||
|
|
||||||
return organization.get_all_children(db, parent_id)
|
|
||||||
|
|
||||||
def get_parents(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
child_id: int
|
|
||||||
) -> List:
|
|
||||||
"""
|
|
||||||
递归获取所有父机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
child_id: 子机构ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
所有父机构列表
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 机构不存在
|
|
||||||
"""
|
|
||||||
if not organization.get(db, child_id):
|
|
||||||
raise NotFoundException("机构")
|
|
||||||
|
|
||||||
return organization.get_parents(db, child_id)
|
|
||||||
|
|
||||||
def create_organization(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: OrganizationCreate,
|
|
||||||
creator_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
obj_in: 创建数据
|
|
||||||
creator_id: 创建人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创建的机构对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AlreadyExistsException: 机构代码已存在
|
|
||||||
NotFoundException: 父机构不存在
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return organization.create(db, obj_in, creator_id)
|
|
||||||
except ValueError as e:
|
|
||||||
if "不存在" in str(e):
|
|
||||||
raise NotFoundException("父机构") from e
|
|
||||||
raise AlreadyExistsException("机构") from e
|
|
||||||
|
|
||||||
def update_organization(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
org_id: int,
|
|
||||||
obj_in: OrganizationUpdate,
|
|
||||||
updater_id: Optional[int] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
更新机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
org_id: 机构ID
|
|
||||||
obj_in: 更新数据
|
|
||||||
updater_id: 更新人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
更新后的机构对象
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 机构不存在
|
|
||||||
"""
|
|
||||||
db_obj = organization.get(db, org_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("机构")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return organization.update(db, db_obj, obj_in, updater_id)
|
|
||||||
except ValueError as e:
|
|
||||||
if "不存在" in str(e):
|
|
||||||
raise NotFoundException("父机构") from e
|
|
||||||
raise
|
|
||||||
|
|
||||||
def delete_organization(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
org_id: int,
|
|
||||||
deleter_id: Optional[int] = None
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
删除机构
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
org_id: 机构ID
|
|
||||||
deleter_id: 删除人ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否删除成功
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: 机构不存在
|
|
||||||
ValueError: 机构下存在子机构
|
|
||||||
"""
|
|
||||||
if not organization.get(db, org_id):
|
|
||||||
raise NotFoundException("机构")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return organization.delete(db, org_id, deleter_id)
|
|
||||||
except ValueError as e:
|
|
||||||
if "子机构" in str(e):
|
|
||||||
raise ValueError("该机构下存在子机构,无法删除") from e
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
organization_service = OrganizationService()
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
"""
|
|
||||||
资产回收业务服务层
|
|
||||||
"""
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
from sqlalchemy.orm import Session, selectinload
|
|
||||||
from app.crud.recovery import recovery_order, recovery_item
|
|
||||||
from app.crud.asset import asset
|
|
||||||
from app.schemas.recovery import (
|
|
||||||
AssetRecoveryOrderCreate,
|
|
||||||
AssetRecoveryOrderUpdate
|
|
||||||
)
|
|
||||||
from app.core.exceptions import NotFoundException, BusinessException
|
|
||||||
|
|
||||||
|
|
||||||
class RecoveryService:
|
|
||||||
"""资产回收服务类"""
|
|
||||||
|
|
||||||
async def get_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""获取回收单详情"""
|
|
||||||
# 使用selectinload预加载关联数据,避免N+1查询
|
|
||||||
from app.models.recovery import AssetRecoveryOrder
|
|
||||||
from app.models.user import User
|
|
||||||
from app.models.recovery import AssetRecoveryItem
|
|
||||||
|
|
||||||
obj = db.query(
|
|
||||||
AssetRecoveryOrder
|
|
||||||
).options(
|
|
||||||
selectinload(AssetRecoveryOrder.items),
|
|
||||||
selectinload(AssetRecoveryOrder.applicant.of_type(User)),
|
|
||||||
selectinload(AssetRecoveryOrder.approver.of_type(User)),
|
|
||||||
selectinload(AssetRecoveryOrder.executor.of_type(User))
|
|
||||||
).filter(
|
|
||||||
AssetRecoveryOrder.id == order_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
return self._load_order_relations(db, obj)
|
|
||||||
|
|
||||||
def get_orders(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 20,
|
|
||||||
recovery_type: Optional[str] = None,
|
|
||||||
approval_status: Optional[str] = None,
|
|
||||||
execute_status: Optional[str] = None,
|
|
||||||
keyword: Optional[str] = None
|
|
||||||
) -> tuple:
|
|
||||||
"""获取回收单列表"""
|
|
||||||
items, total = recovery_order.get_multi(
|
|
||||||
db=db,
|
|
||||||
skip=skip,
|
|
||||||
limit=limit,
|
|
||||||
recovery_type=recovery_type,
|
|
||||||
approval_status=approval_status,
|
|
||||||
execute_status=execute_status,
|
|
||||||
keyword=keyword
|
|
||||||
)
|
|
||||||
|
|
||||||
# 加载关联信息
|
|
||||||
items_with_relations = [self._load_order_relations(db, item) for item in items]
|
|
||||||
|
|
||||||
return items_with_relations, total
|
|
||||||
|
|
||||||
async def create_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj_in: AssetRecoveryOrderCreate,
|
|
||||||
apply_user_id: int
|
|
||||||
):
|
|
||||||
"""创建回收单"""
|
|
||||||
# 验证资产存在性和状态
|
|
||||||
assets = []
|
|
||||||
for asset_id in obj_in.asset_ids:
|
|
||||||
asset_obj = asset.get(db, asset_id)
|
|
||||||
if not asset_obj:
|
|
||||||
raise NotFoundException(f"资产ID {asset_id}")
|
|
||||||
assets.append(asset_obj)
|
|
||||||
|
|
||||||
# 验证资产状态是否允许回收
|
|
||||||
for asset_obj in assets:
|
|
||||||
if not self._can_recover(asset_obj.status, obj_in.recovery_type):
|
|
||||||
raise BusinessException(
|
|
||||||
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许{self._get_recovery_type_name(obj_in.recovery_type)}操作"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成回收单号
|
|
||||||
order_code = await self._generate_order_code(db)
|
|
||||||
|
|
||||||
# 创建回收单
|
|
||||||
db_obj = recovery_order.create(
|
|
||||||
db=db,
|
|
||||||
obj_in=obj_in,
|
|
||||||
order_code=order_code,
|
|
||||||
apply_user_id=apply_user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
def update_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
obj_in: AssetRecoveryOrderUpdate
|
|
||||||
):
|
|
||||||
"""更新回收单"""
|
|
||||||
db_obj = recovery_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 只有待审批状态可以更新
|
|
||||||
if db_obj.approval_status != "pending":
|
|
||||||
raise BusinessException("只有待审批状态的回收单可以更新")
|
|
||||||
|
|
||||||
return recovery_order.update(db, db_obj, obj_in)
|
|
||||||
|
|
||||||
def approve_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
approval_status: str,
|
|
||||||
approval_user_id: int,
|
|
||||||
approval_remark: Optional[str] = None
|
|
||||||
):
|
|
||||||
"""审批回收单"""
|
|
||||||
db_obj = recovery_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.approval_status != "pending":
|
|
||||||
raise BusinessException("该回收单已审批,无法重复审批")
|
|
||||||
|
|
||||||
# 审批
|
|
||||||
db_obj = recovery_order.approve(
|
|
||||||
db=db,
|
|
||||||
db_obj=db_obj,
|
|
||||||
approval_status=approval_status,
|
|
||||||
approval_user_id=approval_user_id,
|
|
||||||
approval_remark=approval_remark
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
def start_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
execute_user_id: int
|
|
||||||
):
|
|
||||||
"""开始回收"""
|
|
||||||
db_obj = recovery_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.approval_status != "approved":
|
|
||||||
raise BusinessException("该回收单未审批通过,无法开始执行")
|
|
||||||
if db_obj.execute_status != "pending":
|
|
||||||
raise BusinessException("该回收单已开始或已完成")
|
|
||||||
|
|
||||||
# 开始回收
|
|
||||||
db_obj = recovery_order.start(db, db_obj, execute_user_id)
|
|
||||||
|
|
||||||
# 更新明细状态为回收中
|
|
||||||
recovery_item.batch_update_recovery_status(db, order_id, "recovering")
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
async def complete_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int,
|
|
||||||
execute_user_id: int
|
|
||||||
):
|
|
||||||
"""完成回收"""
|
|
||||||
db_obj = recovery_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.execute_status not in ["pending", "executing"]:
|
|
||||||
raise BusinessException("该回收单状态不允许完成操作")
|
|
||||||
|
|
||||||
# 完成回收单
|
|
||||||
db_obj = recovery_order.complete(db, db_obj, execute_user_id)
|
|
||||||
|
|
||||||
# 更新资产状态
|
|
||||||
await self._execute_recovery_logic(db, db_obj)
|
|
||||||
|
|
||||||
# 更新明细状态为完成
|
|
||||||
recovery_item.batch_update_recovery_status(db, order_id, "completed")
|
|
||||||
|
|
||||||
return self._load_order_relations(db, db_obj)
|
|
||||||
|
|
||||||
def cancel_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> bool:
|
|
||||||
"""取消回收单"""
|
|
||||||
db_obj = recovery_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 检查状态
|
|
||||||
if db_obj.execute_status == "completed":
|
|
||||||
raise BusinessException("已完成的回收单无法取消")
|
|
||||||
|
|
||||||
recovery_order.cancel(db, db_obj)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def delete_order(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> bool:
|
|
||||||
"""删除回收单"""
|
|
||||||
db_obj = recovery_order.get(db, order_id)
|
|
||||||
if not db_obj:
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
# 只有已取消或已拒绝的可以删除
|
|
||||||
if db_obj.approval_status not in ["rejected", "cancelled"]:
|
|
||||||
raise BusinessException("只能删除已拒绝或已取消的回收单")
|
|
||||||
|
|
||||||
return recovery_order.delete(db, order_id)
|
|
||||||
|
|
||||||
def get_order_items(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_id: int
|
|
||||||
) -> List:
|
|
||||||
"""获取回收单明细"""
|
|
||||||
# 验证回收单存在
|
|
||||||
if not recovery_order.get(db, order_id):
|
|
||||||
raise NotFoundException("回收单")
|
|
||||||
|
|
||||||
return recovery_item.get_by_order(db, order_id)
|
|
||||||
|
|
||||||
def get_statistics(
|
|
||||||
self,
|
|
||||||
db: Session
|
|
||||||
) -> Dict[str, int]:
|
|
||||||
"""获取回收单统计信息"""
|
|
||||||
return recovery_order.get_statistics(db)
|
|
||||||
|
|
||||||
async def _execute_recovery_logic(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
order_obj
|
|
||||||
):
|
|
||||||
"""执行回收逻辑(完成回收时自动执行)"""
|
|
||||||
# 获取明细
|
|
||||||
items = recovery_item.get_by_order(db, order_obj.id)
|
|
||||||
|
|
||||||
# 更新资产状态
|
|
||||||
from app.services.asset_service import asset_service
|
|
||||||
from app.schemas.asset import AssetStatusTransition
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
try:
|
|
||||||
# 根据回收类型确定目标状态
|
|
||||||
if order_obj.recovery_type == "scrap":
|
|
||||||
target_status = "scrapped"
|
|
||||||
remark = f"报废回收: {order_obj.order_code}"
|
|
||||||
else:
|
|
||||||
target_status = "in_stock"
|
|
||||||
remark = f"资产回收: {order_obj.order_code}"
|
|
||||||
|
|
||||||
# 变更资产状态
|
|
||||||
await asset_service.change_asset_status(
|
|
||||||
db=db,
|
|
||||||
asset_id=item.asset_id,
|
|
||||||
status_transition=AssetStatusTransition(
|
|
||||||
new_status=target_status,
|
|
||||||
remark=remark
|
|
||||||
),
|
|
||||||
operator_id=order_obj.execute_user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 记录失败日志
|
|
||||||
print(f"回收资产 {item.asset_code} 失败: {str(e)}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _load_order_relations(
|
|
||||||
self,
|
|
||||||
db: Session,
|
|
||||||
obj
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""加载回收单关联信息"""
|
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"id": obj.id,
|
|
||||||
"order_code": obj.order_code,
|
|
||||||
"recovery_type": obj.recovery_type,
|
|
||||||
"title": obj.title,
|
|
||||||
"asset_count": obj.asset_count,
|
|
||||||
"apply_user_id": obj.apply_user_id,
|
|
||||||
"apply_time": obj.apply_time,
|
|
||||||
"approval_status": obj.approval_status,
|
|
||||||
"approval_user_id": obj.approval_user_id,
|
|
||||||
"approval_time": obj.approval_time,
|
|
||||||
"approval_remark": obj.approval_remark,
|
|
||||||
"execute_status": obj.execute_status,
|
|
||||||
"execute_user_id": obj.execute_user_id,
|
|
||||||
"execute_time": obj.execute_time,
|
|
||||||
"remark": obj.remark,
|
|
||||||
"created_at": obj.created_at,
|
|
||||||
"updated_at": obj.updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载申请人
|
|
||||||
if obj.apply_user_id:
|
|
||||||
apply_user = db.query(User).filter(User.id == obj.apply_user_id).first()
|
|
||||||
if apply_user:
|
|
||||||
result["apply_user"] = {
|
|
||||||
"id": apply_user.id,
|
|
||||||
"real_name": apply_user.real_name,
|
|
||||||
"username": apply_user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载审批人
|
|
||||||
if obj.approval_user_id:
|
|
||||||
approval_user = db.query(User).filter(User.id == obj.approval_user_id).first()
|
|
||||||
if approval_user:
|
|
||||||
result["approval_user"] = {
|
|
||||||
"id": approval_user.id,
|
|
||||||
"real_name": approval_user.real_name,
|
|
||||||
"username": approval_user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载执行人
|
|
||||||
if obj.execute_user_id:
|
|
||||||
execute_user = db.query(User).filter(User.id == obj.execute_user_id).first()
|
|
||||||
if execute_user:
|
|
||||||
result["execute_user"] = {
|
|
||||||
"id": execute_user.id,
|
|
||||||
"real_name": execute_user.real_name,
|
|
||||||
"username": execute_user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
# 加载明细
|
|
||||||
items = recovery_item.get_by_order(db, obj.id)
|
|
||||||
result["items"] = [
|
|
||||||
{
|
|
||||||
"id": item.id,
|
|
||||||
"asset_id": item.asset_id,
|
|
||||||
"asset_code": item.asset_code,
|
|
||||||
"recovery_status": item.recovery_status
|
|
||||||
}
|
|
||||||
for item in items
|
|
||||||
]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _can_recover(self, asset_status: str, recovery_type: str) -> bool:
|
|
||||||
"""判断资产是否可以回收"""
|
|
||||||
# 使用中的资产可以回收
|
|
||||||
if recovery_type in ["user", "org"]:
|
|
||||||
return asset_status == "in_use"
|
|
||||||
# 报废回收可以使用中或维修中的资产
|
|
||||||
elif recovery_type == "scrap":
|
|
||||||
return asset_status in ["in_use", "maintenance", "in_stock"]
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_recovery_type_name(self, recovery_type: str) -> str:
|
|
||||||
"""获取回收类型中文名"""
|
|
||||||
type_names = {
|
|
||||||
"user": "使用人回收",
|
|
||||||
"org": "机构回收",
|
|
||||||
"scrap": "报废回收"
|
|
||||||
}
|
|
||||||
return type_names.get(recovery_type, "回收")
|
|
||||||
|
|
||||||
async def _generate_order_code(self, db: Session) -> str:
|
|
||||||
"""生成回收单号"""
|
|
||||||
from datetime import datetime
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
# 日期部分
|
|
||||||
date_str = datetime.now().strftime("%Y%m%d")
|
|
||||||
|
|
||||||
# 序号部分(5位随机数)
|
|
||||||
sequence = "".join(random.choices(string.digits, k=5))
|
|
||||||
|
|
||||||
# 组合单号: RO-20250124-00001
|
|
||||||
order_code = f"RO-{date_str}-{sequence}"
|
|
||||||
|
|
||||||
# 检查是否重复,如果重复则重新生成
|
|
||||||
while recovery_order.get_by_code(db, order_code):
|
|
||||||
sequence = "".join(random.choices(string.digits, k=5))
|
|
||||||
order_code = f"RO-{date_str}-{sequence}"
|
|
||||||
|
|
||||||
return order_code
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
recovery_service = RecoveryService()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
"""
|
|
||||||
资产状态机服务
|
|
||||||
定义资产状态的转换规则和验证
|
|
||||||
"""
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class AssetStatus(str, Enum):
|
|
||||||
"""资产状态枚举"""
|
|
||||||
PENDING = "pending" # 待入库
|
|
||||||
IN_STOCK = "in_stock" # 库存中
|
|
||||||
IN_USE = "in_use" # 使用中
|
|
||||||
TRANSFERRING = "transferring" # 调拨中
|
|
||||||
MAINTENANCE = "maintenance" # 维修中
|
|
||||||
PENDING_SCRAP = "pending_scrap" # 待报废
|
|
||||||
SCRAPPED = "scrapped" # 已报废
|
|
||||||
LOST = "lost" # 已丢失
|
|
||||||
|
|
||||||
|
|
||||||
class StateMachineService:
|
|
||||||
"""状态机服务类"""
|
|
||||||
|
|
||||||
# 状态转换规则
|
|
||||||
TRANSITIONS: Dict[str, List[str]] = {
|
|
||||||
AssetStatus.PENDING: [
|
|
||||||
AssetStatus.IN_STOCK,
|
|
||||||
AssetStatus.PENDING_SCRAP,
|
|
||||||
],
|
|
||||||
AssetStatus.IN_STOCK: [
|
|
||||||
AssetStatus.IN_USE,
|
|
||||||
AssetStatus.TRANSFERRING,
|
|
||||||
AssetStatus.MAINTENANCE,
|
|
||||||
AssetStatus.PENDING_SCRAP,
|
|
||||||
AssetStatus.LOST,
|
|
||||||
],
|
|
||||||
AssetStatus.IN_USE: [
|
|
||||||
AssetStatus.IN_STOCK,
|
|
||||||
AssetStatus.TRANSFERRING,
|
|
||||||
AssetStatus.MAINTENANCE,
|
|
||||||
AssetStatus.PENDING_SCRAP,
|
|
||||||
AssetStatus.LOST,
|
|
||||||
],
|
|
||||||
AssetStatus.TRANSFERRING: [
|
|
||||||
AssetStatus.IN_STOCK,
|
|
||||||
AssetStatus.IN_USE,
|
|
||||||
],
|
|
||||||
AssetStatus.MAINTENANCE: [
|
|
||||||
AssetStatus.IN_STOCK,
|
|
||||||
AssetStatus.IN_USE,
|
|
||||||
AssetStatus.PENDING_SCRAP,
|
|
||||||
],
|
|
||||||
AssetStatus.PENDING_SCRAP: [
|
|
||||||
AssetStatus.SCRAPPED,
|
|
||||||
AssetStatus.IN_STOCK, # 取消报废
|
|
||||||
],
|
|
||||||
AssetStatus.SCRAPPED: [], # 终态,不可转换
|
|
||||||
AssetStatus.LOST: [], # 终态,不可转换
|
|
||||||
}
|
|
||||||
|
|
||||||
# 状态显示名称
|
|
||||||
STATUS_NAMES: Dict[str, str] = {
|
|
||||||
AssetStatus.PENDING: "待入库",
|
|
||||||
AssetStatus.IN_STOCK: "库存中",
|
|
||||||
AssetStatus.IN_USE: "使用中",
|
|
||||||
AssetStatus.TRANSFERRING: "调拨中",
|
|
||||||
AssetStatus.MAINTENANCE: "维修中",
|
|
||||||
AssetStatus.PENDING_SCRAP: "待报废",
|
|
||||||
AssetStatus.SCRAPPED: "已报废",
|
|
||||||
AssetStatus.LOST: "已丢失",
|
|
||||||
}
|
|
||||||
|
|
||||||
def can_transition(self, current_status: str, target_status: str) -> bool:
|
|
||||||
"""
|
|
||||||
检查状态是否可以转换
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_status: 当前状态
|
|
||||||
target_status: 目标状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否可以转换
|
|
||||||
"""
|
|
||||||
allowed_transitions = self.TRANSITIONS.get(current_status, [])
|
|
||||||
return target_status in allowed_transitions
|
|
||||||
|
|
||||||
def validate_transition(
|
|
||||||
self,
|
|
||||||
current_status: str,
|
|
||||||
target_status: str
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
验证状态转换并返回错误信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_status: 当前状态
|
|
||||||
target_status: 目标状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
错误信息,如果转换有效则返回None
|
|
||||||
"""
|
|
||||||
if current_status == target_status:
|
|
||||||
return "当前状态与目标状态相同"
|
|
||||||
|
|
||||||
if current_status not in self.TRANSITIONS:
|
|
||||||
return f"无效的当前状态: {current_status}"
|
|
||||||
|
|
||||||
if target_status not in self.TRANSITIONS:
|
|
||||||
return f"无效的目标状态: {target_status}"
|
|
||||||
|
|
||||||
if not self.can_transition(current_status, target_status):
|
|
||||||
return f"无法从状态 '{self.get_status_name(current_status)}' 转换到 '{self.get_status_name(target_status)}'"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_status_name(self, status: str) -> str:
|
|
||||||
"""
|
|
||||||
获取状态的显示名称
|
|
||||||
|
|
||||||
Args:
|
|
||||||
status: 状态值
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
状态显示名称
|
|
||||||
"""
|
|
||||||
return self.STATUS_NAMES.get(status, status)
|
|
||||||
|
|
||||||
def get_allowed_transitions(self, current_status: str) -> List[str]:
|
|
||||||
"""
|
|
||||||
获取允许的转换状态列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_status: 当前状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
允许转换到的状态列表
|
|
||||||
"""
|
|
||||||
return self.TRANSITIONS.get(current_status, [])
|
|
||||||
|
|
||||||
def is_terminal_state(self, status: str) -> bool:
|
|
||||||
"""
|
|
||||||
判断是否为终态
|
|
||||||
|
|
||||||
Args:
|
|
||||||
status: 状态值
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否为终态
|
|
||||||
"""
|
|
||||||
return len(self.TRANSITIONS.get(status, [])) == 0
|
|
||||||
|
|
||||||
def get_available_statuses(self) -> List[Dict[str, str]]:
|
|
||||||
"""
|
|
||||||
获取所有可用状态列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
状态列表,每个状态包含value和name
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
{"value": status, "name": name}
|
|
||||||
for status, name in self.STATUS_NAMES.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
|
||||||
state_machine_service = StateMachineService()
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user