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:
Claude
2026-01-25 00:26:21 +08:00
commit e71181f0a3
150 changed files with 39549 additions and 0 deletions

0
app/services/__init__.py Normal file
View File

View File

@@ -0,0 +1,469 @@
"""
资产分配业务服务层
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session, selectinload
from app.crud.allocation import allocation_order, allocation_item
from app.crud.asset import asset
from app.schemas.allocation import (
AllocationOrderCreate,
AllocationOrderUpdate,
AllocationOrderApproval
)
from app.core.exceptions import NotFoundException, BusinessException
class AllocationService:
"""资产分配服务类"""
async def get_order(
self,
db: Session,
order_id: int
) -> Dict[str, Any]:
"""获取分配单详情"""
# 使用selectinload预加载关联数据避免N+1查询
from app.models.allocation import AllocationOrder
from app.models.organization import Organization
from app.models.user import User
from app.models.allocation import AllocationItem
obj = db.query(
AllocationOrder
).options(
selectinload(AllocationOrder.items),
selectinload(AllocationOrder.source_organization.of_type(Organization)),
selectinload(AllocationOrder.target_organization.of_type(Organization)),
selectinload(AllocationOrder.applicant.of_type(User)),
selectinload(AllocationOrder.approver.of_type(User)),
selectinload(AllocationOrder.executor.of_type(User))
).filter(
AllocationOrder.id == order_id
).first()
if not obj:
raise NotFoundException("分配单")
# 加载关联信息
return self._load_order_relations(db, obj)
def get_orders(
self,
db: Session,
skip: int = 0,
limit: int = 20,
order_type: Optional[str] = None,
approval_status: Optional[str] = None,
execute_status: Optional[str] = None,
applicant_id: Optional[int] = None,
target_organization_id: Optional[int] = None,
keyword: Optional[str] = None
) -> tuple:
"""获取分配单列表"""
items, total = allocation_order.get_multi(
db=db,
skip=skip,
limit=limit,
order_type=order_type,
approval_status=approval_status,
execute_status=execute_status,
applicant_id=applicant_id,
target_organization_id=target_organization_id,
keyword=keyword
)
# 加载关联信息
items_with_relations = [self._load_order_relations(db, item) for item in items]
return items_with_relations, total
async def create_order(
self,
db: Session,
obj_in: AllocationOrderCreate,
applicant_id: int
):
"""创建分配单"""
# 验证资产存在性和状态
assets = []
for asset_id in obj_in.asset_ids:
asset_obj = asset.get(db, asset_id)
if not asset_obj:
raise NotFoundException(f"资产ID {asset_id}")
assets.append(asset_obj)
# 验证资产状态是否允许分配
for asset_obj in assets:
if not self._can_allocate(asset_obj.status, obj_in.order_type):
raise BusinessException(
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许{self._get_order_type_name(obj_in.order_type)}操作"
)
# 生成分配单号
order_code = await self._generate_order_code(db, obj_in.order_type)
# 创建分配单
db_obj = allocation_order.create(
db=db,
obj_in=obj_in,
order_code=order_code,
applicant_id=applicant_id
)
return self._load_order_relations(db, db_obj)
def update_order(
self,
db: Session,
order_id: int,
obj_in: AllocationOrderUpdate,
updater_id: int
):
"""更新分配单"""
db_obj = allocation_order.get(db, order_id)
if not db_obj:
raise NotFoundException("分配单")
# 只有草稿或待审批状态可以更新
if db_obj.approval_status not in ["pending", "draft"]:
raise BusinessException("只有待审批状态的分配单可以更新")
return allocation_order.update(db, db_obj, obj_in, updater_id)
async def approve_order(
self,
db: Session,
order_id: int,
approval_in: AllocationOrderApproval,
approver_id: int
):
"""审批分配单"""
db_obj = allocation_order.get(db, order_id)
if not db_obj:
raise NotFoundException("分配单")
# 检查状态
if db_obj.approval_status != "pending":
raise BusinessException("该分配单已审批,无法重复审批")
# 审批
db_obj = allocation_order.approve(
db=db,
db_obj=db_obj,
approval_status=approval_in.approval_status,
approver_id=approver_id,
approval_remark=approval_in.approval_remark
)
# 如果审批通过,执行分配逻辑
if approval_in.approval_status == "approved":
await self._execute_allocation_logic(db, db_obj)
return self._load_order_relations(db, db_obj)
async def execute_order(
self,
db: Session,
order_id: int,
executor_id: int
):
"""执行分配单"""
db_obj = allocation_order.get(db, order_id)
if not db_obj:
raise NotFoundException("分配单")
# 检查状态
if db_obj.approval_status != "approved":
raise BusinessException("该分配单未审批通过,无法执行")
if db_obj.execute_status == "completed":
raise BusinessException("该分配单已执行完成")
# 执行分配单
db_obj = allocation_order.execute(db, db_obj, executor_id)
return self._load_order_relations(db, db_obj)
def cancel_order(
self,
db: Session,
order_id: int
) -> bool:
"""取消分配单"""
db_obj = allocation_order.get(db, order_id)
if not db_obj:
raise NotFoundException("分配单")
# 检查状态
if db_obj.execute_status == "completed":
raise BusinessException("已完成的分配单无法取消")
allocation_order.cancel(db, db_obj)
return True
def delete_order(
self,
db: Session,
order_id: int
) -> bool:
"""删除分配单"""
db_obj = allocation_order.get(db, order_id)
if not db_obj:
raise NotFoundException("分配单")
# 只有草稿或已取消的可以删除
if db_obj.approval_status not in ["draft", "rejected", "cancelled"]:
raise BusinessException("只能删除草稿、已拒绝或已取消的分配单")
return allocation_order.delete(db, order_id)
def get_order_items(
self,
db: Session,
order_id: int
) -> List:
"""获取分配单明细"""
# 验证分配单存在
if not allocation_order.get(db, order_id):
raise NotFoundException("分配单")
return allocation_item.get_by_order(db, order_id)
def get_statistics(
self,
db: Session,
applicant_id: Optional[int] = None
) -> Dict[str, int]:
"""获取分配单统计信息"""
return allocation_order.get_statistics(db, applicant_id)
async def _execute_allocation_logic(
self,
db: Session,
order_obj
):
"""执行分配逻辑(审批通过后自动执行)"""
# 根据单据类型执行不同的逻辑
if order_obj.order_type == "allocation":
await self._execute_allocation(db, order_obj)
elif order_obj.order_type == "transfer":
await self._execute_transfer(db, order_obj)
elif order_obj.order_type == "recovery":
await self._execute_recovery(db, order_obj)
elif order_obj.order_type == "maintenance":
await self._execute_maintenance_allocation(db, order_obj)
elif order_obj.order_type == "scrap":
await self._execute_scrap_allocation(db, order_obj)
async def _execute_allocation(self, db: Session, order_obj):
"""执行资产分配"""
# 更新明细状态为执行中
allocation_item.batch_update_execute_status(db, order_obj.id, "executing")
# 获取明细
items = allocation_item.get_by_order(db, order_obj.id)
# 更新资产状态
from app.services.asset_service import asset_service
from app.schemas.asset import AssetStatusTransition
for item in items:
try:
# 变更资产状态
await asset_service.change_asset_status(
db=db,
asset_id=item.asset_id,
status_transition=AssetStatusTransition(
new_status=item.to_status,
remark=f"分配单: {order_obj.order_code}"
),
operator_id=order_obj.applicant_id
)
# 更新明细状态为完成
allocation_item.update_execute_status(db, item.id, "completed")
except Exception as e:
# 更新明细状态为失败
allocation_item.update_execute_status(
db,
item.id,
"failed",
failure_reason=str(e)
)
async def _execute_transfer(self, db: Session, order_obj):
"""执行资产调拨"""
# 调拨逻辑与分配类似,但需要记录调出和调入网点
await self._execute_allocation(db, order_obj)
async def _execute_recovery(self, db: Session, order_obj):
"""执行资产回收"""
# 回收逻辑
await self._execute_allocation(db, order_obj)
async def _execute_maintenance_allocation(self, db: Session, order_obj):
"""执行维修分配"""
# 维修分配逻辑
await self._execute_allocation(db, order_obj)
async def _execute_scrap_allocation(self, db: Session, order_obj):
"""执行报废分配"""
# 报废分配逻辑
await self._execute_allocation(db, order_obj)
def _load_order_relations(
self,
db: Session,
obj
) -> Dict[str, Any]:
"""加载分配单关联信息"""
from app.models.user import User
from app.models.organization import Organization
result = {
"id": obj.id,
"order_code": obj.order_code,
"order_type": obj.order_type,
"title": obj.title,
"source_organization_id": obj.source_organization_id,
"target_organization_id": obj.target_organization_id,
"applicant_id": obj.applicant_id,
"approver_id": obj.approver_id,
"approval_status": obj.approval_status,
"approval_time": obj.approval_time,
"approval_remark": obj.approval_remark,
"expect_execute_date": obj.expect_execute_date,
"actual_execute_date": obj.actual_execute_date,
"executor_id": obj.executor_id,
"execute_status": obj.execute_status,
"remark": obj.remark,
"created_at": obj.created_at,
"updated_at": obj.updated_at
}
# 加载关联信息
if obj.source_organization_id:
source_org = db.query(Organization).filter(
Organization.id == obj.source_organization_id
).first()
if source_org:
result["source_organization"] = {
"id": source_org.id,
"org_name": source_org.org_name,
"org_type": source_org.org_type
}
if obj.target_organization_id:
target_org = db.query(Organization).filter(
Organization.id == obj.target_organization_id
).first()
if target_org:
result["target_organization"] = {
"id": target_org.id,
"org_name": target_org.org_name,
"org_type": target_org.org_type
}
if obj.applicant_id:
applicant = db.query(User).filter(User.id == obj.applicant_id).first()
if applicant:
result["applicant"] = {
"id": applicant.id,
"real_name": applicant.real_name,
"username": applicant.username
}
if obj.approver_id:
approver = db.query(User).filter(User.id == obj.approver_id).first()
if approver:
result["approver"] = {
"id": approver.id,
"real_name": approver.real_name,
"username": approver.username
}
if obj.executor_id:
executor = db.query(User).filter(User.id == obj.executor_id).first()
if executor:
result["executor"] = {
"id": executor.id,
"real_name": executor.real_name,
"username": executor.username
}
# 加载明细
items = allocation_item.get_by_order(db, obj.id)
result["items"] = [
{
"id": item.id,
"asset_id": item.asset_id,
"asset_code": item.asset_code,
"asset_name": item.asset_name,
"from_status": item.from_status,
"to_status": item.to_status,
"execute_status": item.execute_status,
"failure_reason": item.failure_reason
}
for item in items
]
return result
def _can_allocate(self, asset_status: str, order_type: str) -> bool:
"""判断资产是否可以分配"""
# 库存中或使用中的资产可以分配
if order_type in ["allocation", "transfer"]:
return asset_status in ["in_stock", "in_use"]
elif order_type == "recovery":
return asset_status == "in_use"
elif order_type == "maintenance":
return asset_status in ["in_stock", "in_use"]
elif order_type == "scrap":
return asset_status in ["in_stock", "in_use", "maintenance"]
return False
def _get_order_type_name(self, order_type: str) -> str:
"""获取单据类型中文名"""
type_names = {
"allocation": "分配",
"transfer": "调拨",
"recovery": "回收",
"maintenance": "维修",
"scrap": "报废"
}
return type_names.get(order_type, "操作")
async def _generate_order_code(self, db: Session, order_type: str) -> str:
"""生成分配单号"""
from datetime import datetime
import random
import string
# 单据类型前缀
prefix_map = {
"allocation": "AL",
"transfer": "TF",
"recovery": "RC",
"maintenance": "MT",
"scrap": "SC"
}
prefix = prefix_map.get(order_type, "AL")
# 日期部分
date_str = datetime.now().strftime("%Y%m%d")
# 序号部分4位随机数
sequence = "".join(random.choices(string.digits, k=4))
# 组合单号: AL202501240001
order_code = f"{prefix}{date_str}{sequence}"
# 检查是否重复,如果重复则重新生成
while allocation_order.get_by_code(db, order_code):
sequence = "".join(random.choices(string.digits, k=4))
order_code = f"{prefix}{date_str}{sequence}"
return order_code
# 创建全局实例
allocation_service = AllocationService()

View File

@@ -0,0 +1,296 @@
"""
资产管理业务服务层
"""
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from app.crud.asset import asset, asset_status_history
from app.schemas.asset import (
AssetCreate,
AssetUpdate,
AssetStatusTransition
)
from app.services.state_machine_service import state_machine_service
from app.utils.asset_code import generate_asset_code
from app.utils.qrcode import generate_qr_code, delete_qr_code
from app.core.exceptions import NotFoundException, AlreadyExistsException, StateTransitionException
class AssetService:
"""资产服务类"""
def __init__(self):
self.state_machine = state_machine_service
async def get_asset(self, db: Session, asset_id: int):
"""获取资产详情"""
obj = asset.get(db, asset_id)
if not obj:
raise NotFoundException("资产")
# 加载关联信息
return self._load_relations(db, obj)
def get_assets(
self,
db: Session,
skip: int = 0,
limit: int = 20,
keyword: Optional[str] = None,
device_type_id: Optional[int] = None,
organization_id: Optional[int] = None,
status: Optional[str] = None,
purchase_date_start: Optional[Any] = None,
purchase_date_end: Optional[Any] = None
) -> Tuple[List, int]:
"""获取资产列表"""
return asset.get_multi(
db=db,
skip=skip,
limit=limit,
keyword=keyword,
device_type_id=device_type_id,
organization_id=organization_id,
status=status,
purchase_date_start=purchase_date_start,
purchase_date_end=purchase_date_end
)
async def create_asset(
self,
db: Session,
obj_in: AssetCreate,
creator_id: int
):
"""创建资产"""
# 检查序列号是否已存在
if obj_in.serial_number:
existing = asset.get_by_serial_number(db, obj_in.serial_number)
if existing:
raise AlreadyExistsException("该序列号已被使用")
# 生成资产编码
asset_code = await generate_asset_code(db)
# 创建资产
db_obj = asset.create(db, obj_in, asset_code, creator_id)
# 生成二维码
try:
qr_code_url = generate_qr_code(asset_code)
db_obj.qr_code_url = qr_code_url
db.add(db_obj)
db.commit()
db.refresh(db_obj)
except Exception as e:
# 二维码生成失败不影响资产创建
pass
# 记录状态历史
await self._record_status_change(
db=db,
asset_id=db_obj.id,
old_status=None,
new_status="pending",
operation_type="create",
operator_id=creator_id,
operator_name=None, # 可以从用户表获取
remark="资产创建"
)
return db_obj
def update_asset(
self,
db: Session,
asset_id: int,
obj_in: AssetUpdate,
updater_id: int
):
"""更新资产"""
db_obj = asset.get(db, asset_id)
if not db_obj:
raise NotFoundException("资产")
# 如果更新序列号,检查是否重复
if obj_in.serial_number and obj_in.serial_number != db_obj.serial_number:
existing = asset.get_by_serial_number(db, obj_in.serial_number)
if existing:
raise AlreadyExistsException("该序列号已被使用")
return asset.update(db, db_obj, obj_in, updater_id)
def delete_asset(
self,
db: Session,
asset_id: int,
deleter_id: int
) -> bool:
"""删除资产"""
if not asset.get(db, asset_id):
raise NotFoundException("资产")
return asset.delete(db, asset_id, deleter_id)
async def change_asset_status(
self,
db: Session,
asset_id: int,
status_transition: AssetStatusTransition,
operator_id: int,
operator_name: Optional[str] = None
):
"""变更资产状态"""
db_obj = asset.get(db, asset_id)
if not db_obj:
raise NotFoundException("资产")
# 验证状态转换
error = self.state_machine.validate_transition(
db_obj.status,
status_transition.new_status
)
if error:
raise StateTransitionException(db_obj.status, status_transition.new_status)
# 更新状态
old_status = db_obj.status
asset.update_status(
db=db,
asset_id=asset_id,
new_status=status_transition.new_status,
updater_id=operator_id
)
# 记录状态历史
await self._record_status_change(
db=db,
asset_id=asset_id,
old_status=old_status,
new_status=status_transition.new_status,
operation_type=self._get_operation_type(old_status, status_transition.new_status),
operator_id=operator_id,
operator_name=operator_name,
remark=status_transition.remark,
extra_data=status_transition.extra_data
)
# 刷新对象
db.refresh(db_obj)
return db_obj
def get_asset_status_history(
self,
db: Session,
asset_id: int,
skip: int = 0,
limit: int = 50
) -> List:
"""获取资产状态历史"""
if not asset.get(db, asset_id):
raise NotFoundException("资产")
return asset_status_history.get_by_asset(db, asset_id, skip, limit)
def scan_asset_by_code(
self,
db: Session,
asset_code: str
):
"""扫码查询资产"""
obj = asset.get_by_code(db, asset_code)
if not obj:
raise NotFoundException("资产")
return self._load_relations(db, obj)
def get_statistics(
self,
db: Session,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""获取资产统计信息"""
query = db.query(
func.count(Asset.id).label("total"),
func.sum(Asset.purchase_price).label("total_value")
).filter(Asset.deleted_at.is_(None))
if organization_id:
query = query.filter(Asset.organization_id == organization_id)
result = query.first()
# 按状态统计
status_query = db.query(
Asset.status,
func.count(Asset.id).label("count")
).filter(
Asset.deleted_at.is_(None)
)
if organization_id:
status_query = status_query.filter(Asset.organization_id == organization_id)
status_query = status_query.group_by(Asset.status)
status_distribution = {row.status: row.count for row in status_query.all()}
return {
"total": result.total or 0,
"total_value": float(result.total_value or 0),
"status_distribution": status_distribution
}
def _load_relations(self, db: Session, obj):
"""加载关联信息"""
# 这里可以预加载关联对象
# 例如: obj.device_type, obj.brand, obj.organization等
return obj
async def _record_status_change(
self,
db: Session,
asset_id: int,
old_status: Optional[str],
new_status: str,
operation_type: str,
operator_id: int,
operator_name: Optional[str] = None,
organization_id: Optional[int] = None,
remark: Optional[str] = None,
extra_data: Optional[Dict[str, Any]] = None
):
"""记录状态变更历史"""
asset_status_history.create(
db=db,
asset_id=asset_id,
old_status=old_status,
new_status=new_status,
operation_type=operation_type,
operator_id=operator_id,
operator_name=operator_name,
organization_id=organization_id,
remark=remark,
extra_data=extra_data
)
def _get_operation_type(self, old_status: str, new_status: str) -> str:
"""根据状态转换获取操作类型"""
operation_map = {
("pending", "in_stock"): "in_stock",
("in_stock", "in_use"): "allocate",
("in_use", "in_stock"): "recover",
("in_stock", "transferring"): "transfer",
("in_use", "transferring"): "transfer",
("transferring", "in_use"): "transfer_complete",
("in_stock", "maintenance"): "maintenance",
("in_use", "maintenance"): "maintenance",
("maintenance", "in_stock"): "maintenance_complete",
("maintenance", "in_use"): "maintenance_complete",
("in_stock", "pending_scrap"): "pending_scrap",
("in_use", "pending_scrap"): "pending_scrap",
("pending_scrap", "scrapped"): "scrap",
("pending_scrap", "in_stock"): "cancel_scrap",
}
return operation_map.get((old_status, new_status), "status_change")
# 创建全局实例
asset_service = AssetService()

