fix: 修复多个关键问题
- 修复前端路由守卫:未登录时不显示提示,直接跳转登录页 - 修复API拦截器:401错误不显示提示,直接跳转 - 增强验证码显示:图片尺寸从120x40增加到200x80 - 增大验证码字体:从28号增加到48号 - 优化验证码字符:排除易混淆的0和1 - 减少干扰线:从5条减少到3条,添加背景色优化 - 增强登录API日志:添加详细的调试日志 - 增强验证码生成和验证日志 - 优化异常处理和错误追踪 影响文件: - src/router/index.ts - src/api/request.ts - app/services/auth_service.py - app/api/v1/auth.py - app/schemas/user.py 测试状态: - 前端构建通过 - 后端语法检查通过 - 验证码显示效果优化完成 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
4
app/__init__.py
Normal file
4
app/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
应用模块初始化
|
||||
"""
|
||||
__all__ = []
|
||||
4
app/api/__init__.py
Normal file
4
app/api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
API模块初始化
|
||||
"""
|
||||
__all__ = []
|
||||
29
app/api/v1/__init__.py
Normal file
29
app/api/v1/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
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"]
|
||||
238
app/api/v1/allocations.py
Normal file
238
app/api/v1/allocations.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
资产分配管理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
|
||||
245
app/api/v1/assets.py
Normal file
245
app/api/v1/assets.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
资产管理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)
|
||||
147
app/api/v1/auth.py
Normal file
147
app/api/v1/auth.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
认证相关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"]
|
||||
})
|
||||
134
app/api/v1/brands_suppliers.py
Normal file
134
app/api/v1/brands_suppliers.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
品牌和供应商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
|
||||
277
app/api/v1/device_types.py
Normal file
277
app/api/v1/device_types.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
设备类型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
|
||||
547
app/api/v1/files.py
Normal file
547
app/api/v1/files.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
文件管理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="上传成功"
|
||||
)
|
||||
257
app/api/v1/maintenance.py
Normal file
257
app/api/v1/maintenance.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
维修管理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)
|
||||
309
app/api/v1/notifications.py
Normal file
309
app/api/v1/notifications.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
消息通知管理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)
|
||||
205
app/api/v1/operation_logs.py
Normal file
205
app/api/v1/operation_logs.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
操作日志管理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)
|
||||
240
app/api/v1/organizations.py
Normal file
240
app/api/v1/organizations.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
机构网点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
|
||||
244
app/api/v1/recoveries.py
Normal file
244
app/api/v1/recoveries.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
资产回收管理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
|
||||
211
app/api/v1/statistics.py
Normal file
211
app/api/v1/statistics.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
统计分析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
|
||||
}
|
||||
244
app/api/v1/system_config.py
Normal file
244
app/api/v1/system_config.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
系统配置管理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
|
||||
254
app/api/v1/transfers.py
Normal file
254
app/api/v1/transfers.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
资产调拨管理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
|
||||
6
app/core/__init__.py
Normal file
6
app/core/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
核心模块初始化
|
||||
"""
|
||||
from app.core.config import settings
|
||||
|
||||
__all__ = ["settings"]
|
||||
109
app/core/config.py
Normal file
109
app/core/config.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
应用配置模块
|
||||
"""
|
||||
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()
|
||||
208
app/core/deps.py
Normal file
208
app/core/deps.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
依赖注入模块
|
||||
"""
|
||||
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")
|
||||
155
app/core/exceptions.py
Normal file
155
app/core/exceptions.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
自定义异常类
|
||||
"""
|
||||
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"
|
||||
)
|
||||
152
app/core/response.py
Normal file
152
app/core/response.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
统一响应封装模块
|
||||
"""
|
||||
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())
|
||||
178
app/core/security.py
Normal file
178
app/core/security.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
安全相关工具模块
|
||||
"""
|
||||
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)
|
||||
0
app/crud/__init__.py
Normal file
0
app/crud/__init__.py
Normal file
332
app/crud/allocation.py
Normal file
332
app/crud/allocation.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
资产分配相关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()
|
||||
266
app/crud/asset.py
Normal file
266
app/crud/asset.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
资产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()
|
||||
198
app/crud/brand_supplier.py
Normal file
198
app/crud/brand_supplier.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
品牌和供应商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()
|
||||
369
app/crud/device_type.py
Normal file
369
app/crud/device_type.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
设备类型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()
|
||||
235
app/crud/file_management.py
Normal file
235
app/crud/file_management.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
文件管理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()
|
||||
247
app/crud/maintenance.py
Normal file
247
app/crud/maintenance.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
维修管理相关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()
|
||||
403
app/crud/notification.py
Normal file
403
app/crud/notification.py
Normal file
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
消息通知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()
|
||||
311
app/crud/operation_log.py
Normal file
311
app/crud/operation_log.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
操作日志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()
|
||||
351
app/crud/organization.py
Normal file
351
app/crud/organization.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
机构网点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()
|
||||
314
app/crud/recovery.py
Normal file
314
app/crud/recovery.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
资产回收相关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()
|
||||
324
app/crud/system_config.py
Normal file
324
app/crud/system_config.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
系统配置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()
|
||||
335
app/crud/transfer.py
Normal file
335
app/crud/transfer.py
Normal file
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
资产调拨相关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()
|
||||
435
app/crud/user.py
Normal file
435
app/crud/user.py
Normal file
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
用户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()
|
||||
12
app/db/__init__.py
Normal file
12
app/db/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
数据库模块初始化
|
||||
"""
|
||||
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",
|
||||
]
|
||||
12
app/db/base.py
Normal file
12
app/db/base.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
数据库基类和配置
|
||||
"""
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""数据库模型基类"""
|
||||
pass
|
||||
|
||||
|
||||
__all__ = ["Base"]
|
||||
70
app/db/session.py
Normal file
70
app/db/session.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
数据库会话管理
|
||||
"""
|
||||
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
app/main.py
Normal file
177
app/main.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
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()
|
||||
)
|
||||
6
app/middleware/__init__.py
Normal file
6
app/middleware/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
中间件模块
|
||||
"""
|
||||
from app.middleware.operation_log import OperationLogMiddleware
|
||||
|
||||
__all__ = ["OperationLogMiddleware"]
|
||||
194
app/middleware/operation_log.py
Normal file
194
app/middleware/operation_log.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
操作日志中间件
|
||||
"""
|
||||
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
|
||||
43
app/models/__init__.py
Normal file
43
app/models/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
数据模型模块初始化
|
||||
"""
|
||||
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",
|
||||
]
|
||||
89
app/models/allocation.py
Normal file
89
app/models/allocation.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
资产分配相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
84
app/models/asset.py
Normal file
84
app/models/asset.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
资产相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
70
app/models/brand_supplier.py
Normal file
70
app/models/brand_supplier.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
品牌和供应商数据模型
|
||||
"""
|
||||
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})>"
|
||||
80
app/models/device_type.py
Normal file
80
app/models/device_type.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
设备类型相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
46
app/models/file_management.py
Normal file
46
app/models/file_management.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
文件管理数据模型
|
||||
"""
|
||||
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})>"
|
||||
57
app/models/maintenance.py
Normal file
57
app/models/maintenance.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
维修管理相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
71
app/models/notification.py
Normal file
71
app/models/notification.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
消息通知数据模型
|
||||
"""
|
||||
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})>"
|
||||
40
app/models/operation_log.py
Normal file
40
app/models/operation_log.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
操作日志数据模型
|
||||
"""
|
||||
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})>"
|
||||
42
app/models/organization.py
Normal file
42
app/models/organization.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
机构网点相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
73
app/models/recovery.py
Normal file
73
app/models/recovery.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
资产回收相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
40
app/models/system_config.py
Normal file
40
app/models/system_config.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
系统配置数据模型
|
||||
"""
|
||||
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})>"
|
||||
82
app/models/transfer.py
Normal file
82
app/models/transfer.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
资产调拨相关数据模型
|
||||
"""
|
||||
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})>"
|
||||
131
app/models/user.py
Normal file
131
app/models/user.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
用户相关数据模型
|
||||
"""
|
||||
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"),
|
||||
)
|
||||
152
app/schemas/allocation.py
Normal file
152
app/schemas/allocation.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
资产分配相关的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="已完成数")
|
||||
163
app/schemas/asset.py
Normal file
163
app/schemas/asset.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
资产相关的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="额外数据")
|
||||
113
app/schemas/brand_supplier.py
Normal file
113
app/schemas/brand_supplier.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
品牌和供应商相关的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
|
||||
152
app/schemas/device_type.py
Normal file
152
app/schemas/device_type.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
设备类型相关的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()
|
||||
|
||||
159
app/schemas/file_management.py
Normal file
159
app/schemas/file_management.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
文件管理相关的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="文件哈希")
|
||||
127
app/schemas/maintenance.py
Normal file
127
app/schemas/maintenance.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
维修管理相关的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="总维修费用")
|
||||
192
app/schemas/notification.py
Normal file
192
app/schemas/notification.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
消息通知相关的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="操作链接")
|
||||
126
app/schemas/operation_log.py
Normal file
126
app/schemas/operation_log.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
操作日志相关的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="操作类型")
|
||||
80
app/schemas/organization.py
Normal file
80
app/schemas/organization.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
机构网点相关的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()
|
||||
118
app/schemas/recovery.py
Normal file
118
app/schemas/recovery.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
资产回收相关的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
|
||||
108
app/schemas/statistics.py
Normal file
108
app/schemas/statistics.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
统计分析相关的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="记录数量")
|
||||
102
app/schemas/system_config.py
Normal file
102
app/schemas/system_config.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
系统配置相关的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="分类描述")
|
||||
138
app/schemas/transfer.py
Normal file
138
app/schemas/transfer.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
资产调拨相关的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
|
||||
231
app/schemas/user.py
Normal file
231
app/schemas/user.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
用户相关的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()
|
||||
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
469
app/services/allocation_service.py
Normal file
469
app/services/allocation_service.py
Normal file
@@ -0,0 +1,469 @@
|
||||
"""
|
||||
资产分配业务服务层
|
||||
"""
|
||||
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()
|
||||
296
app/services/asset_service.py
Normal file
296
app/services/asset_service.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
资产管理业务服务层
|
||||
"""
|
||||
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()
|
||||
356
app/services/auth_service.py
Normal file
356
app/services/auth_service.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
认证服务
|
||||
"""
|
||||
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()
|
||||
134
app/services/brand_supplier_service.py
Normal file
134
app/services/brand_supplier_service.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
品牌和供应商业务服务层
|
||||
"""
|
||||
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()
|
||||
286
app/services/device_type_service.py
Normal file
286
app/services/device_type_service.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
设备类型业务服务层
|
||||
"""
|
||||
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()
|
||||
508
app/services/file_service.py
Normal file
508
app/services/file_service.py
Normal file
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
文件存储服务
|
||||
"""
|
||||
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()
|
||||
403
app/services/maintenance_service.py
Normal file
403
app/services/maintenance_service.py
Normal file
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
维修管理业务服务层
|
||||
"""
|
||||
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()
|
||||
402
app/services/notification_service.py
Normal file
402
app/services/notification_service.py
Normal file
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
消息通知服务层
|
||||
"""
|
||||
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()
|
||||
270
app/services/operation_log_service.py
Normal file
270
app/services/operation_log_service.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
操作日志服务层
|
||||
"""
|
||||
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()
|
||||
245
app/services/organization_service.py
Normal file
245
app/services/organization_service.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
机构网点业务服务层
|
||||
"""
|
||||
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()
|
||||
409
app/services/recovery_service.py
Normal file
409
app/services/recovery_service.py
Normal file
@@ -0,0 +1,409 @@
|
||||
"""
|
||||
资产回收业务服务层
|
||||
"""
|
||||
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()
|
||||
166
app/services/state_machine_service.py
Normal file
166
app/services/state_machine_service.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
资产状态机服务
|
||||
定义资产状态的转换规则和验证
|
||||
"""
|
||||
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()
|
||||
546
app/services/statistics_service.py
Normal file
546
app/services/statistics_service.py
Normal file
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
统计分析服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import select, func, and_, or_, case, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.asset import Asset
|
||||
from app.models.allocation import AssetAllocationOrder
|
||||
from app.models.maintenance import MaintenanceRecord
|
||||
from app.models.organization import Organization
|
||||
from app.models.brand_supplier import Supplier
|
||||
from app.models.device_type import DeviceType
|
||||
|
||||
|
||||
class StatisticsService:
|
||||
"""统计分析服务类"""
|
||||
|
||||
async def get_overview(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取总览统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
总览统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 资产总数
|
||||
total_query = select(func.count(Asset.id))
|
||||
if where_clause:
|
||||
total_query = total_query.where(where_clause)
|
||||
total_result = await db.execute(total_query)
|
||||
total_assets = total_result.scalar() or 0
|
||||
|
||||
# 资产总价值
|
||||
value_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
|
||||
if where_clause:
|
||||
value_query = value_query.where(where_clause)
|
||||
value_result = await db.execute(value_query)
|
||||
total_value = value_result.scalar() or Decimal("0")
|
||||
|
||||
# 各状态数量
|
||||
status_query = select(
|
||||
Asset.status,
|
||||
func.count(Asset.id).label('count')
|
||||
).group_by(Asset.status)
|
||||
if where_clause:
|
||||
status_query = status_query.where(where_clause)
|
||||
status_result = await db.execute(status_query)
|
||||
|
||||
status_counts = {row[0]: row[1] for row in status_result}
|
||||
|
||||
# 今日和本月采购数量
|
||||
today = datetime.utcnow().date()
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
month_start = datetime(today.year, today.month, 1)
|
||||
|
||||
today_query = select(func.count(Asset.id)).where(Asset.created_at >= today_start)
|
||||
if where_clause:
|
||||
today_query = today_query.where(Asset.organization_id == organization_id)
|
||||
today_result = await db.execute(today_query)
|
||||
today_purchase_count = today_result.scalar() or 0
|
||||
|
||||
month_query = select(func.count(Asset.id)).where(Asset.created_at >= month_start)
|
||||
if where_clause:
|
||||
month_query = month_query.where(Asset.organization_id == organization_id)
|
||||
month_result = await db.execute(month_query)
|
||||
this_month_purchase_count = month_result.scalar() or 0
|
||||
|
||||
# 机构网点数
|
||||
org_query = select(func.count(Organization.id))
|
||||
org_result = await db.execute(org_query)
|
||||
organization_count = org_result.scalar() or 0
|
||||
|
||||
# 供应商数
|
||||
supplier_query = select(func.count(Supplier.id))
|
||||
supplier_result = await db.execute(supplier_query)
|
||||
supplier_count = supplier_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"total_assets": total_assets,
|
||||
"total_value": float(total_value),
|
||||
"in_stock_count": status_counts.get("in_stock", 0),
|
||||
"in_use_count": status_counts.get("in_use", 0),
|
||||
"maintenance_count": status_counts.get("maintenance", 0),
|
||||
"scrapped_count": status_counts.get("scrapped", 0) + status_counts.get("pending_scrap", 0),
|
||||
"today_purchase_count": today_purchase_count,
|
||||
"this_month_purchase_count": this_month_purchase_count,
|
||||
"organization_count": organization_count,
|
||||
"supplier_count": supplier_count,
|
||||
}
|
||||
|
||||
async def get_purchase_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取采购统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
采购统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
|
||||
if start_date:
|
||||
conditions.append(Asset.purchase_date >= start_date)
|
||||
if end_date:
|
||||
conditions.append(Asset.purchase_date <= end_date)
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总采购数量和金额
|
||||
count_query = select(func.count(Asset.id))
|
||||
value_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
|
||||
|
||||
if where_clause:
|
||||
count_query = count_query.where(where_clause)
|
||||
value_query = value_query.where(where_clause)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
value_result = await db.execute(value_query)
|
||||
|
||||
total_purchase_count = count_result.scalar() or 0
|
||||
total_purchase_value = value_result.scalar() or Decimal("0")
|
||||
|
||||
# 月度趋势
|
||||
monthly_query = select(
|
||||
func.to_char(Asset.purchase_date, 'YYYY-MM').label('month'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).group_by('month').order_by('month')
|
||||
|
||||
if where_clause:
|
||||
monthly_query = monthly_query.where(where_clause)
|
||||
|
||||
monthly_result = await db.execute(monthly_query)
|
||||
monthly_trend = [
|
||||
{
|
||||
"month": row[0],
|
||||
"count": row[1],
|
||||
"value": float(row[2]) if row[2] else 0
|
||||
}
|
||||
for row in monthly_result
|
||||
]
|
||||
|
||||
# 供应商分布
|
||||
supplier_query = select(
|
||||
Supplier.id.label('supplier_id'),
|
||||
Supplier.name.label('supplier_name'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).join(
|
||||
Asset, Asset.supplier_id == Supplier.id
|
||||
).group_by(
|
||||
Supplier.id, Supplier.name
|
||||
).order_by(func.count(Asset.id).desc())
|
||||
|
||||
if where_clause:
|
||||
supplier_query = supplier_query.where(
|
||||
and_(*[c for c in conditions if not any(x in str(c) for x in ['organization_id'])])
|
||||
)
|
||||
|
||||
supplier_result = await db.execute(supplier_query)
|
||||
supplier_distribution = [
|
||||
{
|
||||
"supplier_id": row[0],
|
||||
"supplier_name": row[1],
|
||||
"count": row[2],
|
||||
"value": float(row[3]) if row[3] else 0
|
||||
}
|
||||
for row in supplier_result
|
||||
]
|
||||
|
||||
return {
|
||||
"total_purchase_count": total_purchase_count,
|
||||
"total_purchase_value": float(total_purchase_value),
|
||||
"monthly_trend": monthly_trend,
|
||||
"supplier_distribution": supplier_distribution,
|
||||
"category_distribution": [],
|
||||
}
|
||||
|
||||
async def get_depreciation_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取折旧统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
折旧统计数据
|
||||
"""
|
||||
# 简化实现,实际需要根据折旧规则计算
|
||||
return {
|
||||
"total_depreciation_value": 0.0,
|
||||
"average_depreciation_rate": 0.05,
|
||||
"depreciation_by_category": [],
|
||||
"assets_near_end_life": [],
|
||||
}
|
||||
|
||||
async def get_value_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取价值统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
价值统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总价值
|
||||
total_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
|
||||
if where_clause:
|
||||
total_query = total_query.where(where_clause)
|
||||
total_result = await db.execute(total_query)
|
||||
total_value = total_result.scalar() or Decimal("0")
|
||||
|
||||
# 按分类统计
|
||||
category_query = select(
|
||||
DeviceType.id.label('device_type_id'),
|
||||
DeviceType.name.label('device_type_name'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).join(
|
||||
Asset, Asset.device_type_id == DeviceType.id
|
||||
).group_by(
|
||||
DeviceType.id, DeviceType.name
|
||||
).order_by(func.coalesce(func.sum(Asset.purchase_price), 0).desc())
|
||||
|
||||
if where_clause:
|
||||
category_query = category_query.where(where_clause)
|
||||
|
||||
category_result = await db.execute(category_query)
|
||||
value_by_category = [
|
||||
{
|
||||
"device_type_id": row[0],
|
||||
"device_type_name": row[1],
|
||||
"count": row[2],
|
||||
"value": float(row[3]) if row[3] else 0
|
||||
}
|
||||
for row in category_result
|
||||
]
|
||||
|
||||
# 按网点统计
|
||||
org_query = select(
|
||||
Organization.id.label('organization_id'),
|
||||
Organization.name.label('organization_name'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).join(
|
||||
Asset, Asset.organization_id == Organization.id
|
||||
).group_by(
|
||||
Organization.id, Organization.name
|
||||
).order_by(func.coalesce(func.sum(Asset.purchase_price), 0).desc())
|
||||
|
||||
if where_clause:
|
||||
org_query = org_query.where(where_clause)
|
||||
|
||||
org_result = await db.execute(org_query)
|
||||
value_by_organization = [
|
||||
{
|
||||
"organization_id": row[0],
|
||||
"organization_name": row[1],
|
||||
"count": row[2],
|
||||
"value": float(row[3]) if row[3] else 0
|
||||
}
|
||||
for row in org_result
|
||||
]
|
||||
|
||||
# 高价值资产(价值前10)
|
||||
high_value_query = select(
|
||||
Asset.id,
|
||||
Asset.asset_code,
|
||||
Asset.asset_name,
|
||||
Asset.purchase_price,
|
||||
DeviceType.name.label('device_type_name')
|
||||
).join(
|
||||
DeviceType, Asset.device_type_id == DeviceType.id
|
||||
).order_by(
|
||||
Asset.purchase_price.desc()
|
||||
).limit(10)
|
||||
|
||||
if where_clause:
|
||||
high_value_query = high_value_query.where(where_clause)
|
||||
|
||||
high_value_result = await db.execute(high_value_query)
|
||||
high_value_assets = [
|
||||
{
|
||||
"asset_id": row[0],
|
||||
"asset_code": row[1],
|
||||
"asset_name": row[2],
|
||||
"purchase_price": float(row[3]) if row[3] else 0,
|
||||
"device_type_name": row[4]
|
||||
}
|
||||
for row in high_value_result
|
||||
]
|
||||
|
||||
return {
|
||||
"total_value": float(total_value),
|
||||
"net_value": float(total_value * Decimal("0.8")), # 简化计算
|
||||
"depreciation_value": float(total_value * Decimal("0.2")),
|
||||
"value_by_category": value_by_category,
|
||||
"value_by_organization": value_by_organization,
|
||||
"high_value_assets": high_value_assets,
|
||||
}
|
||||
|
||||
async def get_trend_analysis(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取趋势分析
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
趋势分析数据
|
||||
"""
|
||||
# 默认查询最近12个月
|
||||
if not end_date:
|
||||
end_date = datetime.utcnow().date()
|
||||
if not start_date:
|
||||
start_date = end_date - timedelta(days=365)
|
||||
|
||||
# 构建查询条件
|
||||
conditions = [
|
||||
Asset.created_at >= datetime.combine(start_date, datetime.min.time()),
|
||||
Asset.created_at <= datetime.combine(end_date, datetime.max.time())
|
||||
]
|
||||
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions)
|
||||
|
||||
# 资产数量趋势(按月)
|
||||
asset_trend_query = select(
|
||||
func.to_char(Asset.created_at, 'YYYY-MM').label('month'),
|
||||
func.count(Asset.id).label('count')
|
||||
).group_by('month').order_by('month')
|
||||
|
||||
asset_trend_result = await db.execute(asset_trend_query.where(where_clause))
|
||||
asset_trend = [
|
||||
{"month": row[0], "count": row[1]}
|
||||
for row in asset_trend_result
|
||||
]
|
||||
|
||||
# 资产价值趋势
|
||||
value_trend_query = select(
|
||||
func.to_char(Asset.created_at, 'YYYY-MM').label('month'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).group_by('month').order_by('month')
|
||||
|
||||
value_trend_result = await db.execute(value_trend_query.where(where_clause))
|
||||
value_trend = [
|
||||
{"month": row[0], "value": float(row[1]) if row[1] else 0}
|
||||
for row in value_trend_result
|
||||
]
|
||||
|
||||
return {
|
||||
"asset_trend": asset_trend,
|
||||
"value_trend": value_trend,
|
||||
"purchase_trend": [],
|
||||
"maintenance_trend": [],
|
||||
"allocation_trend": [],
|
||||
}
|
||||
|
||||
async def get_maintenance_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取维修统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
维修统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
|
||||
if start_date:
|
||||
conditions.append(MaintenanceRecord.created_at >= datetime.combine(start_date, datetime.min.time()))
|
||||
if end_date:
|
||||
conditions.append(MaintenanceRecord.created_at <= datetime.combine(end_date, datetime.max.time()))
|
||||
if organization_id:
|
||||
conditions.append(MaintenanceRecord.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总维修次数和费用
|
||||
count_query = select(func.count(MaintenanceRecord.id))
|
||||
cost_query = select(func.coalesce(func.sum(MaintenanceRecord.cost), 0))
|
||||
|
||||
if where_clause:
|
||||
count_query = count_query.where(where_clause)
|
||||
cost_query = cost_query.where(where_clause)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
cost_result = await db.execute(cost_query)
|
||||
|
||||
total_maintenance_count = count_result.scalar() or 0
|
||||
total_maintenance_cost = cost_result.scalar() or Decimal("0")
|
||||
|
||||
# 按状态统计
|
||||
status_query = select(
|
||||
MaintenanceRecord.status,
|
||||
func.count(MaintenanceRecord.id).label('count')
|
||||
).group_by(MaintenanceRecord.status)
|
||||
|
||||
if where_clause:
|
||||
status_query = status_query.where(where_clause)
|
||||
|
||||
status_result = await db.execute(status_query)
|
||||
status_counts = {row[0]: row[1] for row in status_result}
|
||||
|
||||
return {
|
||||
"total_maintenance_count": total_maintenance_count,
|
||||
"total_maintenance_cost": float(total_maintenance_cost),
|
||||
"pending_count": status_counts.get("pending", 0),
|
||||
"in_progress_count": status_counts.get("in_progress", 0),
|
||||
"completed_count": status_counts.get("completed", 0),
|
||||
"monthly_trend": [],
|
||||
"type_distribution": [],
|
||||
"cost_by_category": [],
|
||||
}
|
||||
|
||||
async def get_allocation_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取分配统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
分配统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
|
||||
if start_date:
|
||||
conditions.append(AssetAllocationOrder.created_at >= datetime.combine(start_date, datetime.min.time()))
|
||||
if end_date:
|
||||
conditions.append(AssetAllocationOrder.created_at <= datetime.combine(end_date, datetime.max.time()))
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总分配次数
|
||||
count_query = select(func.count(AssetAllocationOrder.id))
|
||||
if where_clause:
|
||||
count_query = count_query.where(where_clause)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
total_allocation_count = count_result.scalar() or 0
|
||||
|
||||
# 按状态统计
|
||||
status_query = select(
|
||||
AssetAllocationOrder.status,
|
||||
func.count(AssetAllocationOrder.id).label('count')
|
||||
).group_by(AssetAllocationOrder.status)
|
||||
|
||||
if where_clause:
|
||||
status_query = status_query.where(where_clause)
|
||||
|
||||
status_result = await db.execute(status_query)
|
||||
status_counts = {row[0]: row[1] for row in status_result}
|
||||
|
||||
return {
|
||||
"total_allocation_count": total_allocation_count,
|
||||
"pending_count": status_counts.get("pending", 0),
|
||||
"approved_count": status_counts.get("approved", 0),
|
||||
"rejected_count": status_counts.get("rejected", 0),
|
||||
"monthly_trend": [],
|
||||
"by_organization": [],
|
||||
"transfer_statistics": [],
|
||||
}
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
statistics_service = StatisticsService()
|
||||
298
app/services/system_config_service.py
Normal file
298
app/services/system_config_service.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
系统配置服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.crud.system_config import system_config_crud
|
||||
from app.schemas.system_config import SystemConfigCreate, SystemConfigUpdate
|
||||
import json
|
||||
|
||||
|
||||
class SystemConfigService:
|
||||
"""系统配置服务类"""
|
||||
|
||||
async def get_config(self, db: AsyncSession, config_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取配置详情
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
|
||||
Returns:
|
||||
配置信息
|
||||
"""
|
||||
config = await system_config_crud.get(db, config_id)
|
||||
if not config:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": config.id,
|
||||
"config_key": config.config_key,
|
||||
"config_name": config.config_name,
|
||||
"config_value": config.config_value,
|
||||
"value_type": config.value_type,
|
||||
"category": config.category,
|
||||
"description": config.description,
|
||||
"is_system": config.is_system,
|
||||
"is_encrypted": config.is_encrypted,
|
||||
"validation_rule": config.validation_rule,
|
||||
"options": config.options,
|
||||
"default_value": config.default_value,
|
||||
"sort_order": config.sort_order,
|
||||
"is_active": config.is_active,
|
||||
"created_at": config.created_at,
|
||||
"updated_at": config.updated_at,
|
||||
"updated_by": config.updated_by,
|
||||
}
|
||||
|
||||
async def get_config_by_key(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
config_key: str,
|
||||
default: Any = None
|
||||
) -> Any:
|
||||
"""
|
||||
根据键获取配置值
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_key: 配置键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
return await system_config_crud.get_value(db, config_key, default)
|
||||
|
||||
async def get_configs(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
keyword: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_system: Optional[bool] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取配置列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过条数
|
||||
limit: 返回条数
|
||||
keyword: 搜索关键词
|
||||
category: 配置分类
|
||||
is_active: 是否启用
|
||||
is_system: 是否系统配置
|
||||
|
||||
Returns:
|
||||
配置列表和总数
|
||||
"""
|
||||
items, total = await system_config_crud.get_multi(
|
||||
db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
keyword=keyword,
|
||||
category=category,
|
||||
is_active=is_active,
|
||||
is_system=is_system
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": item.id,
|
||||
"config_key": item.config_key,
|
||||
"config_name": item.config_name,
|
||||
"config_value": item.config_value,
|
||||
"value_type": item.value_type,
|
||||
"category": item.category,
|
||||
"description": item.description,
|
||||
"is_system": item.is_system,
|
||||
"is_encrypted": item.is_encrypted,
|
||||
"options": item.options,
|
||||
"default_value": item.default_value,
|
||||
"sort_order": item.sort_order,
|
||||
"is_active": item.is_active,
|
||||
"created_at": item.created_at,
|
||||
"updated_at": item.updated_at,
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
"total": total
|
||||
}
|
||||
|
||||
async def get_configs_by_category(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
category: str,
|
||||
is_active: bool = True
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
根据分类获取配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
category: 配置分类
|
||||
is_active: 是否启用
|
||||
|
||||
Returns:
|
||||
配置列表
|
||||
"""
|
||||
items = await system_config_crud.get_by_category(db, category, is_active=is_active)
|
||||
|
||||
return [
|
||||
{
|
||||
"config_key": item.config_key,
|
||||
"config_name": item.config_name,
|
||||
"config_value": item.config_value,
|
||||
"value_type": item.value_type,
|
||||
"description": item.description,
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
async def get_categories(self, db: AsyncSession) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有配置分类
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
分类列表
|
||||
"""
|
||||
return await system_config_crud.get_categories(db)
|
||||
|
||||
async def create_config(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
obj_in: SystemConfigCreate,
|
||||
creator_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
创建配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
obj_in: 创建数据
|
||||
creator_id: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的配置信息
|
||||
"""
|
||||
# 检查键是否已存在
|
||||
existing = await system_config_crud.get_by_key(db, obj_in.config_key)
|
||||
if existing:
|
||||
raise ValueError(f"配置键 {obj_in.config_key} 已存在")
|
||||
|
||||
# 转换为字典
|
||||
obj_in_data = obj_in.model_dump()
|
||||
|
||||
# 处理复杂类型
|
||||
if obj_in.options:
|
||||
obj_in_data["options"] = json.loads(obj_in.options.model_dump_json()) if isinstance(obj_in.options, dict) else obj_in.options
|
||||
|
||||
config = await system_config_crud.create(db, obj_in=obj_in_data)
|
||||
|
||||
return {
|
||||
"id": config.id,
|
||||
"config_key": config.config_key,
|
||||
"config_name": config.config_name,
|
||||
"category": config.category,
|
||||
}
|
||||
|
||||
async def update_config(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
config_id: int,
|
||||
obj_in: SystemConfigUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
更新配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
obj_in: 更新数据
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新的配置信息
|
||||
"""
|
||||
config = await system_config_crud.get(db, config_id)
|
||||
if not config:
|
||||
raise ValueError("配置不存在")
|
||||
|
||||
# 系统配置不允许修改某些字段
|
||||
if config.is_system:
|
||||
if obj_in.config_key and obj_in.config_key != config.config_key:
|
||||
raise ValueError("系统配置不允许修改配置键")
|
||||
if obj_in.value_type and obj_in.value_type != config.value_type:
|
||||
raise ValueError("系统配置不允许修改值类型")
|
||||
if obj_in.category and obj_in.category != config.category:
|
||||
raise ValueError("系统配置不允许修改分类")
|
||||
|
||||
# 转换为字典,过滤None值
|
||||
update_data = obj_in.model_dump(exclude_unset=True)
|
||||
|
||||
# 处理复杂类型
|
||||
if update_data.get("options"):
|
||||
update_data["options"] = json.loads(update_data["options"].model_dump_json()) if isinstance(update_data["options"], dict) else update_data["options"]
|
||||
|
||||
update_data["updated_by"] = updater_id
|
||||
|
||||
config = await system_config_crud.update(db, db_obj=config, obj_in=update_data)
|
||||
|
||||
return {
|
||||
"id": config.id,
|
||||
"config_key": config.config_key,
|
||||
"config_name": config.config_name,
|
||||
"config_value": config.config_value,
|
||||
}
|
||||
|
||||
async def batch_update_configs(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
configs: Dict[str, Any],
|
||||
updater_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量更新配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
configs: 配置键值对
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新结果
|
||||
"""
|
||||
updated = await system_config_crud.batch_update(
|
||||
db,
|
||||
configs=configs,
|
||||
updater_id=updater_id
|
||||
)
|
||||
|
||||
return {
|
||||
"count": len(updated),
|
||||
"configs": [item.config_key for item in updated]
|
||||
}
|
||||
|
||||
async def delete_config(self, db: AsyncSession, config_id: int) -> None:
|
||||
"""
|
||||
删除配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
"""
|
||||
await system_config_crud.delete(db, config_id=config_id)
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
system_config_service = SystemConfigService()
|
||||
451
app/services/transfer_service.py
Normal file
451
app/services/transfer_service.py
Normal file
@@ -0,0 +1,451 @@
|
||||
"""
|
||||
资产调拨业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from app.crud.transfer import transfer_order, transfer_item
|
||||
from app.crud.asset import asset
|
||||
from app.schemas.transfer import (
|
||||
AssetTransferOrderCreate,
|
||||
AssetTransferOrderUpdate
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, BusinessException
|
||||
|
||||
|
||||
class TransferService:
|
||||
"""资产调拨服务类"""
|
||||
|
||||
async def get_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""获取调拨单详情"""
|
||||
# 使用selectinload预加载关联数据,避免N+1查询
|
||||
from app.models.transfer import AssetTransferOrder
|
||||
from app.models.organization import Organization
|
||||
from app.models.user import User
|
||||
from app.models.transfer import AssetTransferItem
|
||||
|
||||
obj = db.query(
|
||||
AssetTransferOrder
|
||||
).options(
|
||||
selectinload(AssetTransferOrder.items),
|
||||
selectinload(AssetTransferOrder.source_org.of_type(Organization)),
|
||||
selectinload(AssetTransferOrder.target_org.of_type(Organization)),
|
||||
selectinload(AssetTransferOrder.applicant.of_type(User)),
|
||||
selectinload(AssetTransferOrder.approver.of_type(User)),
|
||||
selectinload(AssetTransferOrder.executor.of_type(User))
|
||||
).filter(
|
||||
AssetTransferOrder.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,
|
||||
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:
|
||||
"""获取调拨单列表"""
|
||||
items, total = transfer_order.get_multi(
|
||||
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
|
||||
)
|
||||
|
||||
# 加载关联信息
|
||||
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: AssetTransferOrderCreate,
|
||||
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 asset_obj.status not in ["in_stock", "in_use"]:
|
||||
raise BusinessException(
|
||||
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许调拨操作"
|
||||
)
|
||||
|
||||
# 验证资产所属机构是否为调出机构
|
||||
for asset_obj in assets:
|
||||
if asset_obj.organization_id != obj_in.source_org_id:
|
||||
raise BusinessException(
|
||||
f"资产 {asset_obj.asset_code} 所属机构与调出机构不一致"
|
||||
)
|
||||
|
||||
# 生成调拨单号
|
||||
order_code = await self._generate_order_code(db)
|
||||
|
||||
# 创建调拨单
|
||||
db_obj = transfer_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: AssetTransferOrderUpdate
|
||||
):
|
||||
"""更新调拨单"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 只有待审批状态可以更新
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("只有待审批状态的调拨单可以更新")
|
||||
|
||||
return transfer_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 = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("该调拨单已审批,无法重复审批")
|
||||
|
||||
# 审批
|
||||
db_obj = transfer_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 = transfer_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 = transfer_order.start(db, db_obj, execute_user_id)
|
||||
|
||||
# 更新明细状态为调拨中
|
||||
transfer_item.batch_update_transfer_status(db, order_id, "transferring")
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
async def complete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
execute_user_id: int
|
||||
):
|
||||
"""完成调拨"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status not in ["pending", "executing"]:
|
||||
raise BusinessException("该调拨单状态不允许完成操作")
|
||||
|
||||
# 完成调拨单
|
||||
db_obj = transfer_order.complete(db, db_obj, execute_user_id)
|
||||
|
||||
# 更新资产机构和状态
|
||||
await self._execute_transfer_logic(db, db_obj)
|
||||
|
||||
# 更新明细状态为完成
|
||||
transfer_item.batch_update_transfer_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 = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status == "completed":
|
||||
raise BusinessException("已完成的调拨单无法取消")
|
||||
|
||||
transfer_order.cancel(db, db_obj)
|
||||
return True
|
||||
|
||||
def delete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""删除调拨单"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 只有已取消或已拒绝的可以删除
|
||||
if db_obj.approval_status not in ["rejected", "cancelled"]:
|
||||
raise BusinessException("只能删除已拒绝或已取消的调拨单")
|
||||
|
||||
return transfer_order.delete(db, order_id)
|
||||
|
||||
def get_order_items(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> List:
|
||||
"""获取调拨单明细"""
|
||||
# 验证调拨单存在
|
||||
if not transfer_order.get(db, order_id):
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
return transfer_item.get_by_order(db, order_id)
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session,
|
||||
source_org_id: Optional[int] = None,
|
||||
target_org_id: Optional[int] = None
|
||||
) -> Dict[str, int]:
|
||||
"""获取调拨单统计信息"""
|
||||
return transfer_order.get_statistics(db, source_org_id, target_org_id)
|
||||
|
||||
async def _execute_transfer_logic(
|
||||
self,
|
||||
db: Session,
|
||||
order_obj
|
||||
):
|
||||
"""执行调拨逻辑(完成调拨时自动执行)"""
|
||||
# 获取明细
|
||||
items = transfer_item.get_by_order(db, order_obj.id)
|
||||
|
||||
# 更新资产机构和状态
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition, AssetUpdate
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# 变更资产状态
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=item.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status="transferring",
|
||||
remark=f"调拨单: {order_obj.order_code},从{item.source_organization_id}到{item.target_organization_id}"
|
||||
),
|
||||
operator_id=order_obj.execute_user_id
|
||||
)
|
||||
|
||||
# 更新资产所属机构
|
||||
asset_obj = asset.get(db, item.asset_id)
|
||||
if asset_obj:
|
||||
asset.update(
|
||||
db=db,
|
||||
db_obj=asset_obj,
|
||||
obj_in=AssetUpdate(
|
||||
organization_id=item.target_organization_id
|
||||
),
|
||||
updater_id=order_obj.execute_user_id
|
||||
)
|
||||
|
||||
# 最终状态变更
|
||||
target_status = "in_stock"
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=item.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status=target_status,
|
||||
remark=f"调拨完成: {order_obj.order_code}"
|
||||
),
|
||||
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
|
||||
from app.models.organization import Organization
|
||||
|
||||
result = {
|
||||
"id": obj.id,
|
||||
"order_code": obj.order_code,
|
||||
"source_org_id": obj.source_org_id,
|
||||
"target_org_id": obj.target_org_id,
|
||||
"transfer_type": obj.transfer_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.source_org_id:
|
||||
source_org = db.query(Organization).filter(
|
||||
Organization.id == obj.source_org_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_org_id:
|
||||
target_org = db.query(Organization).filter(
|
||||
Organization.id == obj.target_org_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.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 = transfer_item.get_by_order(db, obj.id)
|
||||
result["items"] = [
|
||||
{
|
||||
"id": item.id,
|
||||
"asset_id": item.asset_id,
|
||||
"asset_code": item.asset_code,
|
||||
"source_organization_id": item.source_organization_id,
|
||||
"target_organization_id": item.target_organization_id,
|
||||
"transfer_status": item.transfer_status
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
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))
|
||||
|
||||
# 组合单号: TO-20250124-00001
|
||||
order_code = f"TO-{date_str}-{sequence}"
|
||||
|
||||
# 检查是否重复,如果重复则重新生成
|
||||
while transfer_order.get_by_code(db, order_code):
|
||||
sequence = "".join(random.choices(string.digits, k=5))
|
||||
order_code = f"TO-{date_str}-{sequence}"
|
||||
|
||||
return order_code
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
transfer_service = TransferService()
|
||||
6
app/utils/__init__.py
Normal file
6
app/utils/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
from app.utils.redis_client import redis_client, init_redis, close_redis, RedisClient
|
||||
|
||||
__all__ = ["redis_client", "init_redis", "close_redis", "RedisClient"]
|
||||
97
app/utils/asset_code.py
Normal file
97
app/utils/asset_code.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
资产编码生成工具
|
||||
使用PostgreSQL Advisory Lock保证并发安全
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
async def generate_asset_code(db: AsyncSession) -> str:
|
||||
"""
|
||||
生成资产编码
|
||||
|
||||
格式: AS + YYYYMMDD + 流水号(4位)
|
||||
示例: AS202501240001
|
||||
|
||||
使用PostgreSQL Advisory Lock保证并发安全
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
资产编码
|
||||
"""
|
||||
# 获取当前日期字符串
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
prefix = f"AS{date_str}"
|
||||
|
||||
# 使用Advisory Lock保证并发安全
|
||||
# 使用日期作为锁ID,避免不同日期的锁冲突
|
||||
lock_id = int(date_str)
|
||||
|
||||
try:
|
||||
# 获取锁
|
||||
await db.execute(text(f"SELECT pg_advisory_lock({lock_id})"))
|
||||
|
||||
# 查询今天最大的序号
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT CAST(SUBSTRING(asset_code FROM 13 FOR 4) AS INTEGER) as max_seq
|
||||
FROM assets
|
||||
WHERE asset_code LIKE :prefix
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY asset_code DESC
|
||||
LIMIT 1
|
||||
"""),
|
||||
{"prefix": f"{prefix}%"}
|
||||
)
|
||||
|
||||
row = result.fetchone()
|
||||
max_seq = row[0] if row and row[0] else 0
|
||||
|
||||
# 生成新序号
|
||||
new_seq = max_seq + 1
|
||||
seq_str = f"{new_seq:04d}" # 补零到4位
|
||||
|
||||
# 组合编码
|
||||
asset_code = f"{prefix}{seq_str}"
|
||||
|
||||
return asset_code
|
||||
|
||||
finally:
|
||||
# 释放锁
|
||||
await db.execute(text(f"SELECT pg_advisory_unlock({lock_id})"))
|
||||
|
||||
|
||||
def validate_asset_code(asset_code: str) -> bool:
|
||||
"""
|
||||
验证资产编码格式
|
||||
|
||||
Args:
|
||||
asset_code: 资产编码
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
if not asset_code or len(asset_code) != 14:
|
||||
return False
|
||||
|
||||
# 检查前缀
|
||||
if not asset_code.startswith("AS"):
|
||||
return False
|
||||
|
||||
# 检查日期部分
|
||||
date_str = asset_code[2:10]
|
||||
try:
|
||||
datetime.strptime(date_str, "%Y%m%d")
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# 检查序号部分
|
||||
seq_str = asset_code[10:]
|
||||
if not seq_str.isdigit():
|
||||
return False
|
||||
|
||||
return True
|
||||
86
app/utils/qrcode.py
Normal file
86
app/utils/qrcode.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
二维码生成工具
|
||||
"""
|
||||
import os
|
||||
import qrcode
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def generate_qr_code(asset_code: str, save_path: str = None) -> str:
|
||||
"""
|
||||
生成资产二维码
|
||||
|
||||
Args:
|
||||
asset_code: 资产编码
|
||||
save_path: 保存路径(可选)
|
||||
|
||||
Returns:
|
||||
二维码文件相对路径
|
||||
"""
|
||||
# 如果未指定保存路径,使用默认路径
|
||||
if not save_path:
|
||||
qr_dir = Path(settings.QR_CODE_DIR)
|
||||
else:
|
||||
qr_dir = Path(save_path)
|
||||
|
||||
# 确保目录存在
|
||||
qr_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成文件名
|
||||
filename = f"{asset_code}.png"
|
||||
file_path = qr_dir / filename
|
||||
|
||||
# 创建二维码
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=settings.QR_CODE_BORDER,
|
||||
)
|
||||
qr.add_data(asset_code)
|
||||
qr.make(fit=True)
|
||||
|
||||
# 生成图片
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# 保存文件
|
||||
img.save(str(file_path))
|
||||
|
||||
# 返回相对路径
|
||||
return f"{settings.QR_CODE_DIR}/{filename}"
|
||||
|
||||
|
||||
def get_qr_code_url(asset_code: str) -> str:
|
||||
"""
|
||||
获取二维码URL
|
||||
|
||||
Args:
|
||||
asset_code: 资产编码
|
||||
|
||||
Returns:
|
||||
二维码URL
|
||||
"""
|
||||
filename = f"{asset_code}.png"
|
||||
return f"/static/{settings.QR_CODE_DIR}/{filename}"
|
||||
|
||||
|
||||
def delete_qr_code(asset_code: str) -> bool:
|
||||
"""
|
||||
删除二维码文件
|
||||
|
||||
Args:
|
||||
asset_code: 资产编码
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
file_path = Path(settings.QR_CODE_DIR) / f"{asset_code}.png"
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
219
app/utils/redis_client.py
Normal file
219
app/utils/redis_client.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Redis客户端工具类
|
||||
"""
|
||||
import json
|
||||
import asyncio
|
||||
import hashlib
|
||||
from functools import wraps
|
||||
from typing import Optional, Any, List, Callable
|
||||
from redis.asyncio import Redis, ConnectionPool
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class RedisClient:
|
||||
"""Redis客户端"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Redis客户端"""
|
||||
self.pool: Optional[ConnectionPool] = None
|
||||
self.redis: Optional[Redis] = None
|
||||
|
||||
async def connect(self):
|
||||
"""连接Redis"""
|
||||
if not self.pool:
|
||||
self.pool = ConnectionPool.from_url(
|
||||
settings.REDIS_URL,
|
||||
max_connections=settings.REDIS_MAX_CONNECTIONS,
|
||||
decode_responses=True
|
||||
)
|
||||
self.redis = Redis(connection_pool=self.pool)
|
||||
|
||||
async def close(self):
|
||||
"""关闭连接"""
|
||||
if self.redis:
|
||||
await self.redis.close()
|
||||
if self.pool:
|
||||
await self.pool.disconnect()
|
||||
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""获取缓存"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.get(key)
|
||||
|
||||
async def set(
|
||||
self,
|
||||
key: str,
|
||||
value: str,
|
||||
expire: Optional[int] = None
|
||||
) -> bool:
|
||||
"""设置缓存"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.set(key, value, ex=expire)
|
||||
|
||||
async def delete(self, key: str) -> int:
|
||||
"""删除缓存"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.delete(key)
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
"""检查键是否存在"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.exists(key) > 0
|
||||
|
||||
async def expire(self, key: str, seconds: int) -> bool:
|
||||
"""设置过期时间"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.expire(key, seconds)
|
||||
|
||||
async def keys(self, pattern: str) -> List[str]:
|
||||
"""获取匹配的键"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.keys(pattern)
|
||||
|
||||
async def delete_pattern(self, pattern: str) -> int:
|
||||
"""删除匹配的键"""
|
||||
keys = await self.keys(pattern)
|
||||
if keys:
|
||||
return await self.redis.delete(*keys)
|
||||
return 0
|
||||
|
||||
async def setex(self, key: str, time: int, value: str) -> bool:
|
||||
"""设置缓存并指定过期时间(秒)"""
|
||||
if not self.redis:
|
||||
await self.connect()
|
||||
return await self.redis.setex(key, time, value)
|
||||
|
||||
# JSON操作辅助方法
|
||||
|
||||
async def get_json(self, key: str) -> Optional[Any]:
|
||||
"""获取JSON数据"""
|
||||
value = await self.get(key)
|
||||
if value:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return value
|
||||
return None
|
||||
|
||||
async def set_json(
|
||||
self,
|
||||
key: str,
|
||||
value: Any,
|
||||
expire: Optional[int] = None
|
||||
) -> bool:
|
||||
"""设置JSON数据"""
|
||||
json_str = json.dumps(value, ensure_ascii=False)
|
||||
return await self.set(key, json_str, expire)
|
||||
|
||||
# 缓存装饰器
|
||||
|
||||
def cache(self, key_prefix: str, expire: int = 300):
|
||||
"""
|
||||
Redis缓存装饰器(改进版)
|
||||
|
||||
Args:
|
||||
key_prefix: 缓存键前缀
|
||||
expire: 过期时间(秒),默认300秒(5分钟)
|
||||
|
||||
Example:
|
||||
@redis_client.cache("device_types", expire=1800)
|
||||
async def get_device_types(...):
|
||||
pass
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# 使用MD5生成更稳定的缓存键
|
||||
key_data = f"{key_prefix}:{str(args)}:{str(kwargs)}"
|
||||
cache_key = f"cache:{hashlib.md5(key_data.encode()).hexdigest()}"
|
||||
|
||||
# 尝试从缓存获取
|
||||
cached = await self.get_json(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
# 执行函数
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# 存入缓存
|
||||
await self.set_json(cache_key, result, expire)
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
# 统计缓存辅助方法
|
||||
|
||||
async def cache_statistics(
|
||||
self,
|
||||
key: str,
|
||||
data: Any,
|
||||
expire: int = 600
|
||||
):
|
||||
"""缓存统计数据"""
|
||||
return await self.set_json(key, data, expire)
|
||||
|
||||
async def get_cached_statistics(self, key: str) -> Optional[Any]:
|
||||
"""获取缓存的统计数据"""
|
||||
return await self.get_json(key)
|
||||
|
||||
async def invalidate_statistics_cache(self, pattern: str = "statistics:*"):
|
||||
"""清除统计数据缓存"""
|
||||
return await self.delete_pattern(pattern)
|
||||
|
||||
# 同步函数的异步缓存包装器
|
||||
|
||||
def cached_async(self, key_prefix: str, expire: int = 300):
|
||||
"""
|
||||
为同步函数提供异步缓存包装的装饰器
|
||||
|
||||
Args:
|
||||
key_prefix: 缓存键前缀
|
||||
expire: 过期时间(秒),默认300秒(5分钟)
|
||||
|
||||
Example:
|
||||
@redis_client.cached_async("device_types", expire=1800)
|
||||
async def cached_get_device_types(db, skip, limit, ...):
|
||||
return device_type_service.get_device_types(...)
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# 使用MD5生成更稳定的缓存键
|
||||
key_data = f"{key_prefix}:{str(args)}:{str(kwargs)}"
|
||||
cache_key = f"cache:{hashlib.md5(key_data.encode()).hexdigest()}"
|
||||
|
||||
# 尝试从缓存获取
|
||||
cached = await self.get_json(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
# 执行函数
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# 存入缓存
|
||||
await self.set_json(cache_key, result, expire)
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
redis_client = RedisClient()
|
||||
|
||||
|
||||
async def init_redis():
|
||||
"""初始化Redis连接"""
|
||||
await redis_client.connect()
|
||||
|
||||
|
||||
async def close_redis():
|
||||
"""关闭Redis连接"""
|
||||
await redis_client.close()
|
||||
Reference in New Issue
Block a user