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/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
|
||||
Reference in New Issue
Block a user