View File

@@ -0,0 +1,356 @@
"""
认证服务
"""
from datetime import datetime, timedelta
from typing import Optional
from fastapi import status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import security_manager
from app.core.exceptions import (
InvalidCredentialsException,
UserLockedException,
UserDisabledException,
CaptchaException
)
from app.crud.user import user_crud
from app.models.user import User
from app.schemas.user import LoginResponse, UserInfo
from app.core.config import settings
import uuid
class AuthService:
"""认证服务类"""
def __init__(self):
self.max_login_failures = 5
self.lock_duration_minutes = 30
async def login(
self,
db: AsyncSession,
username: str,
password: str,
captcha: str,
captcha_key: str
) -> LoginResponse:
"""
用户登录
Args:
db: 数据库会话
username: 用户名
password: 密码
captcha: 验证码
captcha_key: 验证码UUID
Returns:
LoginResponse: 登录响应
Raises:
InvalidCredentialsException: 认证失败
UserLockedException: 用户被锁定
UserDisabledException: 用户被禁用
"""
# 验证验证码
import logging
logger = logging.getLogger(__name__)
logger.info(f"开始验证验证码 - captcha_key: {captcha_key}, captcha: {captcha}")
captcha_valid = await self._verify_captcha(captcha_key, captcha)
logger.info(f"验证码验证结果: {captcha_valid}")
if not captcha_valid:
logger.warning(f"验证码验证失败 - captcha_key: {captcha_key}")
raise CaptchaException()
# 获取用户
user = await user_crud.get_by_username(db, username)
if not user:
raise InvalidCredentialsException("用户名或密码错误")
# 检查用户状态
if user.status == "disabled":
raise UserDisabledException()
if user.status == "locked":
# 检查是否已过锁定时间
if user.locked_until and user.locked_until > datetime.utcnow():
raise UserLockedException(f"账户已被锁定,请在 {user.locked_until.strftime('%Y-%m-%d %H:%M:%S')} 后重试")
else:
# 解锁用户
user.status = "active"
user.locked_until = None
user.login_fail_count = 0
await db.commit()
# 验证密码
if not security_manager.verify_password(password, user.password_hash):
# 增加失败次数
user.login_fail_count += 1
# 检查是否需要锁定
if user.login_fail_count >= self.max_login_failures:
user.status = "locked"
user.locked_until = datetime.utcnow() + timedelta(minutes=self.lock_duration_minutes)
await db.commit()
if user.status == "locked":
raise UserLockedException(f"密码错误次数过多,账户已被锁定 {self.lock_duration_minutes} 分钟")
raise InvalidCredentialsException("用户名或密码错误")
# 登录成功,重置失败次数
await user_crud.update_last_login(db, user)
# 生成Token
access_token = security_manager.create_access_token(
data={"sub": str(user.id), "username": user.username}
)
refresh_token = security_manager.create_refresh_token(
data={"sub": str(user.id), "username": user.username}
)
# 获取用户角色和权限
user_info = await self._build_user_info(db, user)
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token,
token_type="Bearer",
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
user=user_info
)
async def refresh_token(self, db: AsyncSession, refresh_token: str) -> dict:
"""
刷新访问令牌
Args:
db: 数据库会话
refresh_token: 刷新令牌
Returns:
dict: 包含新的访问令牌
"""
payload = security_manager.verify_token(refresh_token, token_type="refresh")
user_id = int(payload.get("sub"))
user = await user_crud.get(db, user_id)
if not user or user.status != "active":
raise InvalidCredentialsException("用户不存在或已被禁用")
# 生成新的访问令牌
access_token = security_manager.create_access_token(
data={"sub": str(user.id), "username": user.username}
)
return {
"access_token": access_token,
"expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
async def change_password(
self,
db: AsyncSession,
user: User,
old_password: str,
new_password: str
) -> bool:
"""
修改密码
Args:
db: 数据库会话
user: 当前用户
old_password: 旧密码
new_password: 新密码
Returns:
bool: 是否修改成功
"""
# 验证旧密码
if not security_manager.verify_password(old_password, user.password_hash):
raise InvalidCredentialsException("旧密码错误")
# 更新密码
return await user_crud.update_password(db, user, new_password)
async def reset_password(
self,
db: AsyncSession,
user_id: int,
new_password: str
) -> bool:
"""
重置用户密码(管理员功能)
Args:
db: 数据库会话
user_id: 用户ID
new_password: 新密码
Returns:
bool: 是否重置成功
"""
user = await user_crud.get(db, user_id)
if not user:
return False
return await user_crud.update_password(db, user, new_password)
async def _generate_captcha(self) -> dict:
"""
生成验证码
Returns:
包含captcha_key和captcha_base64的字典
"""
import logging
logger = logging.getLogger(__name__)
from app.utils.redis_client import redis_client
import random
import string
import base64
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
try:
# 生成4位随机验证码使用更清晰的字符组合排除易混淆的字符
captcha_text = ''.join(random.choices('23456789', k=4)) # 排除0、1
logger.info(f"生成验证码: {captcha_text}")
# 生成验证码图片
width, height = 200, 80 # 增大图片尺寸
# 使用浅色背景而不是纯白
background_color = (245, 245, 250) # 浅蓝灰色
image = Image.new('RGB', (width, height), color=background_color)
draw = ImageDraw.Draw(image)
# 尝试使用更大的字体
try:
# 优先使用系统大字体
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
except:
try:
font = ImageFont.truetype("arial.ttf", 48) # 增大到48
except:
font = ImageFont.load_default()
# 如果使用默认字体,尝试放大
font = font.font_variant(size=48)
# 绘制验证码
draw.text((10, 5), captcha_text, fill='black', font=font)
# 减少干扰线数量从5条减少到3条
for _ in range(3):
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
draw.line([(x1, y1), (x2, y2)], fill='gray', width=1)
# 添加噪点(可选)
for _ in range(50):
x = random.randint(0, width - 1)
y = random.randint(0, height - 1)
draw.point((x, y), fill='lightgray')
# 转换为base64
buffer = BytesIO()
image.save(buffer, format='PNG')
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 生成captcha_key
captcha_key = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
logger.info(f"生成验证码Key: {captcha_key}")
# 存储到Redis5分钟过期
await redis_client.setex(
f"captcha:{captcha_key}",
300,
captcha_text
)
logger.info(f"验证码已存储到Redis: captcha:{captcha_key}, 值: {captcha_text}")
return {
"captcha_key": captcha_key,
"captcha_base64": f"data:image/png;base64,{image_base64}"
}
except Exception as e:
logger.error(f"生成验证码失败: {str(e)}", exc_info=True)
raise
async def _verify_captcha(self, captcha_key: str, captcha: str) -> bool:
"""
验证验证码
Args:
captcha_key: 验证码密钥
captcha: 用户输入的验证码
Returns:
验证是否成功
"""
import logging
logger = logging.getLogger(__name__)
from app.utils.redis_client import redis_client
try:
# 从Redis获取存储的验证码
stored_captcha = await redis_client.get(f"captcha:{captcha_key}")
logger.info(f"Redis中存储的验证码: {stored_captcha}, 用户输入: {captcha}")
if not stored_captcha:
logger.warning(f"验证码已过期或不存在 - captcha_key: {captcha_key}")
return False
# 验证码不区分大小写
is_valid = stored_captcha.lower() == captcha.lower()
logger.info(f"验证码匹配结果: {is_valid}")
return is_valid
except Exception as e:
logger.error(f"验证码验证异常: {str(e)}", exc_info=True)
return False
async def _build_user_info(self, db: AsyncSession, user: User) -> UserInfo:
"""
构建用户信息
Args:
db: 数据库会话
user: 用户对象
Returns:
UserInfo: 用户信息
"""
# 获取用户角色代码列表
role_codes = [role.role_code for role in user.roles]
# 获取用户权限代码列表
permissions = []
for role in user.roles:
for perm in role.permissions:
permissions.append(perm.permission_code)
# 如果是超级管理员,给予所有权限
if user.is_admin:
permissions = ["*:*:*"]
return UserInfo(
id=user.id,
username=user.username,
real_name=user.real_name,
email=user.email,
avatar_url=user.avatar_url,
is_admin=user.is_admin,
status=user.status
)
# 创建服务实例
auth_service = AuthService()

View File

@@ -0,0 +1,134 @@
"""
品牌和供应商业务服务层
"""
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from app.crud.brand_supplier import brand, supplier
from app.schemas.brand_supplier import (
BrandCreate,
BrandUpdate,
SupplierCreate,
SupplierUpdate
)
from app.core.exceptions import NotFoundException, AlreadyExistsException
class BrandService:
"""品牌服务类"""
def get_brand(self, db: Session, brand_id: int):
"""获取品牌详情"""
obj = brand.get(db, brand_id)
if not obj:
raise NotFoundException("品牌")
return obj
def get_brands(
self,
db: Session,
skip: int = 0,
limit: int = 20,
status: Optional[str] = None,
keyword: Optional[str] = None
) -> Tuple[List, int]:
"""获取品牌列表"""
return brand.get_multi(db, skip, limit, status, keyword)
def create_brand(
self,
db: Session,
obj_in: BrandCreate,
creator_id: Optional[int] = None
):
"""创建品牌"""
try:
return brand.create(db, obj_in, creator_id)
except ValueError as e:
raise AlreadyExistsException("品牌") from e
def update_brand(
self,
db: Session,
brand_id: int,
obj_in: BrandUpdate,
updater_id: Optional[int] = None
):
"""更新品牌"""
db_obj = brand.get(db, brand_id)
if not db_obj:
raise NotFoundException("品牌")
return brand.update(db, db_obj, obj_in, updater_id)
def delete_brand(
self,
db: Session,
brand_id: int,
deleter_id: Optional[int] = None
) -> bool:
"""删除品牌"""
if not brand.get(db, brand_id):
raise NotFoundException("品牌")
return brand.delete(db, brand_id, deleter_id)
class SupplierService:
"""供应商服务类"""
def get_supplier(self, db: Session, supplier_id: int):
"""获取供应商详情"""
obj = supplier.get(db, supplier_id)
if not obj:
raise NotFoundException("供应商")
return obj
def get_suppliers(
self,
db: Session,
skip: int = 0,
limit: int = 20,
status: Optional[str] = None,
keyword: Optional[str] = None
) -> Tuple[List, int]:
"""获取供应商列表"""
return supplier.get_multi(db, skip, limit, status, keyword)
def create_supplier(
self,
db: Session,
obj_in: SupplierCreate,
creator_id: Optional[int] = None
):
"""创建供应商"""
try:
return supplier.create(db, obj_in, creator_id)
except ValueError as e:
raise AlreadyExistsException("供应商") from e
def update_supplier(
self,
db: Session,
supplier_id: int,
obj_in: SupplierUpdate,
updater_id: Optional[int] = None
):
"""更新供应商"""
db_obj = supplier.get(db, supplier_id)
if not db_obj:
raise NotFoundException("供应商")
return supplier.update(db, db_obj, obj_in, updater_id)
def delete_supplier(
self,
db: Session,
supplier_id: int,
deleter_id: Optional[int] = None
) -> bool:
"""删除供应商"""
if not supplier.get(db, supplier_id):
raise NotFoundException("供应商")
return supplier.delete(db, supplier_id, deleter_id)
# 创建全局实例
brand_service = BrandService()
supplier_service = SupplierService()

View File

@@ -0,0 +1,286 @@
"""
设备类型业务服务层
"""
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from app.crud.device_type import device_type, device_type_field
from app.schemas.device_type import (
DeviceTypeCreate,
DeviceTypeUpdate,
DeviceTypeFieldCreate,
DeviceTypeFieldUpdate
)
from app.core.exceptions import NotFoundException, AlreadyExistsException
class DeviceTypeService:
"""设备类型服务类"""
def get_device_type(self, db: Session, device_type_id: int, include_fields: bool = False):
"""
获取设备类型详情
Args:
db: 数据库会话
device_type_id: 设备类型ID
include_fields: 是否包含字段列表
Returns:
设备类型对象
Raises:
NotFoundException: 设备类型不存在
"""
obj = device_type.get(db, device_type_id)
if not obj:
raise NotFoundException("设备类型")
# 计算字段数量
field_count = device_type_field.get_by_device_type(db, device_type_id)
obj.field_count = len(field_count)
return obj
def get_device_types(
self,
db: Session,
skip: int = 0,
limit: int = 20,
category: Optional[str] = None,
status: Optional[str] = None,
keyword: Optional[str] = None
) -> Tuple[List, int]:
"""
获取设备类型列表
Args:
db: 数据库会话
skip: 跳过条数
limit: 返回条数
category: 设备分类
status: 状态
keyword: 搜索关键词
Returns:
(设备类型列表, 总数)
"""
items, total = device_type.get_multi(
db=db,
skip=skip,
limit=limit,
category=category,
status=status,
keyword=keyword
)
# 为每个项目添加字段数量
for item in items:
fields = device_type_field.get_by_device_type(db, item.id)
item.field_count = len(fields)
return items, total
def create_device_type(
self,
db: Session,
obj_in: DeviceTypeCreate,
creator_id: Optional[int] = None
):
"""
创建设备类型
Args:
db: 数据库会话
obj_in: 创建数据
creator_id: 创建人ID
Returns:
创建的设备类型对象
Raises:
AlreadyExistsException: 设备类型代码已存在
"""
try:
return device_type.create(db, obj_in, creator_id)
except ValueError as e:
raise AlreadyExistsException("设备类型") from e
def update_device_type(
self,
db: Session,
device_type_id: int,
obj_in: DeviceTypeUpdate,
updater_id: Optional[int] = None
):
"""
更新设备类型
Args:
db: 数据库会话
device_type_id: 设备类型ID
obj_in: 更新数据
updater_id: 更新人ID
Returns:
更新后的设备类型对象
Raises:
NotFoundException: 设备类型不存在
"""
db_obj = device_type.get(db, device_type_id)
if not db_obj:
raise NotFoundException("设备类型")
return device_type.update(db, db_obj, obj_in, updater_id)
def delete_device_type(
self,
db: Session,
device_type_id: int,
deleter_id: Optional[int] = None
) -> bool:
"""
删除设备类型
Args:
db: 数据库会话
device_type_id: 设备类型ID
deleter_id: 删除人ID
Returns:
是否删除成功
Raises:
NotFoundException: 设备类型不存在
"""
if not device_type.get(db, device_type_id):
raise NotFoundException("设备类型")
return device_type.delete(db, device_type_id, deleter_id)
def get_device_type_fields(
self,
db: Session,
device_type_id: int,
status: Optional[str] = None
) -> List:
"""
获取设备类型的字段列表
Args:
db: 数据库会话
device_type_id: 设备类型ID
status: 状态筛选
Returns:
字段列表
Raises:
NotFoundException: 设备类型不存在
"""
# 验证设备类型存在
if not device_type.get(db, device_type_id):
raise NotFoundException("设备类型")
return device_type_field.get_by_device_type(db, device_type_id, status)
def create_device_type_field(
self,
db: Session,
device_type_id: int,
obj_in: DeviceTypeFieldCreate,
creator_id: Optional[int] = None
):
"""
创建设备类型字段
Args:
db: 数据库会话
device_type_id: 设备类型ID
obj_in: 创建数据
creator_id: 创建人ID
Returns:
创建的字段对象
Raises:
NotFoundException: 设备类型不存在
AlreadyExistsException: 字段代码已存在
"""
# 验证设备类型存在
if not device_type.get(db, device_type_id):
raise NotFoundException("设备类型")
try:
return device_type_field.create(db, obj_in, device_type_id, creator_id)
except ValueError as e:
raise AlreadyExistsException("字段") from e
def update_device_type_field(
self,
db: Session,
field_id: int,
obj_in: DeviceTypeFieldUpdate,
updater_id: Optional[int] = None
):
"""
更新设备类型字段
Args:
db: 数据库会话
field_id: 字段ID
obj_in: 更新数据
updater_id: 更新人ID
Returns:
更新后的字段对象
Raises:
NotFoundException: 字段不存在
"""
db_obj = device_type_field.get(db, field_id)
if not db_obj:
raise NotFoundException("字段")
return device_type_field.update(db, db_obj, obj_in, updater_id)
def delete_device_type_field(
self,
db: Session,
field_id: int,
deleter_id: Optional[int] = None
) -> bool:
"""
删除设备类型字段
Args:
db: 数据库会话
field_id: 字段ID
deleter_id: 删除人ID
Returns:
是否删除成功
Raises:
NotFoundException: 字段不存在
"""
if not device_type_field.get(db, field_id):
raise NotFoundException("字段")
return device_type_field.delete(db, field_id, deleter_id)
def get_all_categories(self, db: Session) -> List[str]:
"""
获取所有设备分类
Args:
db: 数据库会话
Returns:
设备分类列表
"""
return device_type.get_all_categories(db)
# 创建全局实例
device_type_service = DeviceTypeService()

View File

@@ -0,0 +1,508 @@
"""
文件存储服务
"""
import os
import uuid
import secrets
import mimetypes
from typing import Optional, Dict, Any, List, Tuple
from pathlib import Path
from datetime import datetime, timedelta
from fastapi import UploadFile, HTTPException, status
from sqlalchemy.orm import Session
from PIL import Image
import io
from app.models.file_management import UploadedFile
from app.schemas.file_management import (
UploadedFileCreate,
FileUploadResponse,
FileShareResponse,
FileStatistics
)
from app.crud.file_management import uploaded_file as crud_uploaded_file
class FileService:
"""文件存储服务"""
# 允许的文件类型白名单
ALLOWED_MIME_TYPES = {
# 图片
'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', 'image/svg+xml',
# 文档
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain', 'text/csv',
# 压缩包
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
# 其他
'application/json', 'application/xml', 'text/xml'
}
# 文件大小限制(字节)- 默认100MB
MAX_FILE_SIZE = 100 * 1024 * 1024
# 图片文件大小限制 - 默认10MB
MAX_IMAGE_SIZE = 10 * 1024 * 1024
# Magic Numbers for file validation
MAGIC_NUMBERS = {
b'\xFF\xD8\xFF': 'image/jpeg',
b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A': 'image/png',
b'GIF87a': 'image/gif',
b'GIF89a': 'image/gif',
b'%PDF': 'application/pdf',
b'PK\x03\x04': 'application/zip',
}
def __init__(self, base_upload_dir: str = "uploads"):
self.base_upload_dir = Path(base_upload_dir)
self.ensure_upload_dirs()
def ensure_upload_dirs(self):
"""确保上传目录存在"""
directories = [
self.base_upload_dir,
self.base_upload_dir / "images",
self.base_upload_dir / "documents",
self.base_upload_dir / "thumbnails",
self.base_upload_dir / "temp",
]
for directory in directories:
directory.mkdir(parents=True, exist_ok=True)
def validate_file_type(self, file: UploadFile) -> bool:
"""验证文件类型"""
# 检查MIME类型
if file.content_type not in self.ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"不支持的文件类型: {file.content_type}"
)
return True
def validate_file_size(self, file: UploadFile) -> bool:
"""验证文件大小"""
# 先检查是否是图片
if file.content_type and file.content_type.startswith('image/'):
max_size = self.MAX_IMAGE_SIZE
else:
max_size = self.MAX_FILE_SIZE
# 读取文件内容检查大小
content = file.file.read()
file.file.seek(0) # 重置文件指针
if len(content) > max_size:
# 转换为MB
size_mb = max_size / (1024 * 1024)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"文件大小超过限制: {size_mb:.0f}MB"
)
return True
def validate_file_content(self, content: bytes) -> str:
"""验证文件内容Magic Number"""
for magic, mime_type in self.MAGIC_NUMBERS.items():
if content.startswith(magic):
return mime_type
return None
async def upload_file(
self,
db: Session,
file: UploadFile,
uploader_id: int,
remark: Optional[str] = None
) -> UploadedFile:
"""
上传文件
Args:
db: 数据库会话
file: 上传的文件
uploader_id: 上传者ID
remark: 备注
Returns:
UploadedFile: 创建的文件记录
"""
# 验证文件类型
self.validate_file_type(file)
# 验证文件大小
self.validate_file_size(file)
# 读取文件内容
content = await file.read()
# 验证文件内容
detected_mime = self.validate_file_content(content)
if detected_mime and detected_mime != file.content_type:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"文件内容与扩展名不匹配"
)
# 生成文件名
file_ext = self.get_file_extension(file.filename)
unique_filename = f"{uuid.uuid4()}{file_ext}"
# 确定存储路径
upload_date = datetime.utcnow()
date_dir = upload_date.strftime("%Y/%m/%d")
save_dir = self.base_upload_dir / date_dir
save_dir.mkdir(parents=True, exist_ok=True)
file_path = save_dir / unique_filename
# 保存文件
with open(file_path, "wb") as f:
f.write(content)
# 生成缩略图(如果是图片)
thumbnail_path = None
if file.content_type and file.content_type.startswith('image/'):
thumbnail_path = self.generate_thumbnail(content, unique_filename, date_dir)
# 创建数据库记录
file_create = UploadedFileCreate(
file_name=unique_filename,
original_name=file.filename,
file_path=str(file_path),
file_size=len(content),
file_type=file.content_type,
file_ext=file_ext.lstrip('.'),
uploader_id=uploader_id
)
db_obj = crud_uploaded_file.create(db, obj_in=file_create.dict())
# 更新缩略图路径
if thumbnail_path:
crud_uploaded_file.update(db, db_obj=db_obj, obj_in={"thumbnail_path": thumbnail_path})
# 模拟病毒扫描
self._scan_virus(file_path)
return db_obj
def generate_thumbnail(
self,
content: bytes,
filename: str,
date_dir: str
) -> Optional[str]:
"""生成缩略图"""
try:
# 打开图片
image = Image.open(io.BytesIO(content))
# 转换为RGB如果是RGBA
if image.mode in ('RGBA', 'P'):
image = image.convert('RGB')
# 创建缩略图
thumbnail_size = (200, 200)
image.thumbnail(thumbnail_size, Image.Resampling.LANCZOS)
# 保存缩略图
thumbnail_dir = self.base_upload_dir / "thumbnails" / date_dir
thumbnail_dir.mkdir(parents=True, exist_ok=True)
thumbnail_name = f"thumb_{filename}"
thumbnail_path = thumbnail_dir / thumbnail_name
image.save(thumbnail_path, 'JPEG', quality=85)
return str(thumbnail_path)
except Exception as e:
print(f"生成缩略图失败: {e}")
return None
def get_file_path(self, file_obj: UploadedFile) -> Path:
"""获取文件路径"""
return Path(file_obj.file_path)
def file_exists(self, file_obj: UploadedFile) -> bool:
"""检查文件是否存在"""
file_path = self.get_file_path(file_obj)
return file_path.exists() and file_path.is_file()
def delete_file_from_disk(self, file_obj: UploadedFile) -> bool:
"""从磁盘删除文件"""
try:
file_path = self.get_file_path(file_obj)
if file_path.exists():
file_path.unlink()
# 删除缩略图
if file_obj.thumbnail_path:
thumbnail_path = Path(file_obj.thumbnail_path)
if thumbnail_path.exists():
thumbnail_path.unlink()
return True
except Exception as e:
print(f"删除文件失败: {e}")
return False
def generate_share_link(
self,
db: Session,
file_id: int,
expire_days: int = 7,
base_url: str = "http://localhost:8000"
) -> FileShareResponse:
"""
生成分享链接
Args:
db: 数据库会话
file_id: 文件ID
expire_days: 有效期(天)
base_url: 基础URL
Returns:
FileShareResponse: 分享链接信息
"""
# 生成分享码
share_code = crud_uploaded_file.generate_share_code(
db,
file_id=file_id,
expire_days=expire_days
)
if not share_code:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="文件不存在"
)
# 获取文件信息
file_obj = crud_uploaded_file.get(db, file_id)
expire_time = file_obj.share_expire_time
# 生成分享URL
share_url = f"{base_url}/api/v1/files/share/{share_code}"
return FileShareResponse(
share_code=share_code,
share_url=share_url,
expire_time=expire_time
)
def get_shared_file(self, db: Session, share_code: str) -> Optional[UploadedFile]:
"""通过分享码获取文件"""
return crud_uploaded_file.get_by_share_code(db, share_code)
def get_statistics(
self,
db: Session,
uploader_id: Optional[int] = None
) -> FileStatistics:
"""获取文件统计信息"""
stats = crud_uploaded_file.get_statistics(db, uploader_id=uploader_id)
return FileStatistics(**stats)
@staticmethod
def get_file_extension(filename: str) -> str:
"""获取文件扩展名"""
return os.path.splitext(filename)[1]
@staticmethod
def get_mime_type(filename: str) -> str:
"""获取MIME类型"""
mime_type, _ = mimetypes.guess_type(filename)
return mime_type or 'application/octet-stream'
@staticmethod
def _scan_virus(file_path: Path) -> bool:
"""
模拟病毒扫描
实际生产环境应集成专业杀毒软件如:
- ClamAV
- VirusTotal API
- Windows Defender
"""
# 模拟扫描
import time
time.sleep(0.1) # 模拟扫描时间
return True # 假设文件安全
# 分片上传管理
class ChunkUploadManager:
"""分片上传管理器"""
def __init__(self):
self.uploads: Dict[str, Dict[str, Any]] = {}
def init_upload(
self,
file_name: str,
file_size: int,
file_type: str,
total_chunks: int,
file_hash: Optional[str] = None
) -> str:
"""初始化分片上传"""
upload_id = str(uuid.uuid4())
self.uploads[upload_id] = {
"file_name": file_name,
"file_size": file_size,
"file_type": file_type,
"total_chunks": total_chunks,
"file_hash": file_hash,
"uploaded_chunks": [],
"created_at": datetime.utcnow()
}
return upload_id
def save_chunk(
self,
upload_id: str,
chunk_index: int,
chunk_data: bytes
) -> bool:
"""保存分片"""
if upload_id not in self.uploads:
return False
upload_info = self.uploads[upload_id]
# 保存分片到临时文件
temp_dir = Path("uploads/temp")
temp_dir.mkdir(parents=True, exist_ok=True)
chunk_filename = f"{upload_id}_chunk_{chunk_index}"
chunk_path = temp_dir / chunk_filename
with open(chunk_path, "wb") as f:
f.write(chunk_data)
# 记录已上传的分片
if chunk_index not in upload_info["uploaded_chunks"]:
upload_info["uploaded_chunks"].append(chunk_index)
return True
def is_complete(self, upload_id: str) -> bool:
"""检查是否所有分片都已上传"""
if upload_id not in self.uploads:
return False
upload_info = self.uploads[upload_id]
return len(upload_info["uploaded_chunks"]) == upload_info["total_chunks"]
def merge_chunks(
self,
db: Session,
upload_id: str,
uploader_id: int,
file_service: FileService
) -> UploadedFile:
"""合并分片"""
if upload_id not in self.uploads:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="上传会话不存在"
)
if not self.is_complete(upload_id):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="分片未全部上传"
)
upload_info = self.uploads[upload_id]
# 合并分片
temp_dir = Path("uploads/temp")
merged_content = b""
for i in range(upload_info["total_chunks"]):
chunk_filename = f"{upload_id}_chunk_{i}"
chunk_path = temp_dir / chunk_filename
if not chunk_path.exists():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"分片 {i} 不存在"
)
with open(chunk_path, "rb") as f:
merged_content += f.read()
# 验证文件大小
if len(merged_content) != upload_info["file_size"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="文件大小不匹配"
)
# 验证文件哈希(如果提供)
if upload_info["file_hash"]:
import hashlib
file_hash = hashlib.md5(merged_content).hexdigest()
if file_hash != upload_info["file_hash"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="文件哈希不匹配"
)
# 保存文件
file_ext = Path(upload_info["file_name"]).suffix
unique_filename = f"{uuid.uuid4()}{file_ext}"
upload_date = datetime.utcnow()
date_dir = upload_date.strftime("%Y/%m/%d")
save_dir = Path("uploads") / date_dir
save_dir.mkdir(parents=True, exist_ok=True)
file_path = save_dir / unique_filename
with open(file_path, "wb") as f:
f.write(merged_content)
# 清理临时文件
self.cleanup_upload(upload_id)
# 创建数据库记录
from app.schemas.file_management import UploadedFileCreate
file_create = UploadedFileCreate(
file_name=unique_filename,
original_name=upload_info["file_name"],
file_path=str(file_path),
file_size=upload_info["file_size"],
file_type=upload_info["file_type"],
file_ext=file_ext.lstrip('.'),
uploader_id=uploader_id
)
db_obj = crud_uploaded_file.create(db, obj_in=file_create.dict())
return db_obj
def cleanup_upload(self, upload_id: str):
"""清理上传会话"""
if upload_id in self.uploads:
del self.uploads[upload_id]
# 清理临时分片文件
temp_dir = Path("uploads/temp")
for chunk_file in temp_dir.glob(f"{upload_id}_chunk_*"):
chunk_file.unlink()
# 创建服务实例
file_service = FileService()
chunk_upload_manager = ChunkUploadManager()

View File

@@ -0,0 +1,403 @@
"""
维修管理业务服务层
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session, selectinload
from app.crud.maintenance import maintenance_record
from app.crud.asset import asset
from app.schemas.maintenance import (
MaintenanceRecordCreate,
MaintenanceRecordUpdate,
MaintenanceRecordStart,
MaintenanceRecordComplete
)
from app.core.exceptions import NotFoundException, BusinessException
class MaintenanceService:
"""维修管理服务类"""
async def get_record(
self,
db: Session,
record_id: int
) -> Dict[str, Any]:
"""获取维修记录详情"""
# 使用selectinload预加载关联数据避免N+1查询
from app.models.maintenance import MaintenanceRecord
from app.models.asset import Asset
from app.models.user import User
from app.models.brand_supplier import Supplier
obj = db.query(
MaintenanceRecord
).options(
selectinload(MaintenanceRecord.asset.of_type(Asset)),
selectinload(MaintenanceRecord.report_user.of_type(User)),
selectinload(MaintenanceRecord.maintenance_user.of_type(User)),
selectinload(MaintenanceRecord.vendor.of_type(Supplier))
).filter(
MaintenanceRecord.id == record_id
).first()
if not obj:
raise NotFoundException("维修记录")
return self._load_relations(db, obj)
def get_records(
self,
db: Session,
skip: int = 0,
limit: int = 20,
asset_id: Optional[int] = None,
status: Optional[str] = None,
fault_type: Optional[str] = None,
priority: Optional[str] = None,
maintenance_type: Optional[str] = None,
keyword: Optional[str] = None
) -> tuple:
"""获取维修记录列表"""
items, total = maintenance_record.get_multi(
db=db,
skip=skip,
limit=limit,
asset_id=asset_id,
status=status,
fault_type=fault_type,
priority=priority,
maintenance_type=maintenance_type,
keyword=keyword
)
# 加载关联信息
items_with_relations = [self._load_relations(db, item) for item in items]
return items_with_relations, total
async def create_record(
self,
db: Session,
obj_in: MaintenanceRecordCreate,
report_user_id: int,
creator_id: int
):
"""创建维修记录"""
# 验证资产存在
asset_obj = asset.get(db, obj_in.asset_id)
if not asset_obj:
raise NotFoundException("资产")
# 生成维修单号
record_code = await self._generate_record_code(db)
# 创建维修记录
db_obj = maintenance_record.create(
db=db,
obj_in=obj_in,
record_code=record_code,
asset_code=asset_obj.asset_code,
report_user_id=report_user_id,
creator_id=creator_id
)
# 如果资产状态不是维修中,则更新状态
if asset_obj.status != "maintenance":
from app.services.asset_service import asset_service
from app.schemas.asset import AssetStatusTransition
try:
await asset_service.change_asset_status(
db=db,
asset_id=asset_obj.id,
status_transition=AssetStatusTransition(
new_status="maintenance",
remark=f"报修: {record_code}"
),
operator_id=report_user_id
)
except Exception as e:
# 状态更新失败不影响维修记录创建
pass
return self._load_relations(db, db_obj)
def update_record(
self,
db: Session,
record_id: int,
obj_in: MaintenanceRecordUpdate,
updater_id: int
):
"""更新维修记录"""
db_obj = maintenance_record.get(db, record_id)
if not db_obj:
raise NotFoundException("维修记录")
# 已完成的维修记录不能更新
if db_obj.status == "completed":
raise BusinessException("已完成的维修记录不能更新")
return maintenance_record.update(db, db_obj, obj_in, updater_id)
async def start_maintenance(
self,
db: Session,
record_id: int,
start_in: MaintenanceRecordStart,
maintenance_user_id: int
):
"""开始维修"""
db_obj = maintenance_record.get(db, record_id)
if not db_obj:
raise NotFoundException("维修记录")
# 检查状态
if db_obj.status != "pending":
raise BusinessException("只有待处理状态的维修记录可以开始维修")
# 验证维修类型
if start_in.maintenance_type == "vendor_repair" and not start_in.vendor_id:
raise BusinessException("外部维修必须指定维修供应商")
# 开始维修
db_obj = maintenance_record.start_maintenance(
db=db,
db_obj=db_obj,
maintenance_type=start_in.maintenance_type,
maintenance_user_id=maintenance_user_id,
vendor_id=start_in.vendor_id
)
return self._load_relations(db, db_obj)
async def complete_maintenance(
self,
db: Session,
record_id: int,
complete_in: MaintenanceRecordComplete,
maintenance_user_id: int
):
"""完成维修"""
db_obj = maintenance_record.get(db, record_id)
if not db_obj:
raise NotFoundException("维修记录")
# 检查状态
if db_obj.status != "in_progress":
raise BusinessException("只有维修中的记录可以完成")
# 完成维修
db_obj = maintenance_record.complete_maintenance(
db=db,
db_obj=db_obj,
maintenance_result=complete_in.maintenance_result,
maintenance_cost=complete_in.maintenance_cost,
replaced_parts=complete_in.replaced_parts,
images=complete_in.images
)
# 恢复资产状态
from app.services.asset_service import asset_service
from app.schemas.asset import AssetStatusTransition
try:
await asset_service.change_asset_status(
db=db,
asset_id=db_obj.asset_id,
status_transition=AssetStatusTransition(
new_status=complete_in.asset_status,
remark=f"维修完成: {db_obj.record_code}"
),
operator_id=maintenance_user_id
)
except Exception as e:
# 状态更新失败不影响维修记录完成
pass
return self._load_relations(db, db_obj)
def cancel_maintenance(
self,
db: Session,
record_id: int
):
"""取消维修"""
db_obj = maintenance_record.get(db, record_id)
if not db_obj:
raise NotFoundException("维修记录")
# 检查状态
if db_obj.status == "completed":
raise BusinessException("已完成的维修记录不能取消")
# 取消维修
db_obj = maintenance_record.cancel_maintenance(db, db_obj)
# 恢复资产状态
asset_obj = asset.get(db, db_obj.asset_id)
if asset_obj and asset_obj.status == "maintenance":
from app.services.asset_service import asset_service
from app.schemas.asset import AssetStatusTransition
try:
# 根据维修前的状态恢复
target_status = "in_stock" # 默认恢复为库存中
asset_service.change_asset_status(
db=db,
asset_id=asset_obj.id,
status_transition=AssetStatusTransition(
new_status=target_status,
remark=f"取消维修: {db_obj.record_code}"
),
operator_id=db_obj.report_user_id or 0
)
except Exception as e:
# 状态更新失败不影响维修记录取消
pass
return self._load_relations(db, db_obj)
def delete_record(
self,
db: Session,
record_id: int
) -> bool:
"""删除维修记录"""
db_obj = maintenance_record.get(db, record_id)
if not db_obj:
raise NotFoundException("维修记录")
# 只能删除待处理或已取消的记录
if db_obj.status not in ["pending", "cancelled"]:
raise BusinessException("只能删除待处理或已取消的维修记录")
return maintenance_record.delete(db, record_id)
def get_asset_records(
self,
db: Session,
asset_id: int,
skip: int = 0,
limit: int = 50
) -> List:
"""获取资产的维修记录"""
# 验证资产存在
if not asset.get(db, asset_id):
raise NotFoundException("资产")
records = maintenance_record.get_by_asset(db, asset_id, skip, limit)
return [self._load_relations(db, record) for record in records]
def get_statistics(
self,
db: Session,
asset_id: Optional[int] = None
) -> Dict[str, Any]:
"""获取维修统计信息"""
return maintenance_record.get_statistics(db, asset_id)
def _load_relations(
self,
db: Session,
obj
) -> Dict[str, Any]:
"""加载维修记录关联信息"""
from app.models.asset import Asset
from app.models.user import User
from app.models.brand_supplier import Supplier
result = {
"id": obj.id,
"record_code": obj.record_code,
"asset_id": obj.asset_id,
"asset_code": obj.asset_code,
"fault_description": obj.fault_description,
"fault_type": obj.fault_type,
"report_user_id": obj.report_user_id,
"report_time": obj.report_time,
"priority": obj.priority,
"maintenance_type": obj.maintenance_type,
"vendor_id": obj.vendor_id,
"maintenance_cost": float(obj.maintenance_cost) if obj.maintenance_cost else None,
"start_time": obj.start_time,
"complete_time": obj.complete_time,
"maintenance_user_id": obj.maintenance_user_id,
"maintenance_result": obj.maintenance_result,
"replaced_parts": obj.replaced_parts,
"status": obj.status,
"images": obj.images,
"remark": obj.remark,
"created_at": obj.created_at,
"updated_at": obj.updated_at
}
# 加载资产信息
if obj.asset_id:
asset_obj = db.query(Asset).filter(Asset.id == obj.asset_id).first()
if asset_obj:
result["asset"] = {
"id": asset_obj.id,
"asset_code": asset_obj.asset_code,
"asset_name": asset_obj.asset_name,
"status": asset_obj.status
}
# 加载报修人信息
if obj.report_user_id:
report_user = db.query(User).filter(User.id == obj.report_user_id).first()
if report_user:
result["report_user"] = {
"id": report_user.id,
"real_name": report_user.real_name,
"username": report_user.username
}
# 加载维修人员信息
if obj.maintenance_user_id:
maintenance_user = db.query(User).filter(User.id == obj.maintenance_user_id).first()
if maintenance_user:
result["maintenance_user"] = {
"id": maintenance_user.id,
"real_name": maintenance_user.real_name,
"username": maintenance_user.username
}
# 加载供应商信息
if obj.vendor_id:
vendor = db.query(Supplier).filter(Supplier.id == obj.vendor_id).first()
if vendor:
result["vendor"] = {
"id": vendor.id,
"supplier_name": vendor.supplier_name,
"contact_person": vendor.contact_person,
"contact_phone": vendor.contact_phone
}
return result
async def _generate_record_code(self, db: Session) -> str:
"""生成维修单号"""
from datetime import datetime
import random
import string
# 日期部分
date_str = datetime.now().strftime("%Y%m%d")
# 序号部分4位随机数
sequence = "".join(random.choices(string.digits, k=4))
# 组合单号: MT202501240001
record_code = f"MT{date_str}{sequence}"
# 检查是否重复,如果重复则重新生成
while maintenance_record.get_by_code(db, record_code):
sequence = "".join(random.choices(string.digits, k=4))
record_code = f"MT{date_str}{sequence}"
return record_code
# 创建全局实例
maintenance_service = MaintenanceService()

View File

@@ -0,0 +1,402 @@
"""
消息通知服务层
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.crud.notification import notification_crud
from app.models.notification import NotificationTemplate
from app.models.user import User
from app.schemas.notification import (
NotificationCreate,
NotificationBatchCreate,
NotificationSendFromTemplate
)
import json
class NotificationService:
"""消息通知服务类"""
async def get_notification(self, db: AsyncSession, notification_id: int) -> Optional[Dict[str, Any]]:
"""
获取消息通知详情
Args:
db: 数据库会话
notification_id: 通知ID
Returns:
通知信息
"""
notification = await notification_crud.get(db, notification_id)
if not notification:
return None
return {
"id": notification.id,
"recipient_id": notification.recipient_id,
"recipient_name": notification.recipient_name,
"title": notification.title,
"content": notification.content,
"notification_type": notification.notification_type,
"priority": notification.priority,
"is_read": notification.is_read,
"read_at": notification.read_at,
"related_entity_type": notification.related_entity_type,
"related_entity_id": notification.related_entity_id,
"action_url": notification.action_url,
"extra_data": notification.extra_data,
"sent_via_email": notification.sent_via_email,
"sent_via_sms": notification.sent_via_sms,
"created_at": notification.created_at,
"expire_at": notification.expire_at,
}
async def get_notifications(
self,
db: AsyncSession,
*,
skip: int = 0,
limit: int = 20,
recipient_id: Optional[int] = None,
notification_type: Optional[str] = None,
priority: Optional[str] = None,
is_read: Optional[bool] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
keyword: Optional[str] = None
) -> Dict[str, Any]:
"""
获取消息通知列表
Args:
db: 数据库会话
skip: 跳过条数
limit: 返回条数
recipient_id: 接收人ID
notification_type: 通知类型
priority: 优先级
is_read: 是否已读
start_time: 开始时间
end_time: 结束时间
keyword: 关键词
Returns:
通知列表和总数
"""
items, total = await notification_crud.get_multi(
db,
skip=skip,
limit=limit,
recipient_id=recipient_id,
notification_type=notification_type,
priority=priority,
is_read=is_read,
start_time=start_time,
end_time=end_time,
keyword=keyword
)
return {
"items": [
{
"id": item.id,
"recipient_id": item.recipient_id,
"recipient_name": item.recipient_name,
"title": item.title,
"content": item.content,
"notification_type": item.notification_type,
"priority": item.priority,
"is_read": item.is_read,
"read_at": item.read_at,
"action_url": item.action_url,
"created_at": item.created_at,
}
for item in items
],
"total": total
}
async def create_notification(
self,
db: AsyncSession,
obj_in: NotificationCreate
) -> Dict[str, Any]:
"""
创建消息通知
Args:
db: 数据库会话
obj_in: 创建数据
Returns:
创建的通知信息
"""
# 获取接收人信息
user_result = await db.execute(
select(User).where(User.id == obj_in.recipient_id)
)
user = user_result.scalar_one_or_none()
if not user:
raise ValueError("接收人不存在")
# 转换为字典
obj_in_data = obj_in.model_dump()
obj_in_data["recipient_name"] = user.real_name
# 处理复杂类型
if obj_in_data.get("extra_data"):
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
# 设置邮件和短信发送标记
obj_in_data["sent_via_email"] = obj_in_data.pop("send_email", False)
obj_in_data["sent_via_sms"] = obj_in_data.pop("send_sms", False)
notification = await notification_crud.create(db, obj_in=obj_in_data)
# TODO: 发送邮件和短信
# if notification.sent_via_email:
# await self._send_email(notification)
# if notification.sent_via_sms:
# await self._send_sms(notification)
return {
"id": notification.id,
"recipient_id": notification.recipient_id,
"title": notification.title,
}
async def batch_create_notifications(
self,
db: AsyncSession,
batch_in: NotificationBatchCreate
) -> Dict[str, Any]:
"""
批量创建消息通知
Args:
db: 数据库会话
batch_in: 批量创建数据
Returns:
创建结果
"""
# 获取接收人信息
user_results = await db.execute(
select(User).where(User.id.in_(batch_in.recipient_ids))
)
users = {user.id: user.real_name for user in user_results.scalars()}
# 准备通知数据
notification_data = {
"title": batch_in.title,
"content": batch_in.content,
"notification_type": batch_in.notification_type.value,
"priority": batch_in.priority.value,
"action_url": batch_in.action_url,
"extra_data": json.loads(batch_in.extra_data.model_dump_json()) if batch_in.extra_data else {},
}
# 批量创建
notifications = await notification_crud.batch_create(
db,
recipient_ids=batch_in.recipient_ids,
notification_data=notification_data
)
# 更新接收人姓名
for notification in notifications:
notification.recipient_name = users.get(notification.recipient_id, "")
await db.flush()
return {
"count": len(notifications),
"notification_ids": [n.id for n in notifications]
}
async def mark_as_read(
self,
db: AsyncSession,
notification_id: int
) -> Dict[str, Any]:
"""
标记为已读
Args:
db: 数据库会话
notification_id: 通知ID
Returns:
更新结果
"""
notification = await notification_crud.mark_as_read(
db,
notification_id=notification_id,
read_at=datetime.utcnow()
)
if not notification:
raise ValueError("通知不存在")
return {
"id": notification.id,
"is_read": notification.is_read,
"read_at": notification.read_at
}
async def mark_all_as_read(
self,
db: AsyncSession,
recipient_id: int
) -> Dict[str, Any]:
"""
标记所有未读为已读
Args:
db: 数据库会话
recipient_id: 接收人ID
Returns:
更新结果
"""
count = await notification_crud.mark_all_as_read(
db,
recipient_id=recipient_id,
read_at=datetime.utcnow()
)
return {
"count": count,
"message": f"已标记 {count} 条通知为已读"
}
async def delete_notification(self, db: AsyncSession, notification_id: int) -> None:
"""
删除消息通知
Args:
db: 数据库会话
notification_id: 通知ID
"""
await notification_crud.delete(db, notification_id=notification_id)
async def batch_delete_notifications(
self,
db: AsyncSession,
notification_ids: List[int]
) -> Dict[str, Any]:
"""
批量删除通知
Args:
db: 数据库会话
notification_ids: 通知ID列表
Returns:
删除结果
"""
count = await notification_crud.batch_delete(db, notification_ids=notification_ids)
return {
"count": count,
"message": f"已删除 {count} 条通知"
}
async def get_unread_count(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
"""
获取未读通知数量
Args:
db: 数据库会话
recipient_id: 接收人ID
Returns:
未读数量
"""
count = await notification_crud.get_unread_count(db, recipient_id)
return {"unread_count": count}
async def get_statistics(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
"""
获取通知统计信息
Args:
db: 数据库会话
recipient_id: 接收人ID
Returns:
统计信息
"""
return await notification_crud.get_statistics(db, recipient_id)
async def send_from_template(
self,
db: AsyncSession,
template_in: NotificationSendFromTemplate
) -> Dict[str, Any]:
"""
从模板发送通知
Args:
db: 数据库会话
template_in: 模板发送数据
Returns:
发送结果
"""
# 获取模板
result = await db.execute(
select(NotificationTemplate).where(
and_(
NotificationTemplate.template_code == template_in.template_code,
NotificationTemplate.is_active == True
)
)
)
template = result.scalar_one_or_none()
if not template:
raise ValueError(f"通知模板 {template_in.template_code} 不存在或未启用")
# 渲染标题和内容
title = self._render_template(template.title_template, template_in.variables)
content = self._render_template(template.content_template, template_in.variables)
# 创建批量通知数据
batch_data = NotificationBatchCreate(
recipient_ids=template_in.recipient_ids,
title=title,
content=content,
notification_type=template.notification_type,
priority=template.priority,
action_url=template_in.action_url,
extra_data={
"template_code": template.template_code,
"variables": template_in.variables
}
)
return await self.batch_create_notifications(db, batch_data)
def _render_template(self, template: str, variables: Dict[str, Any]) -> str:
"""
渲染模板
Args:
template: 模板字符串
variables: 变量字典
Returns:
渲染后的字符串
"""
try:
return template.format(**variables)
except KeyError as e:
raise ValueError(f"模板变量缺失: {e}")
# 创建全局实例
notification_service = NotificationService()

View File

@@ -0,0 +1,270 @@
"""
操作日志服务层
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.crud.operation_log import operation_log_crud
from app.schemas.operation_log import OperationLogCreate
class OperationLogService:
"""操作日志服务类"""
async def get_log(self, db: AsyncSession, log_id: int) -> Optional[Dict[str, Any]]:
"""
获取操作日志详情
Args:
db: 数据库会话
log_id: 日志ID
Returns:
日志信息
"""
log = await operation_log_crud.get(db, log_id)
if not log:
return None
return {
"id": log.id,
"operator_id": log.operator_id,
"operator_name": log.operator_name,
"operator_ip": log.operator_ip,
"module": log.module,
"operation_type": log.operation_type,
"method": log.method,
"url": log.url,
"params": log.params,
"result": log.result,
"error_msg": log.error_msg,
"duration": log.duration,
"user_agent": log.user_agent,
"extra_data": log.extra_data,
"created_at": log.created_at,
}
async def get_logs(
self,
db: AsyncSession,
*,
skip: int = 0,
limit: int = 20,
operator_id: Optional[int] = None,
operator_name: Optional[str] = None,
module: Optional[str] = None,
operation_type: Optional[str] = None,
result: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
keyword: Optional[str] = None
) -> Dict[str, Any]:
"""
获取操作日志列表
Args:
db: 数据库会话
skip: 跳过条数
limit: 返回条数
operator_id: 操作人ID
operator_name: 操作人姓名
module: 模块名称
operation_type: 操作类型
result: 操作结果
start_time: 开始时间
end_time: 结束时间
keyword: 关键词
Returns:
日志列表和总数
"""
items, total = await operation_log_crud.get_multi(
db,
skip=skip,
limit=limit,
operator_id=operator_id,
operator_name=operator_name,
module=module,
operation_type=operation_type,
result=result,
start_time=start_time,
end_time=end_time,
keyword=keyword
)
return {
"items": [
{
"id": item.id,
"operator_id": item.operator_id,
"operator_name": item.operator_name,
"operator_ip": item.operator_ip,
"module": item.module,
"operation_type": item.operation_type,
"method": item.method,
"url": item.url,
"result": item.result,
"error_msg": item.error_msg,
"duration": item.duration,
"created_at": item.created_at,
}
for item in items
],
"total": total
}
async def create_log(
self,
db: AsyncSession,
obj_in: OperationLogCreate
) -> Dict[str, Any]:
"""
创建操作日志
Args:
db: 数据库会话
obj_in: 创建数据
Returns:
创建的日志信息
"""
import json
# 转换为字典
obj_in_data = obj_in.model_dump()
# 处理复杂类型
if obj_in_data.get("extra_data"):
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
log = await operation_log_crud.create(db, obj_in=obj_in_data)
return {
"id": log.id,
"operator_name": log.operator_name,
"module": log.module,
"operation_type": log.operation_type,
}
async def get_statistics(
self,
db: AsyncSession,
*,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None
) -> Dict[str, Any]:
"""
获取操作日志统计信息
Args:
db: 数据库会话
start_time: 开始时间
end_time: 结束时间
Returns:
统计信息
"""
return await operation_log_crud.get_statistics(
db,
start_time=start_time,
end_time=end_time
)
async def get_operator_top(
self,
db: AsyncSession,
*,
limit: int = 10,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None
) -> List[Dict[str, Any]]:
"""
获取操作排行榜
Args:
db: 数据库会话
limit: 返回条数
start_time: 开始时间
end_time: 结束时间
Returns:
操作排行列表
"""
return await operation_log_crud.get_operator_top(
db,
limit=limit,
start_time=start_time,
end_time=end_time
)
async def delete_old_logs(self, db: AsyncSession, *, days: int = 90) -> Dict[str, Any]:
"""
删除旧日志
Args:
db: 数据库会话
days: 保留天数
Returns:
删除结果
"""
count = await operation_log_crud.delete_old_logs(db, days=days)
return {
"deleted_count": count,
"message": f"已删除 {count}{days} 天前的日志"
}
async def export_logs(
self,
db: AsyncSession,
*,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
operator_id: Optional[int] = None,
module: Optional[str] = None,
operation_type: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
导出操作日志
Args:
db: 数据库会话
start_time: 开始时间
end_time: 结束时间
operator_id: 操作人ID
module: 模块名称
operation_type: 操作类型
Returns:
日志列表
"""
items, total = await operation_log_crud.get_multi(
db,
skip=0,
limit=10000, # 导出限制
operator_id=operator_id,
module=module,
operation_type=operation_type,
start_time=start_time,
end_time=end_time
)
return [
{
"操作人": item.operator_name,
"模块": item.module,
"操作类型": item.operation_type,
"请求方法": item.method,
"请求URL": item.url,
"操作结果": item.result,
"错误信息": item.error_msg or "",
"执行时长(毫秒)": item.duration or 0,
"操作时间": item.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"操作IP": item.operator_ip or "",
}
for item in items
]
# 创建全局实例
operation_log_service = OperationLogService()

View File

@@ -0,0 +1,245 @@
"""
机构网点业务服务层
"""
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from app.crud.organization import organization
from app.schemas.organization import OrganizationCreate, OrganizationUpdate
from app.core.exceptions import NotFoundException, AlreadyExistsException
class OrganizationService:
"""机构网点服务类"""
def get_organization(self, db: Session, org_id: int):
"""
获取机构详情
Args:
db: 数据库会话
org_id: 机构ID
Returns:
机构对象
Raises:
NotFoundException: 机构不存在
"""
obj = organization.get(db, org_id)
if not obj:
raise NotFoundException("机构")
return obj
def get_organizations(
self,
db: Session,
skip: int = 0,
limit: int = 20,
org_type: Optional[str] = None,
status: Optional[str] = None,
keyword: Optional[str] = None
) -> Tuple[List, int]:
"""
获取机构列表
Args:
db: 数据库会话
skip: 跳过条数
limit: 返回条数
org_type: 机构类型
status: 状态
keyword: 搜索关键词
Returns:
(机构列表, 总数)
"""
return organization.get_multi(
db=db,
skip=skip,
limit=limit,
org_type=org_type,
status=status,
keyword=keyword
)
def get_organization_tree(
self,
db: Session,
status: Optional[str] = None
) -> List:
"""
获取机构树
Args:
db: 数据库会话
status: 状态筛选
Returns:
机构树列表
"""
return organization.get_tree(db, status)
def get_organization_children(
self,
db: Session,
parent_id: int
) -> List:
"""
获取直接子机构
Args:
db: 数据库会话
parent_id: 父机构ID
Returns:
子机构列表
Raises:
NotFoundException: 父机构不存在
"""
if parent_id > 0 and not organization.get(db, parent_id):
raise NotFoundException("父机构")
return organization.get_children(db, parent_id)
def get_all_children(
self,
db: Session,
parent_id: int
) -> List:
"""
递归获取所有子机构
Args:
db: 数据库会话
parent_id: 父机构ID
Returns:
所有子机构列表
Raises:
NotFoundException: 父机构不存在
"""
if not organization.get(db, parent_id):
raise NotFoundException("机构")
return organization.get_all_children(db, parent_id)
def get_parents(
self,
db: Session,
child_id: int
) -> List:
"""
递归获取所有父机构
Args:
db: 数据库会话
child_id: 子机构ID
Returns:
所有父机构列表
Raises:
NotFoundException: 机构不存在
"""
if not organization.get(db, child_id):
raise NotFoundException("机构")
return organization.get_parents(db, child_id)
def create_organization(
self,
db: Session,
obj_in: OrganizationCreate,
creator_id: Optional[int] = None
):
"""
创建机构
Args:
db: 数据库会话
obj_in: 创建数据
creator_id: 创建人ID
Returns:
创建的机构对象
Raises:
AlreadyExistsException: 机构代码已存在
NotFoundException: 父机构不存在
"""
try:
return organization.create(db, obj_in, creator_id)
except ValueError as e:
if "不存在" in str(e):
raise NotFoundException("父机构") from e
raise AlreadyExistsException("机构") from e
def update_organization(
self,
db: Session,
org_id: int,
obj_in: OrganizationUpdate,
updater_id: Optional[int] = None
):
"""
更新机构
Args:
db: 数据库会话
org_id: 机构ID
obj_in: 更新数据
updater_id: 更新人ID
Returns:
更新后的机构对象
Raises:
NotFoundException: 机构不存在
"""
db_obj = organization.get(db, org_id)
if not db_obj:
raise NotFoundException("机构")
try:
return organization.update(db, db_obj, obj_in, updater_id)
except ValueError as e:
if "不存在" in str(e):
raise NotFoundException("父机构") from e
raise
def delete_organization(
self,
db: Session,
org_id: int,
deleter_id: Optional[int] = None
) -> bool:
"""
删除机构
Args:
db: 数据库会话
org_id: 机构ID
deleter_id: 删除人ID
Returns:
是否删除成功
Raises:
NotFoundException: 机构不存在
ValueError: 机构下存在子机构
"""
if not organization.get(db, org_id):
raise NotFoundException("机构")
try:
return organization.delete(db, org_id, deleter_id)
except ValueError as e:
if "子机构" in str(e):
raise ValueError("该机构下存在子机构,无法删除") from e
raise
# 创建全局实例
organization_service = OrganizationService()

View File

@@ -0,0 +1,409 @@
"""
资产回收业务服务层
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session, selectinload
from app.crud.recovery import recovery_order, recovery_item
from app.crud.asset import asset
from app.schemas.recovery import (
AssetRecoveryOrderCreate,
AssetRecoveryOrderUpdate
)
from app.core.exceptions import NotFoundException, BusinessException
class RecoveryService:
"""资产回收服务类"""
async def get_order(
self,
db: Session,
order_id: int
) -> Dict[str, Any]:
"""获取回收单详情"""
# 使用selectinload预加载关联数据避免N+1查询
from app.models.recovery import AssetRecoveryOrder
from app.models.user import User
from app.models.recovery import AssetRecoveryItem
obj = db.query(
AssetRecoveryOrder
).options(
selectinload(AssetRecoveryOrder.items),
selectinload(AssetRecoveryOrder.applicant.of_type(User)),
selectinload(AssetRecoveryOrder.approver.of_type(User)),
selectinload(AssetRecoveryOrder.executor.of_type(User))
).filter(
AssetRecoveryOrder.id == order_id
).first()
if not obj:
raise NotFoundException("回收单")
# 加载关联信息
return self._load_order_relations(db, obj)
def get_orders(
self,
db: Session,
skip: int = 0,
limit: int = 20,
recovery_type: Optional[str] = None,
approval_status: Optional[str] = None,
execute_status: Optional[str] = None,
keyword: Optional[str] = None
) -> tuple:
"""获取回收单列表"""
items, total = recovery_order.get_multi(
db=db,
skip=skip,
limit=limit,
recovery_type=recovery_type,
approval_status=approval_status,
execute_status=execute_status,
keyword=keyword
)
# 加载关联信息
items_with_relations = [self._load_order_relations(db, item) for item in items]
return items_with_relations, total
async def create_order(
self,
db: Session,
obj_in: AssetRecoveryOrderCreate,
apply_user_id: int
):
"""创建回收单"""
# 验证资产存在性和状态
assets = []
for asset_id in obj_in.asset_ids:
asset_obj = asset.get(db, asset_id)
if not asset_obj:
raise NotFoundException(f"资产ID {asset_id}")
assets.append(asset_obj)
# 验证资产状态是否允许回收
for asset_obj in assets:
if not self._can_recover(asset_obj.status, obj_in.recovery_type):
raise BusinessException(
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许{self._get_recovery_type_name(obj_in.recovery_type)}操作"
)
# 生成回收单号
order_code = await self._generate_order_code(db)
# 创建回收单
db_obj = recovery_order.create(
db=db,
obj_in=obj_in,
order_code=order_code,
apply_user_id=apply_user_id
)
return self._load_order_relations(db, db_obj)
def update_order(
self,
db: Session,
order_id: int,
obj_in: AssetRecoveryOrderUpdate
):
"""更新回收单"""
db_obj = recovery_order.get(db, order_id)
if not db_obj:
raise NotFoundException("回收单")
# 只有待审批状态可以更新
if db_obj.approval_status != "pending":
raise BusinessException("只有待审批状态的回收单可以更新")
return recovery_order.update(db, db_obj, obj_in)
def approve_order(
self,
db: Session,
order_id: int,
approval_status: str,
approval_user_id: int,
approval_remark: Optional[str] = None
):
"""审批回收单"""
db_obj = recovery_order.get(db, order_id)
if not db_obj:
raise NotFoundException("回收单")
# 检查状态
if db_obj.approval_status != "pending":
raise BusinessException("该回收单已审批,无法重复审批")
# 审批
db_obj = recovery_order.approve(
db=db,
db_obj=db_obj,
approval_status=approval_status,
approval_user_id=approval_user_id,
approval_remark=approval_remark
)
return self._load_order_relations(db, db_obj)
def start_order(
self,
db: Session,
order_id: int,
execute_user_id: int
):
"""开始回收"""
db_obj = recovery_order.get(db, order_id)
if not db_obj:
raise NotFoundException("回收单")
# 检查状态
if db_obj.approval_status != "approved":
raise BusinessException("该回收单未审批通过,无法开始执行")
if db_obj.execute_status != "pending":
raise BusinessException("该回收单已开始或已完成")
# 开始回收
db_obj = recovery_order.start(db, db_obj, execute_user_id)
# 更新明细状态为回收中
recovery_item.batch_update_recovery_status(db, order_id, "recovering")
return self._load_order_relations(db, db_obj)
async def complete_order(
self,
db: Session,
order_id: int,
execute_user_id: int
):
"""完成回收"""
db_obj = recovery_order.get(db, order_id)
if not db_obj:
raise NotFoundException("回收单")
# 检查状态
if db_obj.execute_status not in ["pending", "executing"]:
raise BusinessException("该回收单状态不允许完成操作")
# 完成回收单
db_obj = recovery_order.complete(db, db_obj, execute_user_id)
# 更新资产状态
await self._execute_recovery_logic(db, db_obj)
# 更新明细状态为完成
recovery_item.batch_update_recovery_status(db, order_id, "completed")
return self._load_order_relations(db, db_obj)
def cancel_order(
self,
db: Session,
order_id: int
) -> bool:
"""取消回收单"""
db_obj = recovery_order.get(db, order_id)
if not db_obj:
raise NotFoundException("回收单")
# 检查状态
if db_obj.execute_status == "completed":
raise BusinessException("已完成的回收单无法取消")
recovery_order.cancel(db, db_obj)
return True
def delete_order(
self,
db: Session,
order_id: int
) -> bool:
"""删除回收单"""
db_obj = recovery_order.get(db, order_id)
if not db_obj:
raise NotFoundException("回收单")
# 只有已取消或已拒绝的可以删除
if db_obj.approval_status not in ["rejected", "cancelled"]:
raise BusinessException("只能删除已拒绝或已取消的回收单")
return recovery_order.delete(db, order_id)
def get_order_items(
self,
db: Session,
order_id: int
) -> List:
"""获取回收单明细"""
# 验证回收单存在
if not recovery_order.get(db, order_id):
raise NotFoundException("回收单")
return recovery_item.get_by_order(db, order_id)
def get_statistics(
self,
db: Session
) -> Dict[str, int]:
"""获取回收单统计信息"""
return recovery_order.get_statistics(db)
async def _execute_recovery_logic(
self,
db: Session,
order_obj
):
"""执行回收逻辑(完成回收时自动执行)"""
# 获取明细
items = recovery_item.get_by_order(db, order_obj.id)
# 更新资产状态
from app.services.asset_service import asset_service
from app.schemas.asset import AssetStatusTransition
for item in items:
try:
# 根据回收类型确定目标状态
if order_obj.recovery_type == "scrap":
target_status = "scrapped"
remark = f"报废回收: {order_obj.order_code}"
else:
target_status = "in_stock"
remark = f"资产回收: {order_obj.order_code}"
# 变更资产状态
await asset_service.change_asset_status(
db=db,
asset_id=item.asset_id,
status_transition=AssetStatusTransition(
new_status=target_status,
remark=remark
),
operator_id=order_obj.execute_user_id
)
except Exception as e:
# 记录失败日志
print(f"回收资产 {item.asset_code} 失败: {str(e)}")
raise
def _load_order_relations(
self,
db: Session,
obj
) -> Dict[str, Any]:
"""加载回收单关联信息"""
from app.models.user import User
result = {
"id": obj.id,
"order_code": obj.order_code,
"recovery_type": obj.recovery_type,
"title": obj.title,
"asset_count": obj.asset_count,
"apply_user_id": obj.apply_user_id,
"apply_time": obj.apply_time,
"approval_status": obj.approval_status,
"approval_user_id": obj.approval_user_id,
"approval_time": obj.approval_time,
"approval_remark": obj.approval_remark,
"execute_status": obj.execute_status,
"execute_user_id": obj.execute_user_id,
"execute_time": obj.execute_time,
"remark": obj.remark,
"created_at": obj.created_at,
"updated_at": obj.updated_at
}
# 加载申请人
if obj.apply_user_id:
apply_user = db.query(User).filter(User.id == obj.apply_user_id).first()
if apply_user:
result["apply_user"] = {
"id": apply_user.id,
"real_name": apply_user.real_name,
"username": apply_user.username
}
# 加载审批人
if obj.approval_user_id:
approval_user = db.query(User).filter(User.id == obj.approval_user_id).first()
if approval_user:
result["approval_user"] = {
"id": approval_user.id,
"real_name": approval_user.real_name,
"username": approval_user.username
}
# 加载执行人
if obj.execute_user_id:
execute_user = db.query(User).filter(User.id == obj.execute_user_id).first()
if execute_user:
result["execute_user"] = {
"id": execute_user.id,
"real_name": execute_user.real_name,
"username": execute_user.username
}
# 加载明细
items = recovery_item.get_by_order(db, obj.id)
result["items"] = [
{
"id": item.id,
"asset_id": item.asset_id,
"asset_code": item.asset_code,
"recovery_status": item.recovery_status
}
for item in items
]
return result
def _can_recover(self, asset_status: str, recovery_type: str) -> bool:
"""判断资产是否可以回收"""
# 使用中的资产可以回收
if recovery_type in ["user", "org"]:
return asset_status == "in_use"
# 报废回收可以使用中或维修中的资产
elif recovery_type == "scrap":
return asset_status in ["in_use", "maintenance", "in_stock"]
return False
def _get_recovery_type_name(self, recovery_type: str) -> str:
"""获取回收类型中文名"""
type_names = {
"user": "使用人回收",
"org": "机构回收",
"scrap": "报废回收"
}
return type_names.get(recovery_type, "回收")
async def _generate_order_code(self, db: Session) -> str:
"""生成回收单号"""
from datetime import datetime
import random
import string
# 日期部分
date_str = datetime.now().strftime("%Y%m%d")
# 序号部分5位随机数
sequence = "".join(random.choices(string.digits, k=5))
# 组合单号: RO-20250124-00001
order_code = f"RO-{date_str}-{sequence}"
# 检查是否重复,如果重复则重新生成
while recovery_order.get_by_code(db, order_code):
sequence = "".join(random.choices(string.digits, k=5))
order_code = f"RO-{date_str}-{sequence}"
return order_code
# 创建全局实例
recovery_service = RecoveryService()

View File

@@ -0,0 +1,166 @@
"""
资产状态机服务
定义资产状态的转换规则和验证
"""
from typing import Dict, List, Optional
from enum import Enum
class AssetStatus(str, Enum):
"""资产状态枚举"""
PENDING = "pending" # 待入库
IN_STOCK = "in_stock" # 库存中
IN_USE = "in_use" # 使用中
TRANSFERRING = "transferring" # 调拨中
MAINTENANCE = "maintenance" # 维修中
PENDING_SCRAP = "pending_scrap" # 待报废
SCRAPPED = "scrapped" # 已报废
LOST = "lost" # 已丢失
class StateMachineService:
"""状态机服务类"""
# 状态转换规则
TRANSITIONS: Dict[str, List[str]] = {
AssetStatus.PENDING: [
AssetStatus.IN_STOCK,
AssetStatus.PENDING_SCRAP,
],
AssetStatus.IN_STOCK: [
AssetStatus.IN_USE,
AssetStatus.TRANSFERRING,
AssetStatus.MAINTENANCE,
AssetStatus.PENDING_SCRAP,
AssetStatus.LOST,
],
AssetStatus.IN_USE: [
AssetStatus.IN_STOCK,
AssetStatus.TRANSFERRING,
AssetStatus.MAINTENANCE,
AssetStatus.PENDING_SCRAP,
AssetStatus.LOST,
],
AssetStatus.TRANSFERRING: [
AssetStatus.IN_STOCK,
AssetStatus.IN_USE,
],
AssetStatus.MAINTENANCE: [
AssetStatus.IN_STOCK,
AssetStatus.IN_USE,
AssetStatus.PENDING_SCRAP,
],
AssetStatus.PENDING_SCRAP: [
AssetStatus.SCRAPPED,
AssetStatus.IN_STOCK, # 取消报废
],
AssetStatus.SCRAPPED: [], # 终态,不可转换
AssetStatus.LOST: [], # 终态,不可转换
}
# 状态显示名称
STATUS_NAMES: Dict[str, str] = {
AssetStatus.PENDING: "待入库",
AssetStatus.IN_STOCK: "库存中",
AssetStatus.IN_USE: "使用中",
AssetStatus.TRANSFERRING: "调拨中",
AssetStatus.MAINTENANCE: "维修中",
AssetStatus.PENDING_SCRAP: "待报废",
AssetStatus.SCRAPPED: "已报废",
AssetStatus.LOST: "已丢失",
}
def can_transition(self, current_status: str, target_status: str) -> bool:
"""
检查状态是否可以转换
Args:
current_status: 当前状态
target_status: 目标状态
Returns:
是否可以转换
"""
allowed_transitions = self.TRANSITIONS.get(current_status, [])
return target_status in allowed_transitions
def validate_transition(
self,
current_status: str,
target_status: str
) -> Optional[str]:
"""
验证状态转换并返回错误信息
Args:
current_status: 当前状态
target_status: 目标状态
Returns:
错误信息如果转换有效则返回None
"""
if current_status == target_status:
return "当前状态与目标状态相同"
if current_status not in self.TRANSITIONS:
return f"无效的当前状态: {current_status}"
if target_status not in self.TRANSITIONS:
return f"无效的目标状态: {target_status}"
if not self.can_transition(current_status, target_status):
return f"无法从状态 '{self.get_status_name(current_status)}' 转换到 '{self.get_status_name(target_status)}'"
return None
def get_status_name(self, status: str) -> str:
"""
获取状态的显示名称
Args:
status: 状态值
Returns:
状态显示名称
"""
return self.STATUS_NAMES.get(status, status)
def get_allowed_transitions(self, current_status: str) -> List[str]:
"""
获取允许的转换状态列表
Args:
current_status: 当前状态
Returns:
允许转换到的状态列表
"""
return self.TRANSITIONS.get(current_status, [])
def is_terminal_state(self, status: str) -> bool:
"""
判断是否为终态
Args:
status: 状态值
Returns:
是否为终态
"""
return len(self.TRANSITIONS.get(status, [])) == 0
def get_available_statuses(self) -> List[Dict[str, str]]:
"""
获取所有可用状态列表
Returns:
状态列表每个状态包含value和name
"""
return [
{"value": status, "name": name}
for status, name in self.STATUS_NAMES.items()
]
# 创建全局实例
state_machine_service = StateMachineService()

View File

@@ -0,0 +1,546 @@
"""
统计分析服务层
"""
from typing import Optional, List, Dict, Any
from datetime import datetime, date, timedelta
from decimal import Decimal
from sqlalchemy import select, func, and_, or_, case, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.asset import Asset
from app.models.allocation import AssetAllocationOrder
from app.models.maintenance import MaintenanceRecord
from app.models.organization import Organization
from app.models.brand_supplier import Supplier
from app.models.device_type import DeviceType
class StatisticsService:
"""统计分析服务类"""
async def get_overview(
self,
db: AsyncSession,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取总览统计
Args:
db: 数据库会话
organization_id: 网点ID
Returns:
总览统计数据
"""
# 构建查询条件
conditions = []
if organization_id:
conditions.append(Asset.organization_id == organization_id)
where_clause = and_(*conditions) if conditions else None
# 资产总数
total_query = select(func.count(Asset.id))
if where_clause:
total_query = total_query.where(where_clause)
total_result = await db.execute(total_query)
total_assets = total_result.scalar() or 0
# 资产总价值
value_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
if where_clause:
value_query = value_query.where(where_clause)
value_result = await db.execute(value_query)
total_value = value_result.scalar() or Decimal("0")
# 各状态数量
status_query = select(
Asset.status,
func.count(Asset.id).label('count')
).group_by(Asset.status)
if where_clause:
status_query = status_query.where(where_clause)
status_result = await db.execute(status_query)
status_counts = {row[0]: row[1] for row in status_result}
# 今日和本月采购数量
today = datetime.utcnow().date()
today_start = datetime.combine(today, datetime.min.time())
month_start = datetime(today.year, today.month, 1)
today_query = select(func.count(Asset.id)).where(Asset.created_at >= today_start)
if where_clause:
today_query = today_query.where(Asset.organization_id == organization_id)
today_result = await db.execute(today_query)
today_purchase_count = today_result.scalar() or 0
month_query = select(func.count(Asset.id)).where(Asset.created_at >= month_start)
if where_clause:
month_query = month_query.where(Asset.organization_id == organization_id)
month_result = await db.execute(month_query)
this_month_purchase_count = month_result.scalar() or 0
# 机构网点数
org_query = select(func.count(Organization.id))
org_result = await db.execute(org_query)
organization_count = org_result.scalar() or 0
# 供应商数
supplier_query = select(func.count(Supplier.id))
supplier_result = await db.execute(supplier_query)
supplier_count = supplier_result.scalar() or 0
return {
"total_assets": total_assets,
"total_value": float(total_value),
"in_stock_count": status_counts.get("in_stock", 0),
"in_use_count": status_counts.get("in_use", 0),
"maintenance_count": status_counts.get("maintenance", 0),
"scrapped_count": status_counts.get("scrapped", 0) + status_counts.get("pending_scrap", 0),
"today_purchase_count": today_purchase_count,
"this_month_purchase_count": this_month_purchase_count,
"organization_count": organization_count,
"supplier_count": supplier_count,
}
async def get_purchase_statistics(
self,
db: AsyncSession,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取采购统计
Args:
db: 数据库会话
start_date: 开始日期
end_date: 结束日期
organization_id: 网点ID
Returns:
采购统计数据
"""
# 构建查询条件
conditions = []
if start_date:
conditions.append(Asset.purchase_date >= start_date)
if end_date:
conditions.append(Asset.purchase_date <= end_date)
if organization_id:
conditions.append(Asset.organization_id == organization_id)
where_clause = and_(*conditions) if conditions else None
# 总采购数量和金额
count_query = select(func.count(Asset.id))
value_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
if where_clause:
count_query = count_query.where(where_clause)
value_query = value_query.where(where_clause)
count_result = await db.execute(count_query)
value_result = await db.execute(value_query)
total_purchase_count = count_result.scalar() or 0
total_purchase_value = value_result.scalar() or Decimal("0")
# 月度趋势
monthly_query = select(
func.to_char(Asset.purchase_date, 'YYYY-MM').label('month'),
func.count(Asset.id).label('count'),
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
).group_by('month').order_by('month')
if where_clause:
monthly_query = monthly_query.where(where_clause)
monthly_result = await db.execute(monthly_query)
monthly_trend = [
{
"month": row[0],
"count": row[1],
"value": float(row[2]) if row[2] else 0
}
for row in monthly_result
]
# 供应商分布
supplier_query = select(
Supplier.id.label('supplier_id'),
Supplier.name.label('supplier_name'),
func.count(Asset.id).label('count'),
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
).join(
Asset, Asset.supplier_id == Supplier.id
).group_by(
Supplier.id, Supplier.name
).order_by(func.count(Asset.id).desc())
if where_clause:
supplier_query = supplier_query.where(
and_(*[c for c in conditions if not any(x in str(c) for x in ['organization_id'])])
)
supplier_result = await db.execute(supplier_query)
supplier_distribution = [
{
"supplier_id": row[0],
"supplier_name": row[1],
"count": row[2],
"value": float(row[3]) if row[3] else 0
}
for row in supplier_result
]
return {
"total_purchase_count": total_purchase_count,
"total_purchase_value": float(total_purchase_value),
"monthly_trend": monthly_trend,
"supplier_distribution": supplier_distribution,
"category_distribution": [],
}
async def get_depreciation_statistics(
self,
db: AsyncSession,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取折旧统计
Args:
db: 数据库会话
organization_id: 网点ID
Returns:
折旧统计数据
"""
# 简化实现,实际需要根据折旧规则计算
return {
"total_depreciation_value": 0.0,
"average_depreciation_rate": 0.05,
"depreciation_by_category": [],
"assets_near_end_life": [],
}
async def get_value_statistics(
self,
db: AsyncSession,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取价值统计
Args:
db: 数据库会话
organization_id: 网点ID
Returns:
价值统计数据
"""
# 构建查询条件
conditions = []
if organization_id:
conditions.append(Asset.organization_id == organization_id)
where_clause = and_(*conditions) if conditions else None
# 总价值
total_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
if where_clause:
total_query = total_query.where(where_clause)
total_result = await db.execute(total_query)
total_value = total_result.scalar() or Decimal("0")
# 按分类统计
category_query = select(
DeviceType.id.label('device_type_id'),
DeviceType.name.label('device_type_name'),
func.count(Asset.id).label('count'),
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
).join(
Asset, Asset.device_type_id == DeviceType.id
).group_by(
DeviceType.id, DeviceType.name
).order_by(func.coalesce(func.sum(Asset.purchase_price), 0).desc())
if where_clause:
category_query = category_query.where(where_clause)
category_result = await db.execute(category_query)
value_by_category = [
{
"device_type_id": row[0],
"device_type_name": row[1],
"count": row[2],
"value": float(row[3]) if row[3] else 0
}
for row in category_result
]
# 按网点统计
org_query = select(
Organization.id.label('organization_id'),
Organization.name.label('organization_name'),
func.count(Asset.id).label('count'),
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
).join(
Asset, Asset.organization_id == Organization.id
).group_by(
Organization.id, Organization.name
).order_by(func.coalesce(func.sum(Asset.purchase_price), 0).desc())
if where_clause:
org_query = org_query.where(where_clause)
org_result = await db.execute(org_query)
value_by_organization = [
{
"organization_id": row[0],
"organization_name": row[1],
"count": row[2],
"value": float(row[3]) if row[3] else 0
}
for row in org_result
]
# 高价值资产价值前10
high_value_query = select(
Asset.id,
Asset.asset_code,
Asset.asset_name,
Asset.purchase_price,
DeviceType.name.label('device_type_name')
).join(
DeviceType, Asset.device_type_id == DeviceType.id
).order_by(
Asset.purchase_price.desc()
).limit(10)
if where_clause:
high_value_query = high_value_query.where(where_clause)
high_value_result = await db.execute(high_value_query)
high_value_assets = [
{
"asset_id": row[0],
"asset_code": row[1],
"asset_name": row[2],
"purchase_price": float(row[3]) if row[3] else 0,
"device_type_name": row[4]
}
for row in high_value_result
]
return {
"total_value": float(total_value),
"net_value": float(total_value * Decimal("0.8")), # 简化计算
"depreciation_value": float(total_value * Decimal("0.2")),
"value_by_category": value_by_category,
"value_by_organization": value_by_organization,
"high_value_assets": high_value_assets,
}
async def get_trend_analysis(
self,
db: AsyncSession,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取趋势分析
Args:
db: 数据库会话
start_date: 开始日期
end_date: 结束日期
organization_id: 网点ID
Returns:
趋势分析数据
"""
# 默认查询最近12个月
if not end_date:
end_date = datetime.utcnow().date()
if not start_date:
start_date = end_date - timedelta(days=365)
# 构建查询条件
conditions = [
Asset.created_at >= datetime.combine(start_date, datetime.min.time()),
Asset.created_at <= datetime.combine(end_date, datetime.max.time())
]
if organization_id:
conditions.append(Asset.organization_id == organization_id)
where_clause = and_(*conditions)
# 资产数量趋势(按月)
asset_trend_query = select(
func.to_char(Asset.created_at, 'YYYY-MM').label('month'),
func.count(Asset.id).label('count')
).group_by('month').order_by('month')
asset_trend_result = await db.execute(asset_trend_query.where(where_clause))
asset_trend = [
{"month": row[0], "count": row[1]}
for row in asset_trend_result
]
# 资产价值趋势
value_trend_query = select(
func.to_char(Asset.created_at, 'YYYY-MM').label('month'),
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
).group_by('month').order_by('month')
value_trend_result = await db.execute(value_trend_query.where(where_clause))
value_trend = [
{"month": row[0], "value": float(row[1]) if row[1] else 0}
for row in value_trend_result
]
return {
"asset_trend": asset_trend,
"value_trend": value_trend,
"purchase_trend": [],
"maintenance_trend": [],
"allocation_trend": [],
}
async def get_maintenance_statistics(
self,
db: AsyncSession,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取维修统计
Args:
db: 数据库会话
start_date: 开始日期
end_date: 结束日期
organization_id: 网点ID
Returns:
维修统计数据
"""
# 构建查询条件
conditions = []
if start_date:
conditions.append(MaintenanceRecord.created_at >= datetime.combine(start_date, datetime.min.time()))
if end_date:
conditions.append(MaintenanceRecord.created_at <= datetime.combine(end_date, datetime.max.time()))
if organization_id:
conditions.append(MaintenanceRecord.organization_id == organization_id)
where_clause = and_(*conditions) if conditions else None
# 总维修次数和费用
count_query = select(func.count(MaintenanceRecord.id))
cost_query = select(func.coalesce(func.sum(MaintenanceRecord.cost), 0))
if where_clause:
count_query = count_query.where(where_clause)
cost_query = cost_query.where(where_clause)
count_result = await db.execute(count_query)
cost_result = await db.execute(cost_query)
total_maintenance_count = count_result.scalar() or 0
total_maintenance_cost = cost_result.scalar() or Decimal("0")
# 按状态统计
status_query = select(
MaintenanceRecord.status,
func.count(MaintenanceRecord.id).label('count')
).group_by(MaintenanceRecord.status)
if where_clause:
status_query = status_query.where(where_clause)
status_result = await db.execute(status_query)
status_counts = {row[0]: row[1] for row in status_result}
return {
"total_maintenance_count": total_maintenance_count,
"total_maintenance_cost": float(total_maintenance_cost),
"pending_count": status_counts.get("pending", 0),
"in_progress_count": status_counts.get("in_progress", 0),
"completed_count": status_counts.get("completed", 0),
"monthly_trend": [],
"type_distribution": [],
"cost_by_category": [],
}
async def get_allocation_statistics(
self,
db: AsyncSession,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
organization_id: Optional[int] = None
) -> Dict[str, Any]:
"""
获取分配统计
Args:
db: 数据库会话
start_date: 开始日期
end_date: 结束日期
organization_id: 网点ID
Returns:
分配统计数据
"""
# 构建查询条件
conditions = []
if start_date:
conditions.append(AssetAllocationOrder.created_at >= datetime.combine(start_date, datetime.min.time()))
if end_date:
conditions.append(AssetAllocationOrder.created_at <= datetime.combine(end_date, datetime.max.time()))
where_clause = and_(*conditions) if conditions else None
# 总分配次数
count_query = select(func.count(AssetAllocationOrder.id))
if where_clause:
count_query = count_query.where(where_clause)
count_result = await db.execute(count_query)
total_allocation_count = count_result.scalar() or 0
# 按状态统计
status_query = select(
AssetAllocationOrder.status,
func.count(AssetAllocationOrder.id).label('count')
).group_by(AssetAllocationOrder.status)
if where_clause:
status_query = status_query.where(where_clause)
status_result = await db.execute(status_query)
status_counts = {row[0]: row[1] for row in status_result}
return {
"total_allocation_count": total_allocation_count,
"pending_count": status_counts.get("pending", 0),
"approved_count": status_counts.get("approved", 0),
"rejected_count": status_counts.get("rejected", 0),
"monthly_trend": [],
"by_organization": [],
"transfer_statistics": [],
}
# 创建全局实例
statistics_service = StatisticsService()

View File

@@ -0,0 +1,298 @@
"""
系统配置服务层
"""
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.crud.system_config import system_config_crud
from app.schemas.system_config import SystemConfigCreate, SystemConfigUpdate
import json
class SystemConfigService:
"""系统配置服务类"""
async def get_config(self, db: AsyncSession, config_id: int) -> Optional[Dict[str, Any]]:
"""
获取配置详情
Args:
db: 数据库会话
config_id: 配置ID
Returns:
配置信息
"""
config = await system_config_crud.get(db, config_id)
if not config:
return None
return {
"id": config.id,
"config_key": config.config_key,
"config_name": config.config_name,
"config_value": config.config_value,
"value_type": config.value_type,
"category": config.category,
"description": config.description,
"is_system": config.is_system,
"is_encrypted": config.is_encrypted,
"validation_rule": config.validation_rule,
"options": config.options,
"default_value": config.default_value,
"sort_order": config.sort_order,
"is_active": config.is_active,
"created_at": config.created_at,
"updated_at": config.updated_at,
"updated_by": config.updated_by,
}
async def get_config_by_key(
self,
db: AsyncSession,
config_key: str,
default: Any = None
) -> Any:
"""
根据键获取配置值
Args:
db: 数据库会话
config_key: 配置键
default: 默认值
Returns:
配置值
"""
return await system_config_crud.get_value(db, config_key, default)
async def get_configs(
self,
db: AsyncSession,
*,
skip: int = 0,
limit: int = 20,
keyword: Optional[str] = None,
category: Optional[str] = None,
is_active: Optional[bool] = None,
is_system: Optional[bool] = None
) -> Dict[str, Any]:
"""
获取配置列表
Args:
db: 数据库会话
skip: 跳过条数
limit: 返回条数
keyword: 搜索关键词
category: 配置分类
is_active: 是否启用
is_system: 是否系统配置
Returns:
配置列表和总数
"""
items, total = await system_config_crud.get_multi(
db,
skip=skip,
limit=limit,
keyword=keyword,
category=category,
is_active=is_active,
is_system=is_system
)
return {
"items": [
{
"id": item.id,
"config_key": item.config_key,
"config_name": item.config_name,
"config_value": item.config_value,
"value_type": item.value_type,
"category": item.category,
"description": item.description,
"is_system": item.is_system,
"is_encrypted": item.is_encrypted,
"options": item.options,
"default_value": item.default_value,
"sort_order": item.sort_order,
"is_active": item.is_active,
"created_at": item.created_at,
"updated_at": item.updated_at,
}
for item in items
],
"total": total
}
async def get_configs_by_category(
self,
db: AsyncSession,
category: str,
is_active: bool = True
) -> List[Dict[str, Any]]:
"""
根据分类获取配置
Args:
db: 数据库会话
category: 配置分类
is_active: 是否启用
Returns:
配置列表
"""
items = await system_config_crud.get_by_category(db, category, is_active=is_active)
return [
{
"config_key": item.config_key,
"config_name": item.config_name,
"config_value": item.config_value,
"value_type": item.value_type,
"description": item.description,
}
for item in items
]
async def get_categories(self, db: AsyncSession) -> List[Dict[str, Any]]:
"""
获取所有配置分类
Args:
db: 数据库会话
Returns:
分类列表
"""
return await system_config_crud.get_categories(db)
async def create_config(
self,
db: AsyncSession,
obj_in: SystemConfigCreate,
creator_id: Optional[int] = None
) -> Dict[str, Any]:
"""
创建配置
Args:
db: 数据库会话
obj_in: 创建数据
creator_id: 创建人ID
Returns:
创建的配置信息
"""
# 检查键是否已存在
existing = await system_config_crud.get_by_key(db, obj_in.config_key)
if existing:
raise ValueError(f"配置键 {obj_in.config_key} 已存在")
# 转换为字典
obj_in_data = obj_in.model_dump()
# 处理复杂类型
if obj_in.options:
obj_in_data["options"] = json.loads(obj_in.options.model_dump_json()) if isinstance(obj_in.options, dict) else obj_in.options
config = await system_config_crud.create(db, obj_in=obj_in_data)
return {
"id": config.id,
"config_key": config.config_key,
"config_name": config.config_name,
"category": config.category,
}
async def update_config(
self,
db: AsyncSession,
config_id: int,
obj_in: SystemConfigUpdate,
updater_id: Optional[int] = None
) -> Dict[str, Any]:
"""
更新配置
Args:
db: 数据库会话
config_id: 配置ID
obj_in: 更新数据
updater_id: 更新人ID
Returns:
更新的配置信息
"""
config = await system_config_crud.get(db, config_id)
if not config:
raise ValueError("配置不存在")
# 系统配置不允许修改某些字段
if config.is_system:
if obj_in.config_key and obj_in.config_key != config.config_key:
raise ValueError("系统配置不允许修改配置键")
if obj_in.value_type and obj_in.value_type != config.value_type:
raise ValueError("系统配置不允许修改值类型")
if obj_in.category and obj_in.category != config.category:
raise ValueError("系统配置不允许修改分类")
# 转换为字典过滤None值
update_data = obj_in.model_dump(exclude_unset=True)
# 处理复杂类型
if update_data.get("options"):
update_data["options"] = json.loads(update_data["options"].model_dump_json()) if isinstance(update_data["options"], dict) else update_data["options"]
update_data["updated_by"] = updater_id
config = await system_config_crud.update(db, db_obj=config, obj_in=update_data)
return {
"id": config.id,
"config_key": config.config_key,
"config_name": config.config_name,
"config_value": config.config_value,
}
async def batch_update_configs(
self,
db: AsyncSession,
configs: Dict[str, Any],
updater_id: Optional[int] = None
) -> Dict[str, Any]:
"""
批量更新配置
Args:
db: 数据库会话
configs: 配置键值对
updater_id: 更新人ID
Returns:
更新结果
"""
updated = await system_config_crud.batch_update(
db,
configs=configs,
updater_id=updater_id
)
return {
"count": len(updated),
"configs": [item.config_key for item in updated]
}
async def delete_config(self, db: AsyncSession, config_id: int) -> None:
"""
删除配置
Args:
db: 数据库会话
config_id: 配置ID
"""
await system_config_crud.delete(db, config_id=config_id)
# 创建全局实例
system_config_service = SystemConfigService()

View File

@@ -0,0 +1,451 @@
"""
资产调拨业务服务层
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session, selectinload
from app.crud.transfer import transfer_order, transfer_item
from app.crud.asset import asset
from app.schemas.transfer import (
AssetTransferOrderCreate,
AssetTransferOrderUpdate
)
from app.core.exceptions import NotFoundException, BusinessException
class TransferService:
"""资产调拨服务类"""
async def get_order(
self,
db: Session,
order_id: int
) -> Dict[str, Any]:
"""获取调拨单详情"""
# 使用selectinload预加载关联数据避免N+1查询
from app.models.transfer import AssetTransferOrder
from app.models.organization import Organization
from app.models.user import User
from app.models.transfer import AssetTransferItem
obj = db.query(
AssetTransferOrder
).options(
selectinload(AssetTransferOrder.items),
selectinload(AssetTransferOrder.source_org.of_type(Organization)),
selectinload(AssetTransferOrder.target_org.of_type(Organization)),
selectinload(AssetTransferOrder.applicant.of_type(User)),
selectinload(AssetTransferOrder.approver.of_type(User)),
selectinload(AssetTransferOrder.executor.of_type(User))
).filter(
AssetTransferOrder.id == order_id
).first()
if not obj:
raise NotFoundException("调拨单")
# 加载关联信息
return self._load_order_relations(db, obj)
def get_orders(
self,
db: Session,
skip: int = 0,
limit: int = 20,
transfer_type: Optional[str] = None,
approval_status: Optional[str] = None,
execute_status: Optional[str] = None,
source_org_id: Optional[int] = None,
target_org_id: Optional[int] = None,
keyword: Optional[str] = None
) -> tuple:
"""获取调拨单列表"""
items, total = transfer_order.get_multi(
db=db,
skip=skip,
limit=limit,
transfer_type=transfer_type,
approval_status=approval_status,
execute_status=execute_status,
source_org_id=source_org_id,
target_org_id=target_org_id,
keyword=keyword
)
# 加载关联信息
items_with_relations = [self._load_order_relations(db, item) for item in items]
return items_with_relations, total
async def create_order(
self,
db: Session,
obj_in: AssetTransferOrderCreate,
apply_user_id: int
):
"""创建调拨单"""
# 验证资产存在性和状态
assets = []
for asset_id in obj_in.asset_ids:
asset_obj = asset.get(db, asset_id)
if not asset_obj:
raise NotFoundException(f"资产ID {asset_id}")
assets.append(asset_obj)
# 验证资产状态是否允许调拨
for asset_obj in assets:
if asset_obj.status not in ["in_stock", "in_use"]:
raise BusinessException(
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许调拨操作"
)
# 验证资产所属机构是否为调出机构
for asset_obj in assets:
if asset_obj.organization_id != obj_in.source_org_id:
raise BusinessException(
f"资产 {asset_obj.asset_code} 所属机构与调出机构不一致"
)
# 生成调拨单号
order_code = await self._generate_order_code(db)
# 创建调拨单
db_obj = transfer_order.create(
db=db,
obj_in=obj_in,
order_code=order_code,
apply_user_id=apply_user_id
)
return self._load_order_relations(db, db_obj)
def update_order(
self,
db: Session,
order_id: int,
obj_in: AssetTransferOrderUpdate
):
"""更新调拨单"""
db_obj = transfer_order.get(db, order_id)
if not db_obj:
raise NotFoundException("调拨单")
# 只有待审批状态可以更新
if db_obj.approval_status != "pending":
raise BusinessException("只有待审批状态的调拨单可以更新")
return transfer_order.update(db, db_obj, obj_in)
def approve_order(
self,
db: Session,
order_id: int,
approval_status: str,
approval_user_id: int,
approval_remark: Optional[str] = None
):
"""审批调拨单"""
db_obj = transfer_order.get(db, order_id)
if not db_obj:
raise NotFoundException("调拨单")
# 检查状态
if db_obj.approval_status != "pending":
raise BusinessException("该调拨单已审批,无法重复审批")
# 审批
db_obj = transfer_order.approve(
db=db,
db_obj=db_obj,
approval_status=approval_status,
approval_user_id=approval_user_id,
approval_remark=approval_remark
)
return self._load_order_relations(db, db_obj)
def start_order(
self,
db: Session,
order_id: int,
execute_user_id: int
):
"""开始调拨"""
db_obj = transfer_order.get(db, order_id)
if not db_obj:
raise NotFoundException("调拨单")
# 检查状态
if db_obj.approval_status != "approved":
raise BusinessException("该调拨单未审批通过,无法开始执行")
if db_obj.execute_status != "pending":
raise BusinessException("该调拨单已开始或已完成")
# 开始调拨
db_obj = transfer_order.start(db, db_obj, execute_user_id)
# 更新明细状态为调拨中
transfer_item.batch_update_transfer_status(db, order_id, "transferring")
return self._load_order_relations(db, db_obj)
async def complete_order(
self,
db: Session,
order_id: int,
execute_user_id: int
):
"""完成调拨"""
db_obj = transfer_order.get(db, order_id)
if not db_obj:
raise NotFoundException("调拨单")
# 检查状态
if db_obj.execute_status not in ["pending", "executing"]:
raise BusinessException("该调拨单状态不允许完成操作")
# 完成调拨单
db_obj = transfer_order.complete(db, db_obj, execute_user_id)
# 更新资产机构和状态
await self._execute_transfer_logic(db, db_obj)
# 更新明细状态为完成
transfer_item.batch_update_transfer_status(db, order_id, "completed")
return self._load_order_relations(db, db_obj)
def cancel_order(
self,
db: Session,
order_id: int
) -> bool:
"""取消调拨单"""
db_obj = transfer_order.get(db, order_id)
if not db_obj:
raise NotFoundException("调拨单")
# 检查状态
if db_obj.execute_status == "completed":
raise BusinessException("已完成的调拨单无法取消")
transfer_order.cancel(db, db_obj)
return True
def delete_order(
self,
db: Session,
order_id: int
) -> bool:
"""删除调拨单"""
db_obj = transfer_order.get(db, order_id)
if not db_obj:
raise NotFoundException("调拨单")
# 只有已取消或已拒绝的可以删除
if db_obj.approval_status not in ["rejected", "cancelled"]:
raise BusinessException("只能删除已拒绝或已取消的调拨单")
return transfer_order.delete(db, order_id)
def get_order_items(
self,
db: Session,
order_id: int
) -> List:
"""获取调拨单明细"""
# 验证调拨单存在
if not transfer_order.get(db, order_id):
raise NotFoundException("调拨单")
return transfer_item.get_by_order(db, order_id)
def get_statistics(
self,
db: Session,
source_org_id: Optional[int] = None,
target_org_id: Optional[int] = None
) -> Dict[str, int]:
"""获取调拨单统计信息"""
return transfer_order.get_statistics(db, source_org_id, target_org_id)
async def _execute_transfer_logic(
self,
db: Session,
order_obj
):
"""执行调拨逻辑(完成调拨时自动执行)"""
# 获取明细
items = transfer_item.get_by_order(db, order_obj.id)
# 更新资产机构和状态
from app.services.asset_service import asset_service
from app.schemas.asset import AssetStatusTransition, AssetUpdate
for item in items:
try:
# 变更资产状态
await asset_service.change_asset_status(
db=db,
asset_id=item.asset_id,
status_transition=AssetStatusTransition(
new_status="transferring",
remark=f"调拨单: {order_obj.order_code},从{item.source_organization_id}{item.target_organization_id}"
),
operator_id=order_obj.execute_user_id
)
# 更新资产所属机构
asset_obj = asset.get(db, item.asset_id)
if asset_obj:
asset.update(
db=db,
db_obj=asset_obj,
obj_in=AssetUpdate(
organization_id=item.target_organization_id
),
updater_id=order_obj.execute_user_id
)
# 最终状态变更
target_status = "in_stock"
await asset_service.change_asset_status(
db=db,
asset_id=item.asset_id,
status_transition=AssetStatusTransition(
new_status=target_status,
remark=f"调拨完成: {order_obj.order_code}"
),
operator_id=order_obj.execute_user_id
)
except Exception as e:
# 记录失败日志
print(f"调拨资产 {item.asset_code} 失败: {str(e)}")
raise
def _load_order_relations(
self,
db: Session,
obj
) -> Dict[str, Any]:
"""加载调拨单关联信息"""
from app.models.user import User
from app.models.organization import Organization
result = {
"id": obj.id,
"order_code": obj.order_code,
"source_org_id": obj.source_org_id,
"target_org_id": obj.target_org_id,
"transfer_type": obj.transfer_type,
"title": obj.title,
"asset_count": obj.asset_count,
"apply_user_id": obj.apply_user_id,
"apply_time": obj.apply_time,
"approval_status": obj.approval_status,
"approval_user_id": obj.approval_user_id,
"approval_time": obj.approval_time,
"approval_remark": obj.approval_remark,
"execute_status": obj.execute_status,
"execute_user_id": obj.execute_user_id,
"execute_time": obj.execute_time,
"remark": obj.remark,
"created_at": obj.created_at,
"updated_at": obj.updated_at
}
# 加载调出机构
if obj.source_org_id:
source_org = db.query(Organization).filter(
Organization.id == obj.source_org_id
).first()
if source_org:
result["source_organization"] = {
"id": source_org.id,
"org_name": source_org.org_name,
"org_type": source_org.org_type
}
# 加载调入机构
if obj.target_org_id:
target_org = db.query(Organization).filter(
Organization.id == obj.target_org_id
).first()
if target_org:
result["target_organization"] = {
"id": target_org.id,
"org_name": target_org.org_name,
"org_type": target_org.org_type
}
# 加载申请人
if obj.apply_user_id:
apply_user = db.query(User).filter(User.id == obj.apply_user_id).first()
if apply_user:
result["apply_user"] = {
"id": apply_user.id,
"real_name": apply_user.real_name,
"username": apply_user.username
}
# 加载审批人
if obj.approval_user_id:
approval_user = db.query(User).filter(User.id == obj.approval_user_id).first()
if approval_user:
result["approval_user"] = {
"id": approval_user.id,
"real_name": approval_user.real_name,
"username": approval_user.username
}
# 加载执行人
if obj.execute_user_id:
execute_user = db.query(User).filter(User.id == obj.execute_user_id).first()
if execute_user:
result["execute_user"] = {
"id": execute_user.id,
"real_name": execute_user.real_name,
"username": execute_user.username
}
# 加载明细
items = transfer_item.get_by_order(db, obj.id)
result["items"] = [
{
"id": item.id,
"asset_id": item.asset_id,
"asset_code": item.asset_code,
"source_organization_id": item.source_organization_id,
"target_organization_id": item.target_organization_id,
"transfer_status": item.transfer_status
}
for item in items
]
return result
async def _generate_order_code(self, db: Session) -> str:
"""生成调拨单号"""
from datetime import datetime
import random
import string
# 日期部分
date_str = datetime.now().strftime("%Y%m%d")
# 序号部分5位随机数
sequence = "".join(random.choices(string.digits, k=5))
# 组合单号: TO-20250124-00001
order_code = f"TO-{date_str}-{sequence}"
# 检查是否重复,如果重复则重新生成
while transfer_order.get_by_code(db, order_code):
sequence = "".join(random.choices(string.digits, k=5))
order_code = f"TO-{date_str}-{sequence}"
return order_code
# 创建全局实例
transfer_service = TransferService()