Fix API compatibility and add user/role/permission and asset import/export
This commit is contained in:
0
backend_new/app/services/__init__.py
Normal file
0
backend_new/app/services/__init__.py
Normal file
469
backend_new/app/services/allocation_service.py
Normal file
469
backend_new/app/services/allocation_service.py
Normal file
@@ -0,0 +1,469 @@
|
||||
"""
|
||||
资产分配业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from app.crud.allocation import allocation_order, allocation_item
|
||||
from app.crud.asset import asset
|
||||
from app.schemas.allocation import (
|
||||
AllocationOrderCreate,
|
||||
AllocationOrderUpdate,
|
||||
AllocationOrderApproval
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, BusinessException
|
||||
|
||||
|
||||
class AllocationService:
|
||||
"""资产分配服务类"""
|
||||
|
||||
async def get_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""获取分配单详情"""
|
||||
# 使用selectinload预加载关联数据,避免N+1查询
|
||||
from app.models.allocation import AllocationOrder
|
||||
from app.models.organization import Organization
|
||||
from app.models.user import User
|
||||
from app.models.allocation import AllocationItem
|
||||
|
||||
obj = db.query(
|
||||
AllocationOrder
|
||||
).options(
|
||||
selectinload(AllocationOrder.items),
|
||||
selectinload(AllocationOrder.source_organization.of_type(Organization)),
|
||||
selectinload(AllocationOrder.target_organization.of_type(Organization)),
|
||||
selectinload(AllocationOrder.applicant.of_type(User)),
|
||||
selectinload(AllocationOrder.approver.of_type(User)),
|
||||
selectinload(AllocationOrder.executor.of_type(User))
|
||||
).filter(
|
||||
AllocationOrder.id == order_id
|
||||
).first()
|
||||
|
||||
if not obj:
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
# 加载关联信息
|
||||
return self._load_order_relations(db, obj)
|
||||
|
||||
def get_orders(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
order_type: Optional[str] = None,
|
||||
approval_status: Optional[str] = None,
|
||||
execute_status: Optional[str] = None,
|
||||
applicant_id: Optional[int] = None,
|
||||
target_organization_id: Optional[int] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> tuple:
|
||||
"""获取分配单列表"""
|
||||
items, total = allocation_order.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
order_type=order_type,
|
||||
approval_status=approval_status,
|
||||
execute_status=execute_status,
|
||||
applicant_id=applicant_id,
|
||||
target_organization_id=target_organization_id,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
# 加载关联信息
|
||||
items_with_relations = [self._load_order_relations(db, item) for item in items]
|
||||
|
||||
return items_with_relations, total
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: AllocationOrderCreate,
|
||||
applicant_id: int
|
||||
):
|
||||
"""创建分配单"""
|
||||
# 验证资产存在性和状态
|
||||
assets = []
|
||||
for asset_id in obj_in.asset_ids:
|
||||
asset_obj = asset.get(db, asset_id)
|
||||
if not asset_obj:
|
||||
raise NotFoundException(f"资产ID {asset_id}")
|
||||
assets.append(asset_obj)
|
||||
|
||||
# 验证资产状态是否允许分配
|
||||
for asset_obj in assets:
|
||||
if not self._can_allocate(asset_obj.status, obj_in.order_type):
|
||||
raise BusinessException(
|
||||
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许{self._get_order_type_name(obj_in.order_type)}操作"
|
||||
)
|
||||
|
||||
# 生成分配单号
|
||||
order_code = await self._generate_order_code(db, obj_in.order_type)
|
||||
|
||||
# 创建分配单
|
||||
db_obj = allocation_order.create(
|
||||
db=db,
|
||||
obj_in=obj_in,
|
||||
order_code=order_code,
|
||||
applicant_id=applicant_id
|
||||
)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def update_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
obj_in: AllocationOrderUpdate,
|
||||
updater_id: int
|
||||
):
|
||||
"""更新分配单"""
|
||||
db_obj = allocation_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
# 只有草稿或待审批状态可以更新
|
||||
if db_obj.approval_status not in ["pending", "draft"]:
|
||||
raise BusinessException("只有待审批状态的分配单可以更新")
|
||||
|
||||
return allocation_order.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
async def approve_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
approval_in: AllocationOrderApproval,
|
||||
approver_id: int
|
||||
):
|
||||
"""审批分配单"""
|
||||
db_obj = allocation_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("该分配单已审批,无法重复审批")
|
||||
|
||||
# 审批
|
||||
db_obj = allocation_order.approve(
|
||||
db=db,
|
||||
db_obj=db_obj,
|
||||
approval_status=approval_in.approval_status,
|
||||
approver_id=approver_id,
|
||||
approval_remark=approval_in.approval_remark
|
||||
)
|
||||
|
||||
# 如果审批通过,执行分配逻辑
|
||||
if approval_in.approval_status == "approved":
|
||||
await self._execute_allocation_logic(db, db_obj)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
async def execute_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
executor_id: int
|
||||
):
|
||||
"""执行分配单"""
|
||||
db_obj = allocation_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "approved":
|
||||
raise BusinessException("该分配单未审批通过,无法执行")
|
||||
if db_obj.execute_status == "completed":
|
||||
raise BusinessException("该分配单已执行完成")
|
||||
|
||||
# 执行分配单
|
||||
db_obj = allocation_order.execute(db, db_obj, executor_id)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def cancel_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""取消分配单"""
|
||||
db_obj = allocation_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status == "completed":
|
||||
raise BusinessException("已完成的分配单无法取消")
|
||||
|
||||
allocation_order.cancel(db, db_obj)
|
||||
return True
|
||||
|
||||
def delete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""删除分配单"""
|
||||
db_obj = allocation_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
# 只有草稿或已取消的可以删除
|
||||
if db_obj.approval_status not in ["draft", "rejected", "cancelled"]:
|
||||
raise BusinessException("只能删除草稿、已拒绝或已取消的分配单")
|
||||
|
||||
return allocation_order.delete(db, order_id)
|
||||
|
||||
def get_order_items(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> List:
|
||||
"""获取分配单明细"""
|
||||
# 验证分配单存在
|
||||
if not allocation_order.get(db, order_id):
|
||||
raise NotFoundException("分配单")
|
||||
|
||||
return allocation_item.get_by_order(db, order_id)
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session,
|
||||
applicant_id: Optional[int] = None
|
||||
) -> Dict[str, int]:
|
||||
"""获取分配单统计信息"""
|
||||
return allocation_order.get_statistics(db, applicant_id)
|
||||
|
||||
async def _execute_allocation_logic(
|
||||
self,
|
||||
db: Session,
|
||||
order_obj
|
||||
):
|
||||
"""执行分配逻辑(审批通过后自动执行)"""
|
||||
# 根据单据类型执行不同的逻辑
|
||||
if order_obj.order_type == "allocation":
|
||||
await self._execute_allocation(db, order_obj)
|
||||
elif order_obj.order_type == "transfer":
|
||||
await self._execute_transfer(db, order_obj)
|
||||
elif order_obj.order_type == "recovery":
|
||||
await self._execute_recovery(db, order_obj)
|
||||
elif order_obj.order_type == "maintenance":
|
||||
await self._execute_maintenance_allocation(db, order_obj)
|
||||
elif order_obj.order_type == "scrap":
|
||||
await self._execute_scrap_allocation(db, order_obj)
|
||||
|
||||
async def _execute_allocation(self, db: Session, order_obj):
|
||||
"""执行资产分配"""
|
||||
# 更新明细状态为执行中
|
||||
allocation_item.batch_update_execute_status(db, order_obj.id, "executing")
|
||||
|
||||
# 获取明细
|
||||
items = allocation_item.get_by_order(db, order_obj.id)
|
||||
|
||||
# 更新资产状态
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# 变更资产状态
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=item.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status=item.to_status,
|
||||
remark=f"分配单: {order_obj.order_code}"
|
||||
),
|
||||
operator_id=order_obj.applicant_id
|
||||
)
|
||||
|
||||
# 更新明细状态为完成
|
||||
allocation_item.update_execute_status(db, item.id, "completed")
|
||||
except Exception as e:
|
||||
# 更新明细状态为失败
|
||||
allocation_item.update_execute_status(
|
||||
db,
|
||||
item.id,
|
||||
"failed",
|
||||
failure_reason=str(e)
|
||||
)
|
||||
|
||||
async def _execute_transfer(self, db: Session, order_obj):
|
||||
"""执行资产调拨"""
|
||||
# 调拨逻辑与分配类似,但需要记录调出和调入网点
|
||||
await self._execute_allocation(db, order_obj)
|
||||
|
||||
async def _execute_recovery(self, db: Session, order_obj):
|
||||
"""执行资产回收"""
|
||||
# 回收逻辑
|
||||
await self._execute_allocation(db, order_obj)
|
||||
|
||||
async def _execute_maintenance_allocation(self, db: Session, order_obj):
|
||||
"""执行维修分配"""
|
||||
# 维修分配逻辑
|
||||
await self._execute_allocation(db, order_obj)
|
||||
|
||||
async def _execute_scrap_allocation(self, db: Session, order_obj):
|
||||
"""执行报废分配"""
|
||||
# 报废分配逻辑
|
||||
await self._execute_allocation(db, order_obj)
|
||||
|
||||
def _load_order_relations(
|
||||
self,
|
||||
db: Session,
|
||||
obj
|
||||
) -> Dict[str, Any]:
|
||||
"""加载分配单关联信息"""
|
||||
from app.models.user import User
|
||||
from app.models.organization import Organization
|
||||
|
||||
result = {
|
||||
"id": obj.id,
|
||||
"order_code": obj.order_code,
|
||||
"order_type": obj.order_type,
|
||||
"title": obj.title,
|
||||
"source_organization_id": obj.source_organization_id,
|
||||
"target_organization_id": obj.target_organization_id,
|
||||
"applicant_id": obj.applicant_id,
|
||||
"approver_id": obj.approver_id,
|
||||
"approval_status": obj.approval_status,
|
||||
"approval_time": obj.approval_time,
|
||||
"approval_remark": obj.approval_remark,
|
||||
"expect_execute_date": obj.expect_execute_date,
|
||||
"actual_execute_date": obj.actual_execute_date,
|
||||
"executor_id": obj.executor_id,
|
||||
"execute_status": obj.execute_status,
|
||||
"remark": obj.remark,
|
||||
"created_at": obj.created_at,
|
||||
"updated_at": obj.updated_at
|
||||
}
|
||||
|
||||
# 加载关联信息
|
||||
if obj.source_organization_id:
|
||||
source_org = db.query(Organization).filter(
|
||||
Organization.id == obj.source_organization_id
|
||||
).first()
|
||||
if source_org:
|
||||
result["source_organization"] = {
|
||||
"id": source_org.id,
|
||||
"org_name": source_org.org_name,
|
||||
"org_type": source_org.org_type
|
||||
}
|
||||
|
||||
if obj.target_organization_id:
|
||||
target_org = db.query(Organization).filter(
|
||||
Organization.id == obj.target_organization_id
|
||||
).first()
|
||||
if target_org:
|
||||
result["target_organization"] = {
|
||||
"id": target_org.id,
|
||||
"org_name": target_org.org_name,
|
||||
"org_type": target_org.org_type
|
||||
}
|
||||
|
||||
if obj.applicant_id:
|
||||
applicant = db.query(User).filter(User.id == obj.applicant_id).first()
|
||||
if applicant:
|
||||
result["applicant"] = {
|
||||
"id": applicant.id,
|
||||
"real_name": applicant.real_name,
|
||||
"username": applicant.username
|
||||
}
|
||||
|
||||
if obj.approver_id:
|
||||
approver = db.query(User).filter(User.id == obj.approver_id).first()
|
||||
if approver:
|
||||
result["approver"] = {
|
||||
"id": approver.id,
|
||||
"real_name": approver.real_name,
|
||||
"username": approver.username
|
||||
}
|
||||
|
||||
if obj.executor_id:
|
||||
executor = db.query(User).filter(User.id == obj.executor_id).first()
|
||||
if executor:
|
||||
result["executor"] = {
|
||||
"id": executor.id,
|
||||
"real_name": executor.real_name,
|
||||
"username": executor.username
|
||||
}
|
||||
|
||||
# 加载明细
|
||||
items = allocation_item.get_by_order(db, obj.id)
|
||||
result["items"] = [
|
||||
{
|
||||
"id": item.id,
|
||||
"asset_id": item.asset_id,
|
||||
"asset_code": item.asset_code,
|
||||
"asset_name": item.asset_name,
|
||||
"from_status": item.from_status,
|
||||
"to_status": item.to_status,
|
||||
"execute_status": item.execute_status,
|
||||
"failure_reason": item.failure_reason
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
def _can_allocate(self, asset_status: str, order_type: str) -> bool:
|
||||
"""判断资产是否可以分配"""
|
||||
# 库存中或使用中的资产可以分配
|
||||
if order_type in ["allocation", "transfer"]:
|
||||
return asset_status in ["in_stock", "in_use"]
|
||||
elif order_type == "recovery":
|
||||
return asset_status == "in_use"
|
||||
elif order_type == "maintenance":
|
||||
return asset_status in ["in_stock", "in_use"]
|
||||
elif order_type == "scrap":
|
||||
return asset_status in ["in_stock", "in_use", "maintenance"]
|
||||
return False
|
||||
|
||||
def _get_order_type_name(self, order_type: str) -> str:
|
||||
"""获取单据类型中文名"""
|
||||
type_names = {
|
||||
"allocation": "分配",
|
||||
"transfer": "调拨",
|
||||
"recovery": "回收",
|
||||
"maintenance": "维修",
|
||||
"scrap": "报废"
|
||||
}
|
||||
return type_names.get(order_type, "操作")
|
||||
|
||||
async def _generate_order_code(self, db: Session, order_type: str) -> str:
|
||||
"""生成分配单号"""
|
||||
from datetime import datetime
|
||||
import random
|
||||
import string
|
||||
|
||||
# 单据类型前缀
|
||||
prefix_map = {
|
||||
"allocation": "AL",
|
||||
"transfer": "TF",
|
||||
"recovery": "RC",
|
||||
"maintenance": "MT",
|
||||
"scrap": "SC"
|
||||
}
|
||||
prefix = prefix_map.get(order_type, "AL")
|
||||
|
||||
# 日期部分
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# 序号部分(4位随机数)
|
||||
sequence = "".join(random.choices(string.digits, k=4))
|
||||
|
||||
# 组合单号: AL202501240001
|
||||
order_code = f"{prefix}{date_str}{sequence}"
|
||||
|
||||
# 检查是否重复,如果重复则重新生成
|
||||
while allocation_order.get_by_code(db, order_code):
|
||||
sequence = "".join(random.choices(string.digits, k=4))
|
||||
order_code = f"{prefix}{date_str}{sequence}"
|
||||
|
||||
return order_code
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
allocation_service = AllocationService()
|
||||
296
backend_new/app/services/asset_service.py
Normal file
296
backend_new/app/services/asset_service.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
资产管理业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from app.crud.asset import asset, asset_status_history
|
||||
from app.schemas.asset import (
|
||||
AssetCreate,
|
||||
AssetUpdate,
|
||||
AssetStatusTransition
|
||||
)
|
||||
from app.services.state_machine_service import state_machine_service
|
||||
from app.utils.asset_code import generate_asset_code
|
||||
from app.utils.qrcode import generate_qr_code, delete_qr_code
|
||||
from app.core.exceptions import NotFoundException, AlreadyExistsException, StateTransitionException
|
||||
|
||||
|
||||
class AssetService:
|
||||
"""资产服务类"""
|
||||
|
||||
def __init__(self):
|
||||
self.state_machine = state_machine_service
|
||||
|
||||
async def get_asset(self, db: Session, asset_id: int):
|
||||
"""获取资产详情"""
|
||||
obj = asset.get(db, asset_id)
|
||||
if not obj:
|
||||
raise NotFoundException("资产")
|
||||
|
||||
# 加载关联信息
|
||||
return self._load_relations(db, obj)
|
||||
|
||||
def get_assets(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
keyword: Optional[str] = None,
|
||||
device_type_id: Optional[int] = None,
|
||||
organization_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
purchase_date_start: Optional[Any] = None,
|
||||
purchase_date_end: Optional[Any] = None
|
||||
) -> Tuple[List, int]:
|
||||
"""获取资产列表"""
|
||||
return asset.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
keyword=keyword,
|
||||
device_type_id=device_type_id,
|
||||
organization_id=organization_id,
|
||||
status=status,
|
||||
purchase_date_start=purchase_date_start,
|
||||
purchase_date_end=purchase_date_end
|
||||
)
|
||||
|
||||
async def create_asset(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: AssetCreate,
|
||||
creator_id: int
|
||||
):
|
||||
"""创建资产"""
|
||||
# 检查序列号是否已存在
|
||||
if obj_in.serial_number:
|
||||
existing = asset.get_by_serial_number(db, obj_in.serial_number)
|
||||
if existing:
|
||||
raise AlreadyExistsException("该序列号已被使用")
|
||||
|
||||
# 生成资产编码
|
||||
asset_code = await generate_asset_code(db)
|
||||
|
||||
# 创建资产
|
||||
db_obj = asset.create(db, obj_in, asset_code, creator_id)
|
||||
|
||||
# 生成二维码
|
||||
try:
|
||||
qr_code_url = generate_qr_code(asset_code)
|
||||
db_obj.qr_code_url = qr_code_url
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
except Exception as e:
|
||||
# 二维码生成失败不影响资产创建
|
||||
pass
|
||||
|
||||
# 记录状态历史
|
||||
await self._record_status_change(
|
||||
db=db,
|
||||
asset_id=db_obj.id,
|
||||
old_status=None,
|
||||
new_status="pending",
|
||||
operation_type="create",
|
||||
operator_id=creator_id,
|
||||
operator_name=None, # 可以从用户表获取
|
||||
remark="资产创建"
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def update_asset(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: int,
|
||||
obj_in: AssetUpdate,
|
||||
updater_id: int
|
||||
):
|
||||
"""更新资产"""
|
||||
db_obj = asset.get(db, asset_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("资产")
|
||||
|
||||
# 如果更新序列号,检查是否重复
|
||||
if obj_in.serial_number and obj_in.serial_number != db_obj.serial_number:
|
||||
existing = asset.get_by_serial_number(db, obj_in.serial_number)
|
||||
if existing:
|
||||
raise AlreadyExistsException("该序列号已被使用")
|
||||
|
||||
return asset.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
def delete_asset(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: int,
|
||||
deleter_id: int
|
||||
) -> bool:
|
||||
"""删除资产"""
|
||||
if not asset.get(db, asset_id):
|
||||
raise NotFoundException("资产")
|
||||
return asset.delete(db, asset_id, deleter_id)
|
||||
|
||||
async def change_asset_status(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: int,
|
||||
status_transition: AssetStatusTransition,
|
||||
operator_id: int,
|
||||
operator_name: Optional[str] = None
|
||||
):
|
||||
"""变更资产状态"""
|
||||
db_obj = asset.get(db, asset_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("资产")
|
||||
|
||||
# 验证状态转换
|
||||
error = self.state_machine.validate_transition(
|
||||
db_obj.status,
|
||||
status_transition.new_status
|
||||
)
|
||||
if error:
|
||||
raise StateTransitionException(db_obj.status, status_transition.new_status)
|
||||
|
||||
# 更新状态
|
||||
old_status = db_obj.status
|
||||
asset.update_status(
|
||||
db=db,
|
||||
asset_id=asset_id,
|
||||
new_status=status_transition.new_status,
|
||||
updater_id=operator_id
|
||||
)
|
||||
|
||||
# 记录状态历史
|
||||
await self._record_status_change(
|
||||
db=db,
|
||||
asset_id=asset_id,
|
||||
old_status=old_status,
|
||||
new_status=status_transition.new_status,
|
||||
operation_type=self._get_operation_type(old_status, status_transition.new_status),
|
||||
operator_id=operator_id,
|
||||
operator_name=operator_name,
|
||||
remark=status_transition.remark,
|
||||
extra_data=status_transition.extra_data
|
||||
)
|
||||
|
||||
# 刷新对象
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def get_asset_status_history(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 50
|
||||
) -> List:
|
||||
"""获取资产状态历史"""
|
||||
if not asset.get(db, asset_id):
|
||||
raise NotFoundException("资产")
|
||||
|
||||
return asset_status_history.get_by_asset(db, asset_id, skip, limit)
|
||||
|
||||
def scan_asset_by_code(
|
||||
self,
|
||||
db: Session,
|
||||
asset_code: str
|
||||
):
|
||||
"""扫码查询资产"""
|
||||
obj = asset.get_by_code(db, asset_code)
|
||||
if not obj:
|
||||
raise NotFoundException("资产")
|
||||
|
||||
return self._load_relations(db, obj)
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取资产统计信息"""
|
||||
query = db.query(
|
||||
func.count(Asset.id).label("total"),
|
||||
func.sum(Asset.purchase_price).label("total_value")
|
||||
).filter(Asset.deleted_at.is_(None))
|
||||
|
||||
if organization_id:
|
||||
query = query.filter(Asset.organization_id == organization_id)
|
||||
|
||||
result = query.first()
|
||||
|
||||
# 按状态统计
|
||||
status_query = db.query(
|
||||
Asset.status,
|
||||
func.count(Asset.id).label("count")
|
||||
).filter(
|
||||
Asset.deleted_at.is_(None)
|
||||
)
|
||||
|
||||
if organization_id:
|
||||
status_query = status_query.filter(Asset.organization_id == organization_id)
|
||||
|
||||
status_query = status_query.group_by(Asset.status)
|
||||
status_distribution = {row.status: row.count for row in status_query.all()}
|
||||
|
||||
return {
|
||||
"total": result.total or 0,
|
||||
"total_value": float(result.total_value or 0),
|
||||
"status_distribution": status_distribution
|
||||
}
|
||||
|
||||
def _load_relations(self, db: Session, obj):
|
||||
"""加载关联信息"""
|
||||
# 这里可以预加载关联对象
|
||||
# 例如: obj.device_type, obj.brand, obj.organization等
|
||||
return obj
|
||||
|
||||
async def _record_status_change(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: int,
|
||||
old_status: Optional[str],
|
||||
new_status: str,
|
||||
operation_type: str,
|
||||
operator_id: int,
|
||||
operator_name: Optional[str] = None,
|
||||
organization_id: Optional[int] = None,
|
||||
remark: Optional[str] = None,
|
||||
extra_data: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""记录状态变更历史"""
|
||||
asset_status_history.create(
|
||||
db=db,
|
||||
asset_id=asset_id,
|
||||
old_status=old_status,
|
||||
new_status=new_status,
|
||||
operation_type=operation_type,
|
||||
operator_id=operator_id,
|
||||
operator_name=operator_name,
|
||||
organization_id=organization_id,
|
||||
remark=remark,
|
||||
extra_data=extra_data
|
||||
)
|
||||
|
||||
def _get_operation_type(self, old_status: str, new_status: str) -> str:
|
||||
"""根据状态转换获取操作类型"""
|
||||
operation_map = {
|
||||
("pending", "in_stock"): "in_stock",
|
||||
("in_stock", "in_use"): "allocate",
|
||||
("in_use", "in_stock"): "recover",
|
||||
("in_stock", "transferring"): "transfer",
|
||||
("in_use", "transferring"): "transfer",
|
||||
("transferring", "in_use"): "transfer_complete",
|
||||
("in_stock", "maintenance"): "maintenance",
|
||||
("in_use", "maintenance"): "maintenance",
|
||||
("maintenance", "in_stock"): "maintenance_complete",
|
||||
("maintenance", "in_use"): "maintenance_complete",
|
||||
("in_stock", "pending_scrap"): "pending_scrap",
|
||||
("in_use", "pending_scrap"): "pending_scrap",
|
||||
("pending_scrap", "scrapped"): "scrap",
|
||||
("pending_scrap", "in_stock"): "cancel_scrap",
|
||||
}
|
||||
return operation_map.get((old_status, new_status), "status_change")
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
asset_service = AssetService()
|
||||
346
backend_new/app/services/auth_service.py
Normal file
346
backend_new/app/services/auth_service.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
认证服务
|
||||
"""
|
||||
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的字典
|
||||
"""
|
||||
from app.utils.redis_client import redis_client
|
||||
import random
|
||||
import string
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# 生成4位随机验证码,使用更清晰的字符组合(排除易混淆的字符)
|
||||
captcha_text = ''.join(random.choices('23456789', k=4)) # 排除0、1
|
||||
|
||||
# 生成验证码图片
|
||||
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))
|
||||
|
||||
# 存储到Redis,5分钟过期
|
||||
await redis_client.setex(
|
||||
f"captcha:{captcha_key}",
|
||||
300,
|
||||
captcha_text
|
||||
)
|
||||
|
||||
return {
|
||||
"captcha_key": captcha_key,
|
||||
"captcha_base64": f"data:image/png;base64,{image_base64}"
|
||||
}
|
||||
|
||||
async def _verify_captcha(self, captcha_key: str, captcha: str) -> bool:
|
||||
"""
|
||||
验证验证码
|
||||
|
||||
Args:
|
||||
captcha_key: 验证码密钥
|
||||
captcha: 用户输入的验证码
|
||||
|
||||
Returns:
|
||||
验证是否成功
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from app.utils.redis_client import redis_client
|
||||
|
||||
try:
|
||||
# 从Redis获取存储的验证码
|
||||
stored_captcha = await redis_client.get(f"captcha:{captcha_key}")
|
||||
logger.info(f"Redis中存储的验证码: {stored_captcha}, 用户输入: {captcha}")
|
||||
|
||||
if not stored_captcha:
|
||||
logger.warning(f"验证码已过期或不存在 - captcha_key: {captcha_key}")
|
||||
return False
|
||||
|
||||
# 验证码不区分大小写
|
||||
is_valid = stored_captcha.lower() == captcha.lower()
|
||||
logger.info(f"验证码匹配结果: {is_valid}")
|
||||
|
||||
return is_valid
|
||||
except Exception as e:
|
||||
logger.error(f"验证码验证异常: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def _build_user_info(self, db: AsyncSession, user: User) -> UserInfo:
|
||||
"""
|
||||
构建用户信息
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user: 用户对象
|
||||
|
||||
Returns:
|
||||
UserInfo: 用户信息
|
||||
"""
|
||||
# 获取用户角色代码列表
|
||||
role_codes = [role.role_code for role in user.roles]
|
||||
|
||||
# 获取用户权限代码列表
|
||||
permissions = []
|
||||
for role in user.roles:
|
||||
for perm in role.permissions:
|
||||
permissions.append(perm.permission_code)
|
||||
|
||||
# 如果是超级管理员,给予所有权限
|
||||
if user.is_admin:
|
||||
permissions = ["*:*:*"]
|
||||
|
||||
return UserInfo(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
real_name=user.real_name,
|
||||
email=user.email,
|
||||
avatar_url=user.avatar_url,
|
||||
is_admin=user.is_admin,
|
||||
status=user.status
|
||||
)
|
||||
|
||||
|
||||
# 创建服务实例
|
||||
auth_service = AuthService()
|
||||
134
backend_new/app/services/brand_supplier_service.py
Normal file
134
backend_new/app/services/brand_supplier_service.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
品牌和供应商业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from app.crud.brand_supplier import brand, supplier
|
||||
from app.schemas.brand_supplier import (
|
||||
BrandCreate,
|
||||
BrandUpdate,
|
||||
SupplierCreate,
|
||||
SupplierUpdate
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, AlreadyExistsException
|
||||
|
||||
|
||||
class BrandService:
|
||||
"""品牌服务类"""
|
||||
|
||||
def get_brand(self, db: Session, brand_id: int):
|
||||
"""获取品牌详情"""
|
||||
obj = brand.get(db, brand_id)
|
||||
if not obj:
|
||||
raise NotFoundException("品牌")
|
||||
return obj
|
||||
|
||||
def get_brands(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
status: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> Tuple[List, int]:
|
||||
"""获取品牌列表"""
|
||||
return brand.get_multi(db, skip, limit, status, keyword)
|
||||
|
||||
def create_brand(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: BrandCreate,
|
||||
creator_id: Optional[int] = None
|
||||
):
|
||||
"""创建品牌"""
|
||||
try:
|
||||
return brand.create(db, obj_in, creator_id)
|
||||
except ValueError as e:
|
||||
raise AlreadyExistsException("品牌") from e
|
||||
|
||||
def update_brand(
|
||||
self,
|
||||
db: Session,
|
||||
brand_id: int,
|
||||
obj_in: BrandUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
):
|
||||
"""更新品牌"""
|
||||
db_obj = brand.get(db, brand_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("品牌")
|
||||
return brand.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
def delete_brand(
|
||||
self,
|
||||
db: Session,
|
||||
brand_id: int,
|
||||
deleter_id: Optional[int] = None
|
||||
) -> bool:
|
||||
"""删除品牌"""
|
||||
if not brand.get(db, brand_id):
|
||||
raise NotFoundException("品牌")
|
||||
return brand.delete(db, brand_id, deleter_id)
|
||||
|
||||
|
||||
class SupplierService:
|
||||
"""供应商服务类"""
|
||||
|
||||
def get_supplier(self, db: Session, supplier_id: int):
|
||||
"""获取供应商详情"""
|
||||
obj = supplier.get(db, supplier_id)
|
||||
if not obj:
|
||||
raise NotFoundException("供应商")
|
||||
return obj
|
||||
|
||||
def get_suppliers(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
status: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> Tuple[List, int]:
|
||||
"""获取供应商列表"""
|
||||
return supplier.get_multi(db, skip, limit, status, keyword)
|
||||
|
||||
def create_supplier(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: SupplierCreate,
|
||||
creator_id: Optional[int] = None
|
||||
):
|
||||
"""创建供应商"""
|
||||
try:
|
||||
return supplier.create(db, obj_in, creator_id)
|
||||
except ValueError as e:
|
||||
raise AlreadyExistsException("供应商") from e
|
||||
|
||||
def update_supplier(
|
||||
self,
|
||||
db: Session,
|
||||
supplier_id: int,
|
||||
obj_in: SupplierUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
):
|
||||
"""更新供应商"""
|
||||
db_obj = supplier.get(db, supplier_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("供应商")
|
||||
return supplier.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
def delete_supplier(
|
||||
self,
|
||||
db: Session,
|
||||
supplier_id: int,
|
||||
deleter_id: Optional[int] = None
|
||||
) -> bool:
|
||||
"""删除供应商"""
|
||||
if not supplier.get(db, supplier_id):
|
||||
raise NotFoundException("供应商")
|
||||
return supplier.delete(db, supplier_id, deleter_id)
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
brand_service = BrandService()
|
||||
supplier_service = SupplierService()
|
||||
286
backend_new/app/services/device_type_service.py
Normal file
286
backend_new/app/services/device_type_service.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
设备类型业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from app.crud.device_type import device_type, device_type_field
|
||||
from app.schemas.device_type import (
|
||||
DeviceTypeCreate,
|
||||
DeviceTypeUpdate,
|
||||
DeviceTypeFieldCreate,
|
||||
DeviceTypeFieldUpdate
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, AlreadyExistsException
|
||||
|
||||
|
||||
class DeviceTypeService:
|
||||
"""设备类型服务类"""
|
||||
|
||||
def get_device_type(self, db: Session, device_type_id: int, include_fields: bool = False):
|
||||
"""
|
||||
获取设备类型详情
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
device_type_id: 设备类型ID
|
||||
include_fields: 是否包含字段列表
|
||||
|
||||
Returns:
|
||||
设备类型对象
|
||||
|
||||
Raises:
|
||||
NotFoundException: 设备类型不存在
|
||||
"""
|
||||
obj = device_type.get(db, device_type_id)
|
||||
if not obj:
|
||||
raise NotFoundException("设备类型")
|
||||
|
||||
# 计算字段数量
|
||||
field_count = device_type_field.get_by_device_type(db, device_type_id)
|
||||
obj.field_count = len(field_count)
|
||||
|
||||
return obj
|
||||
|
||||
def get_device_types(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
category: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> Tuple[List, int]:
|
||||
"""
|
||||
获取设备类型列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过条数
|
||||
limit: 返回条数
|
||||
category: 设备分类
|
||||
status: 状态
|
||||
keyword: 搜索关键词
|
||||
|
||||
Returns:
|
||||
(设备类型列表, 总数)
|
||||
"""
|
||||
items, total = device_type.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
category=category,
|
||||
status=status,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
# 为每个项目添加字段数量
|
||||
for item in items:
|
||||
fields = device_type_field.get_by_device_type(db, item.id)
|
||||
item.field_count = len(fields)
|
||||
|
||||
return items, total
|
||||
|
||||
def create_device_type(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: DeviceTypeCreate,
|
||||
creator_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
创建设备类型
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
obj_in: 创建数据
|
||||
creator_id: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的设备类型对象
|
||||
|
||||
Raises:
|
||||
AlreadyExistsException: 设备类型代码已存在
|
||||
"""
|
||||
try:
|
||||
return device_type.create(db, obj_in, creator_id)
|
||||
except ValueError as e:
|
||||
raise AlreadyExistsException("设备类型") from e
|
||||
|
||||
def update_device_type(
|
||||
self,
|
||||
db: Session,
|
||||
device_type_id: int,
|
||||
obj_in: DeviceTypeUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
更新设备类型
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
device_type_id: 设备类型ID
|
||||
obj_in: 更新数据
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新后的设备类型对象
|
||||
|
||||
Raises:
|
||||
NotFoundException: 设备类型不存在
|
||||
"""
|
||||
db_obj = device_type.get(db, device_type_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("设备类型")
|
||||
|
||||
return device_type.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
def delete_device_type(
|
||||
self,
|
||||
db: Session,
|
||||
device_type_id: int,
|
||||
deleter_id: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
删除设备类型
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
device_type_id: 设备类型ID
|
||||
deleter_id: 删除人ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
|
||||
Raises:
|
||||
NotFoundException: 设备类型不存在
|
||||
"""
|
||||
if not device_type.get(db, device_type_id):
|
||||
raise NotFoundException("设备类型")
|
||||
|
||||
return device_type.delete(db, device_type_id, deleter_id)
|
||||
|
||||
def get_device_type_fields(
|
||||
self,
|
||||
db: Session,
|
||||
device_type_id: int,
|
||||
status: Optional[str] = None
|
||||
) -> List:
|
||||
"""
|
||||
获取设备类型的字段列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
device_type_id: 设备类型ID
|
||||
status: 状态筛选
|
||||
|
||||
Returns:
|
||||
字段列表
|
||||
|
||||
Raises:
|
||||
NotFoundException: 设备类型不存在
|
||||
"""
|
||||
# 验证设备类型存在
|
||||
if not device_type.get(db, device_type_id):
|
||||
raise NotFoundException("设备类型")
|
||||
|
||||
return device_type_field.get_by_device_type(db, device_type_id, status)
|
||||
|
||||
def create_device_type_field(
|
||||
self,
|
||||
db: Session,
|
||||
device_type_id: int,
|
||||
obj_in: DeviceTypeFieldCreate,
|
||||
creator_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
创建设备类型字段
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
device_type_id: 设备类型ID
|
||||
obj_in: 创建数据
|
||||
creator_id: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的字段对象
|
||||
|
||||
Raises:
|
||||
NotFoundException: 设备类型不存在
|
||||
AlreadyExistsException: 字段代码已存在
|
||||
"""
|
||||
# 验证设备类型存在
|
||||
if not device_type.get(db, device_type_id):
|
||||
raise NotFoundException("设备类型")
|
||||
|
||||
try:
|
||||
return device_type_field.create(db, obj_in, device_type_id, creator_id)
|
||||
except ValueError as e:
|
||||
raise AlreadyExistsException("字段") from e
|
||||
|
||||
def update_device_type_field(
|
||||
self,
|
||||
db: Session,
|
||||
field_id: int,
|
||||
obj_in: DeviceTypeFieldUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
更新设备类型字段
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
field_id: 字段ID
|
||||
obj_in: 更新数据
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新后的字段对象
|
||||
|
||||
Raises:
|
||||
NotFoundException: 字段不存在
|
||||
"""
|
||||
db_obj = device_type_field.get(db, field_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("字段")
|
||||
|
||||
return device_type_field.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
def delete_device_type_field(
|
||||
self,
|
||||
db: Session,
|
||||
field_id: int,
|
||||
deleter_id: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
删除设备类型字段
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
field_id: 字段ID
|
||||
deleter_id: 删除人ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
|
||||
Raises:
|
||||
NotFoundException: 字段不存在
|
||||
"""
|
||||
if not device_type_field.get(db, field_id):
|
||||
raise NotFoundException("字段")
|
||||
|
||||
return device_type_field.delete(db, field_id, deleter_id)
|
||||
|
||||
def get_all_categories(self, db: Session) -> List[str]:
|
||||
"""
|
||||
获取所有设备分类
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
设备分类列表
|
||||
"""
|
||||
return device_type.get_all_categories(db)
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
device_type_service = DeviceTypeService()
|
||||
508
backend_new/app/services/file_service.py
Normal file
508
backend_new/app/services/file_service.py
Normal file
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
文件存储服务
|
||||
"""
|
||||
import os
|
||||
import uuid
|
||||
import secrets
|
||||
import mimetypes
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import UploadFile, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
from app.models.file_management import UploadedFile
|
||||
from app.schemas.file_management import (
|
||||
UploadedFileCreate,
|
||||
FileUploadResponse,
|
||||
FileShareResponse,
|
||||
FileStatistics
|
||||
)
|
||||
from app.crud.file_management import uploaded_file as crud_uploaded_file
|
||||
|
||||
|
||||
class FileService:
|
||||
"""文件存储服务"""
|
||||
|
||||
# 允许的文件类型白名单
|
||||
ALLOWED_MIME_TYPES = {
|
||||
# 图片
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', 'image/svg+xml',
|
||||
# 文档
|
||||
'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain', 'text/csv',
|
||||
# 压缩包
|
||||
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
||||
# 其他
|
||||
'application/json', 'application/xml', 'text/xml'
|
||||
}
|
||||
|
||||
# 文件大小限制(字节)- 默认100MB
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024
|
||||
|
||||
# 图片文件大小限制 - 默认10MB
|
||||
MAX_IMAGE_SIZE = 10 * 1024 * 1024
|
||||
|
||||
# Magic Numbers for file validation
|
||||
MAGIC_NUMBERS = {
|
||||
b'\xFF\xD8\xFF': 'image/jpeg',
|
||||
b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A': 'image/png',
|
||||
b'GIF87a': 'image/gif',
|
||||
b'GIF89a': 'image/gif',
|
||||
b'%PDF': 'application/pdf',
|
||||
b'PK\x03\x04': 'application/zip',
|
||||
}
|
||||
|
||||
def __init__(self, base_upload_dir: str = "uploads"):
|
||||
self.base_upload_dir = Path(base_upload_dir)
|
||||
self.ensure_upload_dirs()
|
||||
|
||||
def ensure_upload_dirs(self):
|
||||
"""确保上传目录存在"""
|
||||
directories = [
|
||||
self.base_upload_dir,
|
||||
self.base_upload_dir / "images",
|
||||
self.base_upload_dir / "documents",
|
||||
self.base_upload_dir / "thumbnails",
|
||||
self.base_upload_dir / "temp",
|
||||
]
|
||||
for directory in directories:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def validate_file_type(self, file: UploadFile) -> bool:
|
||||
"""验证文件类型"""
|
||||
# 检查MIME类型
|
||||
if file.content_type not in self.ALLOWED_MIME_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"不支持的文件类型: {file.content_type}"
|
||||
)
|
||||
return True
|
||||
|
||||
def validate_file_size(self, file: UploadFile) -> bool:
|
||||
"""验证文件大小"""
|
||||
# 先检查是否是图片
|
||||
if file.content_type and file.content_type.startswith('image/'):
|
||||
max_size = self.MAX_IMAGE_SIZE
|
||||
else:
|
||||
max_size = self.MAX_FILE_SIZE
|
||||
|
||||
# 读取文件内容检查大小
|
||||
content = file.file.read()
|
||||
file.file.seek(0) # 重置文件指针
|
||||
|
||||
if len(content) > max_size:
|
||||
# 转换为MB
|
||||
size_mb = max_size / (1024 * 1024)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"文件大小超过限制: {size_mb:.0f}MB"
|
||||
)
|
||||
return True
|
||||
|
||||
def validate_file_content(self, content: bytes) -> str:
|
||||
"""验证文件内容(Magic Number)"""
|
||||
for magic, mime_type in self.MAGIC_NUMBERS.items():
|
||||
if content.startswith(magic):
|
||||
return mime_type
|
||||
return None
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
db: Session,
|
||||
file: UploadFile,
|
||||
uploader_id: int,
|
||||
remark: Optional[str] = None
|
||||
) -> UploadedFile:
|
||||
"""
|
||||
上传文件
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
file: 上传的文件
|
||||
uploader_id: 上传者ID
|
||||
remark: 备注
|
||||
|
||||
Returns:
|
||||
UploadedFile: 创建的文件记录
|
||||
"""
|
||||
# 验证文件类型
|
||||
self.validate_file_type(file)
|
||||
|
||||
# 验证文件大小
|
||||
self.validate_file_size(file)
|
||||
|
||||
# 读取文件内容
|
||||
content = await file.read()
|
||||
|
||||
# 验证文件内容
|
||||
detected_mime = self.validate_file_content(content)
|
||||
if detected_mime and detected_mime != file.content_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"文件内容与扩展名不匹配"
|
||||
)
|
||||
|
||||
# 生成文件名
|
||||
file_ext = self.get_file_extension(file.filename)
|
||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
||||
|
||||
# 确定存储路径
|
||||
upload_date = datetime.utcnow()
|
||||
date_dir = upload_date.strftime("%Y/%m/%d")
|
||||
save_dir = self.base_upload_dir / date_dir
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = save_dir / unique_filename
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
# 生成缩略图(如果是图片)
|
||||
thumbnail_path = None
|
||||
if file.content_type and file.content_type.startswith('image/'):
|
||||
thumbnail_path = self.generate_thumbnail(content, unique_filename, date_dir)
|
||||
|
||||
# 创建数据库记录
|
||||
file_create = UploadedFileCreate(
|
||||
file_name=unique_filename,
|
||||
original_name=file.filename,
|
||||
file_path=str(file_path),
|
||||
file_size=len(content),
|
||||
file_type=file.content_type,
|
||||
file_ext=file_ext.lstrip('.'),
|
||||
uploader_id=uploader_id
|
||||
)
|
||||
|
||||
db_obj = crud_uploaded_file.create(db, obj_in=file_create.dict())
|
||||
|
||||
# 更新缩略图路径
|
||||
if thumbnail_path:
|
||||
crud_uploaded_file.update(db, db_obj=db_obj, obj_in={"thumbnail_path": thumbnail_path})
|
||||
|
||||
# 模拟病毒扫描
|
||||
self._scan_virus(file_path)
|
||||
|
||||
return db_obj
|
||||
|
||||
def generate_thumbnail(
|
||||
self,
|
||||
content: bytes,
|
||||
filename: str,
|
||||
date_dir: str
|
||||
) -> Optional[str]:
|
||||
"""生成缩略图"""
|
||||
try:
|
||||
# 打开图片
|
||||
image = Image.open(io.BytesIO(content))
|
||||
|
||||
# 转换为RGB(如果是RGBA)
|
||||
if image.mode in ('RGBA', 'P'):
|
||||
image = image.convert('RGB')
|
||||
|
||||
# 创建缩略图
|
||||
thumbnail_size = (200, 200)
|
||||
image.thumbnail(thumbnail_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# 保存缩略图
|
||||
thumbnail_dir = self.base_upload_dir / "thumbnails" / date_dir
|
||||
thumbnail_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
thumbnail_name = f"thumb_{filename}"
|
||||
thumbnail_path = thumbnail_dir / thumbnail_name
|
||||
image.save(thumbnail_path, 'JPEG', quality=85)
|
||||
|
||||
return str(thumbnail_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"生成缩略图失败: {e}")
|
||||
return None
|
||||
|
||||
def get_file_path(self, file_obj: UploadedFile) -> Path:
|
||||
"""获取文件路径"""
|
||||
return Path(file_obj.file_path)
|
||||
|
||||
def file_exists(self, file_obj: UploadedFile) -> bool:
|
||||
"""检查文件是否存在"""
|
||||
file_path = self.get_file_path(file_obj)
|
||||
return file_path.exists() and file_path.is_file()
|
||||
|
||||
def delete_file_from_disk(self, file_obj: UploadedFile) -> bool:
|
||||
"""从磁盘删除文件"""
|
||||
try:
|
||||
file_path = self.get_file_path(file_obj)
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
# 删除缩略图
|
||||
if file_obj.thumbnail_path:
|
||||
thumbnail_path = Path(file_obj.thumbnail_path)
|
||||
if thumbnail_path.exists():
|
||||
thumbnail_path.unlink()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"删除文件失败: {e}")
|
||||
return False
|
||||
|
||||
def generate_share_link(
|
||||
self,
|
||||
db: Session,
|
||||
file_id: int,
|
||||
expire_days: int = 7,
|
||||
base_url: str = "http://localhost:8000"
|
||||
) -> FileShareResponse:
|
||||
"""
|
||||
生成分享链接
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
file_id: 文件ID
|
||||
expire_days: 有效期(天)
|
||||
base_url: 基础URL
|
||||
|
||||
Returns:
|
||||
FileShareResponse: 分享链接信息
|
||||
"""
|
||||
# 生成分享码
|
||||
share_code = crud_uploaded_file.generate_share_code(
|
||||
db,
|
||||
file_id=file_id,
|
||||
expire_days=expire_days
|
||||
)
|
||||
|
||||
if not share_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 获取文件信息
|
||||
file_obj = crud_uploaded_file.get(db, file_id)
|
||||
expire_time = file_obj.share_expire_time
|
||||
|
||||
# 生成分享URL
|
||||
share_url = f"{base_url}/api/v1/files/share/{share_code}"
|
||||
|
||||
return FileShareResponse(
|
||||
share_code=share_code,
|
||||
share_url=share_url,
|
||||
expire_time=expire_time
|
||||
)
|
||||
|
||||
def get_shared_file(self, db: Session, share_code: str) -> Optional[UploadedFile]:
|
||||
"""通过分享码获取文件"""
|
||||
return crud_uploaded_file.get_by_share_code(db, share_code)
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session,
|
||||
uploader_id: Optional[int] = None
|
||||
) -> FileStatistics:
|
||||
"""获取文件统计信息"""
|
||||
stats = crud_uploaded_file.get_statistics(db, uploader_id=uploader_id)
|
||||
return FileStatistics(**stats)
|
||||
|
||||
@staticmethod
|
||||
def get_file_extension(filename: str) -> str:
|
||||
"""获取文件扩展名"""
|
||||
return os.path.splitext(filename)[1]
|
||||
|
||||
@staticmethod
|
||||
def get_mime_type(filename: str) -> str:
|
||||
"""获取MIME类型"""
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
return mime_type or 'application/octet-stream'
|
||||
|
||||
@staticmethod
|
||||
def _scan_virus(file_path: Path) -> bool:
|
||||
"""
|
||||
模拟病毒扫描
|
||||
|
||||
实际生产环境应集成专业杀毒软件如:
|
||||
- ClamAV
|
||||
- VirusTotal API
|
||||
- Windows Defender
|
||||
"""
|
||||
# 模拟扫描
|
||||
import time
|
||||
time.sleep(0.1) # 模拟扫描时间
|
||||
return True # 假设文件安全
|
||||
|
||||
|
||||
# 分片上传管理
|
||||
class ChunkUploadManager:
|
||||
"""分片上传管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.uploads: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def init_upload(
|
||||
self,
|
||||
file_name: str,
|
||||
file_size: int,
|
||||
file_type: str,
|
||||
total_chunks: int,
|
||||
file_hash: Optional[str] = None
|
||||
) -> str:
|
||||
"""初始化分片上传"""
|
||||
upload_id = str(uuid.uuid4())
|
||||
|
||||
self.uploads[upload_id] = {
|
||||
"file_name": file_name,
|
||||
"file_size": file_size,
|
||||
"file_type": file_type,
|
||||
"total_chunks": total_chunks,
|
||||
"file_hash": file_hash,
|
||||
"uploaded_chunks": [],
|
||||
"created_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
return upload_id
|
||||
|
||||
def save_chunk(
|
||||
self,
|
||||
upload_id: str,
|
||||
chunk_index: int,
|
||||
chunk_data: bytes
|
||||
) -> bool:
|
||||
"""保存分片"""
|
||||
if upload_id not in self.uploads:
|
||||
return False
|
||||
|
||||
upload_info = self.uploads[upload_id]
|
||||
|
||||
# 保存分片到临时文件
|
||||
temp_dir = Path("uploads/temp")
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
chunk_filename = f"{upload_id}_chunk_{chunk_index}"
|
||||
chunk_path = temp_dir / chunk_filename
|
||||
|
||||
with open(chunk_path, "wb") as f:
|
||||
f.write(chunk_data)
|
||||
|
||||
# 记录已上传的分片
|
||||
if chunk_index not in upload_info["uploaded_chunks"]:
|
||||
upload_info["uploaded_chunks"].append(chunk_index)
|
||||
|
||||
return True
|
||||
|
||||
def is_complete(self, upload_id: str) -> bool:
|
||||
"""检查是否所有分片都已上传"""
|
||||
if upload_id not in self.uploads:
|
||||
return False
|
||||
|
||||
upload_info = self.uploads[upload_id]
|
||||
return len(upload_info["uploaded_chunks"]) == upload_info["total_chunks"]
|
||||
|
||||
def merge_chunks(
|
||||
self,
|
||||
db: Session,
|
||||
upload_id: str,
|
||||
uploader_id: int,
|
||||
file_service: FileService
|
||||
) -> UploadedFile:
|
||||
"""合并分片"""
|
||||
if upload_id not in self.uploads:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="上传会话不存在"
|
||||
)
|
||||
|
||||
if not self.is_complete(upload_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="分片未全部上传"
|
||||
)
|
||||
|
||||
upload_info = self.uploads[upload_id]
|
||||
|
||||
# 合并分片
|
||||
temp_dir = Path("uploads/temp")
|
||||
merged_content = b""
|
||||
|
||||
for i in range(upload_info["total_chunks"]):
|
||||
chunk_filename = f"{upload_id}_chunk_{i}"
|
||||
chunk_path = temp_dir / chunk_filename
|
||||
|
||||
if not chunk_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"分片 {i} 不存在"
|
||||
)
|
||||
|
||||
with open(chunk_path, "rb") as f:
|
||||
merged_content += f.read()
|
||||
|
||||
# 验证文件大小
|
||||
if len(merged_content) != upload_info["file_size"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="文件大小不匹配"
|
||||
)
|
||||
|
||||
# 验证文件哈希(如果提供)
|
||||
if upload_info["file_hash"]:
|
||||
import hashlib
|
||||
file_hash = hashlib.md5(merged_content).hexdigest()
|
||||
if file_hash != upload_info["file_hash"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="文件哈希不匹配"
|
||||
)
|
||||
|
||||
# 保存文件
|
||||
file_ext = Path(upload_info["file_name"]).suffix
|
||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
||||
upload_date = datetime.utcnow()
|
||||
date_dir = upload_date.strftime("%Y/%m/%d")
|
||||
save_dir = Path("uploads") / date_dir
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = save_dir / unique_filename
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(merged_content)
|
||||
|
||||
# 清理临时文件
|
||||
self.cleanup_upload(upload_id)
|
||||
|
||||
# 创建数据库记录
|
||||
from app.schemas.file_management import UploadedFileCreate
|
||||
file_create = UploadedFileCreate(
|
||||
file_name=unique_filename,
|
||||
original_name=upload_info["file_name"],
|
||||
file_path=str(file_path),
|
||||
file_size=upload_info["file_size"],
|
||||
file_type=upload_info["file_type"],
|
||||
file_ext=file_ext.lstrip('.'),
|
||||
uploader_id=uploader_id
|
||||
)
|
||||
|
||||
db_obj = crud_uploaded_file.create(db, obj_in=file_create.dict())
|
||||
|
||||
return db_obj
|
||||
|
||||
def cleanup_upload(self, upload_id: str):
|
||||
"""清理上传会话"""
|
||||
if upload_id in self.uploads:
|
||||
del self.uploads[upload_id]
|
||||
|
||||
# 清理临时分片文件
|
||||
temp_dir = Path("uploads/temp")
|
||||
for chunk_file in temp_dir.glob(f"{upload_id}_chunk_*"):
|
||||
chunk_file.unlink()
|
||||
|
||||
|
||||
# 创建服务实例
|
||||
file_service = FileService()
|
||||
chunk_upload_manager = ChunkUploadManager()
|
||||
403
backend_new/app/services/maintenance_service.py
Normal file
403
backend_new/app/services/maintenance_service.py
Normal file
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
维修管理业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from app.crud.maintenance import maintenance_record
|
||||
from app.crud.asset import asset
|
||||
from app.schemas.maintenance import (
|
||||
MaintenanceRecordCreate,
|
||||
MaintenanceRecordUpdate,
|
||||
MaintenanceRecordStart,
|
||||
MaintenanceRecordComplete
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, BusinessException
|
||||
|
||||
|
||||
class MaintenanceService:
|
||||
"""维修管理服务类"""
|
||||
|
||||
async def get_record(
|
||||
self,
|
||||
db: Session,
|
||||
record_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""获取维修记录详情"""
|
||||
# 使用selectinload预加载关联数据,避免N+1查询
|
||||
from app.models.maintenance import MaintenanceRecord
|
||||
from app.models.asset import Asset
|
||||
from app.models.user import User
|
||||
from app.models.brand_supplier import Supplier
|
||||
|
||||
obj = db.query(
|
||||
MaintenanceRecord
|
||||
).options(
|
||||
selectinload(MaintenanceRecord.asset.of_type(Asset)),
|
||||
selectinload(MaintenanceRecord.report_user.of_type(User)),
|
||||
selectinload(MaintenanceRecord.maintenance_user.of_type(User)),
|
||||
selectinload(MaintenanceRecord.vendor.of_type(Supplier))
|
||||
).filter(
|
||||
MaintenanceRecord.id == record_id
|
||||
).first()
|
||||
|
||||
if not obj:
|
||||
raise NotFoundException("维修记录")
|
||||
|
||||
return self._load_relations(db, obj)
|
||||
|
||||
def get_records(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
asset_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
fault_type: Optional[str] = None,
|
||||
priority: Optional[str] = None,
|
||||
maintenance_type: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> tuple:
|
||||
"""获取维修记录列表"""
|
||||
items, total = maintenance_record.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
asset_id=asset_id,
|
||||
status=status,
|
||||
fault_type=fault_type,
|
||||
priority=priority,
|
||||
maintenance_type=maintenance_type,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
# 加载关联信息
|
||||
items_with_relations = [self._load_relations(db, item) for item in items]
|
||||
|
||||
return items_with_relations, total
|
||||
|
||||
async def create_record(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: MaintenanceRecordCreate,
|
||||
report_user_id: int,
|
||||
creator_id: int
|
||||
):
|
||||
"""创建维修记录"""
|
||||
# 验证资产存在
|
||||
asset_obj = asset.get(db, obj_in.asset_id)
|
||||
if not asset_obj:
|
||||
raise NotFoundException("资产")
|
||||
|
||||
# 生成维修单号
|
||||
record_code = await self._generate_record_code(db)
|
||||
|
||||
# 创建维修记录
|
||||
db_obj = maintenance_record.create(
|
||||
db=db,
|
||||
obj_in=obj_in,
|
||||
record_code=record_code,
|
||||
asset_code=asset_obj.asset_code,
|
||||
report_user_id=report_user_id,
|
||||
creator_id=creator_id
|
||||
)
|
||||
|
||||
# 如果资产状态不是维修中,则更新状态
|
||||
if asset_obj.status != "maintenance":
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition
|
||||
|
||||
try:
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=asset_obj.id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status="maintenance",
|
||||
remark=f"报修: {record_code}"
|
||||
),
|
||||
operator_id=report_user_id
|
||||
)
|
||||
except Exception as e:
|
||||
# 状态更新失败不影响维修记录创建
|
||||
pass
|
||||
|
||||
return self._load_relations(db, db_obj)
|
||||
|
||||
def update_record(
|
||||
self,
|
||||
db: Session,
|
||||
record_id: int,
|
||||
obj_in: MaintenanceRecordUpdate,
|
||||
updater_id: int
|
||||
):
|
||||
"""更新维修记录"""
|
||||
db_obj = maintenance_record.get(db, record_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("维修记录")
|
||||
|
||||
# 已完成的维修记录不能更新
|
||||
if db_obj.status == "completed":
|
||||
raise BusinessException("已完成的维修记录不能更新")
|
||||
|
||||
return maintenance_record.update(db, db_obj, obj_in, updater_id)
|
||||
|
||||
async def start_maintenance(
|
||||
self,
|
||||
db: Session,
|
||||
record_id: int,
|
||||
start_in: MaintenanceRecordStart,
|
||||
maintenance_user_id: int
|
||||
):
|
||||
"""开始维修"""
|
||||
db_obj = maintenance_record.get(db, record_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("维修记录")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.status != "pending":
|
||||
raise BusinessException("只有待处理状态的维修记录可以开始维修")
|
||||
|
||||
# 验证维修类型
|
||||
if start_in.maintenance_type == "vendor_repair" and not start_in.vendor_id:
|
||||
raise BusinessException("外部维修必须指定维修供应商")
|
||||
|
||||
# 开始维修
|
||||
db_obj = maintenance_record.start_maintenance(
|
||||
db=db,
|
||||
db_obj=db_obj,
|
||||
maintenance_type=start_in.maintenance_type,
|
||||
maintenance_user_id=maintenance_user_id,
|
||||
vendor_id=start_in.vendor_id
|
||||
)
|
||||
|
||||
return self._load_relations(db, db_obj)
|
||||
|
||||
async def complete_maintenance(
|
||||
self,
|
||||
db: Session,
|
||||
record_id: int,
|
||||
complete_in: MaintenanceRecordComplete,
|
||||
maintenance_user_id: int
|
||||
):
|
||||
"""完成维修"""
|
||||
db_obj = maintenance_record.get(db, record_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("维修记录")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.status != "in_progress":
|
||||
raise BusinessException("只有维修中的记录可以完成")
|
||||
|
||||
# 完成维修
|
||||
db_obj = maintenance_record.complete_maintenance(
|
||||
db=db,
|
||||
db_obj=db_obj,
|
||||
maintenance_result=complete_in.maintenance_result,
|
||||
maintenance_cost=complete_in.maintenance_cost,
|
||||
replaced_parts=complete_in.replaced_parts,
|
||||
images=complete_in.images
|
||||
)
|
||||
|
||||
# 恢复资产状态
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition
|
||||
|
||||
try:
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=db_obj.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status=complete_in.asset_status,
|
||||
remark=f"维修完成: {db_obj.record_code}"
|
||||
),
|
||||
operator_id=maintenance_user_id
|
||||
)
|
||||
except Exception as e:
|
||||
# 状态更新失败不影响维修记录完成
|
||||
pass
|
||||
|
||||
return self._load_relations(db, db_obj)
|
||||
|
||||
def cancel_maintenance(
|
||||
self,
|
||||
db: Session,
|
||||
record_id: int
|
||||
):
|
||||
"""取消维修"""
|
||||
db_obj = maintenance_record.get(db, record_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("维修记录")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.status == "completed":
|
||||
raise BusinessException("已完成的维修记录不能取消")
|
||||
|
||||
# 取消维修
|
||||
db_obj = maintenance_record.cancel_maintenance(db, db_obj)
|
||||
|
||||
# 恢复资产状态
|
||||
asset_obj = asset.get(db, db_obj.asset_id)
|
||||
if asset_obj and asset_obj.status == "maintenance":
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition
|
||||
|
||||
try:
|
||||
# 根据维修前的状态恢复
|
||||
target_status = "in_stock" # 默认恢复为库存中
|
||||
asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=asset_obj.id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status=target_status,
|
||||
remark=f"取消维修: {db_obj.record_code}"
|
||||
),
|
||||
operator_id=db_obj.report_user_id or 0
|
||||
)
|
||||
except Exception as e:
|
||||
# 状态更新失败不影响维修记录取消
|
||||
pass
|
||||
|
||||
return self._load_relations(db, db_obj)
|
||||
|
||||
def delete_record(
|
||||
self,
|
||||
db: Session,
|
||||
record_id: int
|
||||
) -> bool:
|
||||
"""删除维修记录"""
|
||||
db_obj = maintenance_record.get(db, record_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("维修记录")
|
||||
|
||||
# 只能删除待处理或已取消的记录
|
||||
if db_obj.status not in ["pending", "cancelled"]:
|
||||
raise BusinessException("只能删除待处理或已取消的维修记录")
|
||||
|
||||
return maintenance_record.delete(db, record_id)
|
||||
|
||||
def get_asset_records(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 50
|
||||
) -> List:
|
||||
"""获取资产的维修记录"""
|
||||
# 验证资产存在
|
||||
if not asset.get(db, asset_id):
|
||||
raise NotFoundException("资产")
|
||||
|
||||
records = maintenance_record.get_by_asset(db, asset_id, skip, limit)
|
||||
return [self._load_relations(db, record) for record in records]
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session,
|
||||
asset_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取维修统计信息"""
|
||||
return maintenance_record.get_statistics(db, asset_id)
|
||||
|
||||
def _load_relations(
|
||||
self,
|
||||
db: Session,
|
||||
obj
|
||||
) -> Dict[str, Any]:
|
||||
"""加载维修记录关联信息"""
|
||||
from app.models.asset import Asset
|
||||
from app.models.user import User
|
||||
from app.models.brand_supplier import Supplier
|
||||
|
||||
result = {
|
||||
"id": obj.id,
|
||||
"record_code": obj.record_code,
|
||||
"asset_id": obj.asset_id,
|
||||
"asset_code": obj.asset_code,
|
||||
"fault_description": obj.fault_description,
|
||||
"fault_type": obj.fault_type,
|
||||
"report_user_id": obj.report_user_id,
|
||||
"report_time": obj.report_time,
|
||||
"priority": obj.priority,
|
||||
"maintenance_type": obj.maintenance_type,
|
||||
"vendor_id": obj.vendor_id,
|
||||
"maintenance_cost": float(obj.maintenance_cost) if obj.maintenance_cost else None,
|
||||
"start_time": obj.start_time,
|
||||
"complete_time": obj.complete_time,
|
||||
"maintenance_user_id": obj.maintenance_user_id,
|
||||
"maintenance_result": obj.maintenance_result,
|
||||
"replaced_parts": obj.replaced_parts,
|
||||
"status": obj.status,
|
||||
"images": obj.images,
|
||||
"remark": obj.remark,
|
||||
"created_at": obj.created_at,
|
||||
"updated_at": obj.updated_at
|
||||
}
|
||||
|
||||
# 加载资产信息
|
||||
if obj.asset_id:
|
||||
asset_obj = db.query(Asset).filter(Asset.id == obj.asset_id).first()
|
||||
if asset_obj:
|
||||
result["asset"] = {
|
||||
"id": asset_obj.id,
|
||||
"asset_code": asset_obj.asset_code,
|
||||
"asset_name": asset_obj.asset_name,
|
||||
"status": asset_obj.status
|
||||
}
|
||||
|
||||
# 加载报修人信息
|
||||
if obj.report_user_id:
|
||||
report_user = db.query(User).filter(User.id == obj.report_user_id).first()
|
||||
if report_user:
|
||||
result["report_user"] = {
|
||||
"id": report_user.id,
|
||||
"real_name": report_user.real_name,
|
||||
"username": report_user.username
|
||||
}
|
||||
|
||||
# 加载维修人员信息
|
||||
if obj.maintenance_user_id:
|
||||
maintenance_user = db.query(User).filter(User.id == obj.maintenance_user_id).first()
|
||||
if maintenance_user:
|
||||
result["maintenance_user"] = {
|
||||
"id": maintenance_user.id,
|
||||
"real_name": maintenance_user.real_name,
|
||||
"username": maintenance_user.username
|
||||
}
|
||||
|
||||
# 加载供应商信息
|
||||
if obj.vendor_id:
|
||||
vendor = db.query(Supplier).filter(Supplier.id == obj.vendor_id).first()
|
||||
if vendor:
|
||||
result["vendor"] = {
|
||||
"id": vendor.id,
|
||||
"supplier_name": vendor.supplier_name,
|
||||
"contact_person": vendor.contact_person,
|
||||
"contact_phone": vendor.contact_phone
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
async def _generate_record_code(self, db: Session) -> str:
|
||||
"""生成维修单号"""
|
||||
from datetime import datetime
|
||||
import random
|
||||
import string
|
||||
|
||||
# 日期部分
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# 序号部分(4位随机数)
|
||||
sequence = "".join(random.choices(string.digits, k=4))
|
||||
|
||||
# 组合单号: MT202501240001
|
||||
record_code = f"MT{date_str}{sequence}"
|
||||
|
||||
# 检查是否重复,如果重复则重新生成
|
||||
while maintenance_record.get_by_code(db, record_code):
|
||||
sequence = "".join(random.choices(string.digits, k=4))
|
||||
record_code = f"MT{date_str}{sequence}"
|
||||
|
||||
return record_code
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
maintenance_service = MaintenanceService()
|
||||
402
backend_new/app/services/notification_service.py
Normal file
402
backend_new/app/services/notification_service.py
Normal file
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
消息通知服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.crud.notification import notification_crud
|
||||
from app.models.notification import NotificationTemplate
|
||||
from app.models.user import User
|
||||
from app.schemas.notification import (
|
||||
NotificationCreate,
|
||||
NotificationBatchCreate,
|
||||
NotificationSendFromTemplate
|
||||
)
|
||||
import json
|
||||
|
||||
|
||||
class NotificationService:
|
||||
"""消息通知服务类"""
|
||||
|
||||
async def get_notification(self, db: AsyncSession, notification_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取消息通知详情
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
notification_id: 通知ID
|
||||
|
||||
Returns:
|
||||
通知信息
|
||||
"""
|
||||
notification = await notification_crud.get(db, notification_id)
|
||||
if not notification:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": notification.id,
|
||||
"recipient_id": notification.recipient_id,
|
||||
"recipient_name": notification.recipient_name,
|
||||
"title": notification.title,
|
||||
"content": notification.content,
|
||||
"notification_type": notification.notification_type,
|
||||
"priority": notification.priority,
|
||||
"is_read": notification.is_read,
|
||||
"read_at": notification.read_at,
|
||||
"related_entity_type": notification.related_entity_type,
|
||||
"related_entity_id": notification.related_entity_id,
|
||||
"action_url": notification.action_url,
|
||||
"extra_data": notification.extra_data,
|
||||
"sent_via_email": notification.sent_via_email,
|
||||
"sent_via_sms": notification.sent_via_sms,
|
||||
"created_at": notification.created_at,
|
||||
"expire_at": notification.expire_at,
|
||||
}
|
||||
|
||||
async def get_notifications(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
recipient_id: Optional[int] = None,
|
||||
notification_type: Optional[str] = None,
|
||||
priority: Optional[str] = None,
|
||||
is_read: Optional[bool] = None,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取消息通知列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过条数
|
||||
limit: 返回条数
|
||||
recipient_id: 接收人ID
|
||||
notification_type: 通知类型
|
||||
priority: 优先级
|
||||
is_read: 是否已读
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
keyword: 关键词
|
||||
|
||||
Returns:
|
||||
通知列表和总数
|
||||
"""
|
||||
items, total = await notification_crud.get_multi(
|
||||
db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
recipient_id=recipient_id,
|
||||
notification_type=notification_type,
|
||||
priority=priority,
|
||||
is_read=is_read,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": item.id,
|
||||
"recipient_id": item.recipient_id,
|
||||
"recipient_name": item.recipient_name,
|
||||
"title": item.title,
|
||||
"content": item.content,
|
||||
"notification_type": item.notification_type,
|
||||
"priority": item.priority,
|
||||
"is_read": item.is_read,
|
||||
"read_at": item.read_at,
|
||||
"action_url": item.action_url,
|
||||
"created_at": item.created_at,
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
"total": total
|
||||
}
|
||||
|
||||
async def create_notification(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
obj_in: NotificationCreate
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
创建消息通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
obj_in: 创建数据
|
||||
|
||||
Returns:
|
||||
创建的通知信息
|
||||
"""
|
||||
# 获取接收人信息
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == obj_in.recipient_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise ValueError("接收人不存在")
|
||||
|
||||
# 转换为字典
|
||||
obj_in_data = obj_in.model_dump()
|
||||
obj_in_data["recipient_name"] = user.real_name
|
||||
|
||||
# 处理复杂类型
|
||||
if obj_in_data.get("extra_data"):
|
||||
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
|
||||
|
||||
# 设置邮件和短信发送标记
|
||||
obj_in_data["sent_via_email"] = obj_in_data.pop("send_email", False)
|
||||
obj_in_data["sent_via_sms"] = obj_in_data.pop("send_sms", False)
|
||||
|
||||
notification = await notification_crud.create(db, obj_in=obj_in_data)
|
||||
|
||||
# TODO: 发送邮件和短信
|
||||
# if notification.sent_via_email:
|
||||
# await self._send_email(notification)
|
||||
# if notification.sent_via_sms:
|
||||
# await self._send_sms(notification)
|
||||
|
||||
return {
|
||||
"id": notification.id,
|
||||
"recipient_id": notification.recipient_id,
|
||||
"title": notification.title,
|
||||
}
|
||||
|
||||
async def batch_create_notifications(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
batch_in: NotificationBatchCreate
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量创建消息通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
batch_in: 批量创建数据
|
||||
|
||||
Returns:
|
||||
创建结果
|
||||
"""
|
||||
# 获取接收人信息
|
||||
user_results = await db.execute(
|
||||
select(User).where(User.id.in_(batch_in.recipient_ids))
|
||||
)
|
||||
users = {user.id: user.real_name for user in user_results.scalars()}
|
||||
|
||||
# 准备通知数据
|
||||
notification_data = {
|
||||
"title": batch_in.title,
|
||||
"content": batch_in.content,
|
||||
"notification_type": batch_in.notification_type.value,
|
||||
"priority": batch_in.priority.value,
|
||||
"action_url": batch_in.action_url,
|
||||
"extra_data": json.loads(batch_in.extra_data.model_dump_json()) if batch_in.extra_data else {},
|
||||
}
|
||||
|
||||
# 批量创建
|
||||
notifications = await notification_crud.batch_create(
|
||||
db,
|
||||
recipient_ids=batch_in.recipient_ids,
|
||||
notification_data=notification_data
|
||||
)
|
||||
|
||||
# 更新接收人姓名
|
||||
for notification in notifications:
|
||||
notification.recipient_name = users.get(notification.recipient_id, "")
|
||||
|
||||
await db.flush()
|
||||
|
||||
return {
|
||||
"count": len(notifications),
|
||||
"notification_ids": [n.id for n in notifications]
|
||||
}
|
||||
|
||||
async def mark_as_read(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
notification_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
标记为已读
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
notification_id: 通知ID
|
||||
|
||||
Returns:
|
||||
更新结果
|
||||
"""
|
||||
notification = await notification_crud.mark_as_read(
|
||||
db,
|
||||
notification_id=notification_id,
|
||||
read_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
if not notification:
|
||||
raise ValueError("通知不存在")
|
||||
|
||||
return {
|
||||
"id": notification.id,
|
||||
"is_read": notification.is_read,
|
||||
"read_at": notification.read_at
|
||||
}
|
||||
|
||||
async def mark_all_as_read(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
recipient_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
标记所有未读为已读
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
recipient_id: 接收人ID
|
||||
|
||||
Returns:
|
||||
更新结果
|
||||
"""
|
||||
count = await notification_crud.mark_all_as_read(
|
||||
db,
|
||||
recipient_id=recipient_id,
|
||||
read_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"message": f"已标记 {count} 条通知为已读"
|
||||
}
|
||||
|
||||
async def delete_notification(self, db: AsyncSession, notification_id: int) -> None:
|
||||
"""
|
||||
删除消息通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
notification_id: 通知ID
|
||||
"""
|
||||
await notification_crud.delete(db, notification_id=notification_id)
|
||||
|
||||
async def batch_delete_notifications(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
notification_ids: List[int]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量删除通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
notification_ids: 通知ID列表
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
count = await notification_crud.batch_delete(db, notification_ids=notification_ids)
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"message": f"已删除 {count} 条通知"
|
||||
}
|
||||
|
||||
async def get_unread_count(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取未读通知数量
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
recipient_id: 接收人ID
|
||||
|
||||
Returns:
|
||||
未读数量
|
||||
"""
|
||||
count = await notification_crud.get_unread_count(db, recipient_id)
|
||||
|
||||
return {"unread_count": count}
|
||||
|
||||
async def get_statistics(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取通知统计信息
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
recipient_id: 接收人ID
|
||||
|
||||
Returns:
|
||||
统计信息
|
||||
"""
|
||||
return await notification_crud.get_statistics(db, recipient_id)
|
||||
|
||||
async def send_from_template(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
template_in: NotificationSendFromTemplate
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
从模板发送通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
template_in: 模板发送数据
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
"""
|
||||
# 获取模板
|
||||
result = await db.execute(
|
||||
select(NotificationTemplate).where(
|
||||
and_(
|
||||
NotificationTemplate.template_code == template_in.template_code,
|
||||
NotificationTemplate.is_active == True
|
||||
)
|
||||
)
|
||||
)
|
||||
template = result.scalar_one_or_none()
|
||||
if not template:
|
||||
raise ValueError(f"通知模板 {template_in.template_code} 不存在或未启用")
|
||||
|
||||
# 渲染标题和内容
|
||||
title = self._render_template(template.title_template, template_in.variables)
|
||||
content = self._render_template(template.content_template, template_in.variables)
|
||||
|
||||
# 创建批量通知数据
|
||||
batch_data = NotificationBatchCreate(
|
||||
recipient_ids=template_in.recipient_ids,
|
||||
title=title,
|
||||
content=content,
|
||||
notification_type=template.notification_type,
|
||||
priority=template.priority,
|
||||
action_url=template_in.action_url,
|
||||
extra_data={
|
||||
"template_code": template.template_code,
|
||||
"variables": template_in.variables
|
||||
}
|
||||
)
|
||||
|
||||
return await self.batch_create_notifications(db, batch_data)
|
||||
|
||||
def _render_template(self, template: str, variables: Dict[str, Any]) -> str:
|
||||
"""
|
||||
渲染模板
|
||||
|
||||
Args:
|
||||
template: 模板字符串
|
||||
variables: 变量字典
|
||||
|
||||
Returns:
|
||||
渲染后的字符串
|
||||
"""
|
||||
try:
|
||||
return template.format(**variables)
|
||||
except KeyError as e:
|
||||
raise ValueError(f"模板变量缺失: {e}")
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
notification_service = NotificationService()
|
||||
270
backend_new/app/services/operation_log_service.py
Normal file
270
backend_new/app/services/operation_log_service.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
操作日志服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.crud.operation_log import operation_log_crud
|
||||
from app.schemas.operation_log import OperationLogCreate
|
||||
|
||||
|
||||
class OperationLogService:
|
||||
"""操作日志服务类"""
|
||||
|
||||
async def get_log(self, db: AsyncSession, log_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取操作日志详情
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
log_id: 日志ID
|
||||
|
||||
Returns:
|
||||
日志信息
|
||||
"""
|
||||
log = await operation_log_crud.get(db, log_id)
|
||||
if not log:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": log.id,
|
||||
"operator_id": log.operator_id,
|
||||
"operator_name": log.operator_name,
|
||||
"operator_ip": log.operator_ip,
|
||||
"module": log.module,
|
||||
"operation_type": log.operation_type,
|
||||
"method": log.method,
|
||||
"url": log.url,
|
||||
"params": log.params,
|
||||
"result": log.result,
|
||||
"error_msg": log.error_msg,
|
||||
"duration": log.duration,
|
||||
"user_agent": log.user_agent,
|
||||
"extra_data": log.extra_data,
|
||||
"created_at": log.created_at,
|
||||
}
|
||||
|
||||
async def get_logs(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
operator_id: Optional[int] = None,
|
||||
operator_name: Optional[str] = None,
|
||||
module: Optional[str] = None,
|
||||
operation_type: Optional[str] = None,
|
||||
result: Optional[str] = None,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取操作日志列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过条数
|
||||
limit: 返回条数
|
||||
operator_id: 操作人ID
|
||||
operator_name: 操作人姓名
|
||||
module: 模块名称
|
||||
operation_type: 操作类型
|
||||
result: 操作结果
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
keyword: 关键词
|
||||
|
||||
Returns:
|
||||
日志列表和总数
|
||||
"""
|
||||
items, total = await operation_log_crud.get_multi(
|
||||
db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
operator_id=operator_id,
|
||||
operator_name=operator_name,
|
||||
module=module,
|
||||
operation_type=operation_type,
|
||||
result=result,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": item.id,
|
||||
"operator_id": item.operator_id,
|
||||
"operator_name": item.operator_name,
|
||||
"operator_ip": item.operator_ip,
|
||||
"module": item.module,
|
||||
"operation_type": item.operation_type,
|
||||
"method": item.method,
|
||||
"url": item.url,
|
||||
"result": item.result,
|
||||
"error_msg": item.error_msg,
|
||||
"duration": item.duration,
|
||||
"created_at": item.created_at,
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
"total": total
|
||||
}
|
||||
|
||||
async def create_log(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
obj_in: OperationLogCreate
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
创建操作日志
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
obj_in: 创建数据
|
||||
|
||||
Returns:
|
||||
创建的日志信息
|
||||
"""
|
||||
import json
|
||||
|
||||
# 转换为字典
|
||||
obj_in_data = obj_in.model_dump()
|
||||
|
||||
# 处理复杂类型
|
||||
if obj_in_data.get("extra_data"):
|
||||
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
|
||||
|
||||
log = await operation_log_crud.create(db, obj_in=obj_in_data)
|
||||
|
||||
return {
|
||||
"id": log.id,
|
||||
"operator_name": log.operator_name,
|
||||
"module": log.module,
|
||||
"operation_type": log.operation_type,
|
||||
}
|
||||
|
||||
async def get_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取操作日志统计信息
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
统计信息
|
||||
"""
|
||||
return await operation_log_crud.get_statistics(
|
||||
db,
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
)
|
||||
|
||||
async def get_operator_top(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
limit: int = 10,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取操作排行榜
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
limit: 返回条数
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
操作排行列表
|
||||
"""
|
||||
return await operation_log_crud.get_operator_top(
|
||||
db,
|
||||
limit=limit,
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
)
|
||||
|
||||
async def delete_old_logs(self, db: AsyncSession, *, days: int = 90) -> Dict[str, Any]:
|
||||
"""
|
||||
删除旧日志
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
days: 保留天数
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
count = await operation_log_crud.delete_old_logs(db, days=days)
|
||||
return {
|
||||
"deleted_count": count,
|
||||
"message": f"已删除 {count} 条 {days} 天前的日志"
|
||||
}
|
||||
|
||||
async def export_logs(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
operator_id: Optional[int] = None,
|
||||
module: Optional[str] = None,
|
||||
operation_type: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
导出操作日志
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
operator_id: 操作人ID
|
||||
module: 模块名称
|
||||
operation_type: 操作类型
|
||||
|
||||
Returns:
|
||||
日志列表
|
||||
"""
|
||||
items, total = await operation_log_crud.get_multi(
|
||||
db,
|
||||
skip=0,
|
||||
limit=10000, # 导出限制
|
||||
operator_id=operator_id,
|
||||
module=module,
|
||||
operation_type=operation_type,
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"操作人": item.operator_name,
|
||||
"模块": item.module,
|
||||
"操作类型": item.operation_type,
|
||||
"请求方法": item.method,
|
||||
"请求URL": item.url,
|
||||
"操作结果": item.result,
|
||||
"错误信息": item.error_msg or "",
|
||||
"执行时长(毫秒)": item.duration or 0,
|
||||
"操作时间": item.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"操作IP": item.operator_ip or "",
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
operation_log_service = OperationLogService()
|
||||
245
backend_new/app/services/organization_service.py
Normal file
245
backend_new/app/services/organization_service.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
机构网点业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from app.crud.organization import organization
|
||||
from app.schemas.organization import OrganizationCreate, OrganizationUpdate
|
||||
from app.core.exceptions import NotFoundException, AlreadyExistsException
|
||||
|
||||
|
||||
class OrganizationService:
|
||||
"""机构网点服务类"""
|
||||
|
||||
def get_organization(self, db: Session, org_id: int):
|
||||
"""
|
||||
获取机构详情
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
org_id: 机构ID
|
||||
|
||||
Returns:
|
||||
机构对象
|
||||
|
||||
Raises:
|
||||
NotFoundException: 机构不存在
|
||||
"""
|
||||
obj = organization.get(db, org_id)
|
||||
if not obj:
|
||||
raise NotFoundException("机构")
|
||||
return obj
|
||||
|
||||
def get_organizations(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
org_type: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> Tuple[List, int]:
|
||||
"""
|
||||
获取机构列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过条数
|
||||
limit: 返回条数
|
||||
org_type: 机构类型
|
||||
status: 状态
|
||||
keyword: 搜索关键词
|
||||
|
||||
Returns:
|
||||
(机构列表, 总数)
|
||||
"""
|
||||
return organization.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
org_type=org_type,
|
||||
status=status,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
def get_organization_tree(
|
||||
self,
|
||||
db: Session,
|
||||
status: Optional[str] = None
|
||||
) -> List:
|
||||
"""
|
||||
获取机构树
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
status: 状态筛选
|
||||
|
||||
Returns:
|
||||
机构树列表
|
||||
"""
|
||||
return organization.get_tree(db, status)
|
||||
|
||||
def get_organization_children(
|
||||
self,
|
||||
db: Session,
|
||||
parent_id: int
|
||||
) -> List:
|
||||
"""
|
||||
获取直接子机构
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
parent_id: 父机构ID
|
||||
|
||||
Returns:
|
||||
子机构列表
|
||||
|
||||
Raises:
|
||||
NotFoundException: 父机构不存在
|
||||
"""
|
||||
if parent_id > 0 and not organization.get(db, parent_id):
|
||||
raise NotFoundException("父机构")
|
||||
|
||||
return organization.get_children(db, parent_id)
|
||||
|
||||
def get_all_children(
|
||||
self,
|
||||
db: Session,
|
||||
parent_id: int
|
||||
) -> List:
|
||||
"""
|
||||
递归获取所有子机构
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
parent_id: 父机构ID
|
||||
|
||||
Returns:
|
||||
所有子机构列表
|
||||
|
||||
Raises:
|
||||
NotFoundException: 父机构不存在
|
||||
"""
|
||||
if not organization.get(db, parent_id):
|
||||
raise NotFoundException("机构")
|
||||
|
||||
return organization.get_all_children(db, parent_id)
|
||||
|
||||
def get_parents(
|
||||
self,
|
||||
db: Session,
|
||||
child_id: int
|
||||
) -> List:
|
||||
"""
|
||||
递归获取所有父机构
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
child_id: 子机构ID
|
||||
|
||||
Returns:
|
||||
所有父机构列表
|
||||
|
||||
Raises:
|
||||
NotFoundException: 机构不存在
|
||||
"""
|
||||
if not organization.get(db, child_id):
|
||||
raise NotFoundException("机构")
|
||||
|
||||
return organization.get_parents(db, child_id)
|
||||
|
||||
def create_organization(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: OrganizationCreate,
|
||||
creator_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
创建机构
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
obj_in: 创建数据
|
||||
creator_id: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的机构对象
|
||||
|
||||
Raises:
|
||||
AlreadyExistsException: 机构代码已存在
|
||||
NotFoundException: 父机构不存在
|
||||
"""
|
||||
try:
|
||||
return organization.create(db, obj_in, creator_id)
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise NotFoundException("父机构") from e
|
||||
raise AlreadyExistsException("机构") from e
|
||||
|
||||
def update_organization(
|
||||
self,
|
||||
db: Session,
|
||||
org_id: int,
|
||||
obj_in: OrganizationUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
更新机构
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
org_id: 机构ID
|
||||
obj_in: 更新数据
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新后的机构对象
|
||||
|
||||
Raises:
|
||||
NotFoundException: 机构不存在
|
||||
"""
|
||||
db_obj = organization.get(db, org_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("机构")
|
||||
|
||||
try:
|
||||
return organization.update(db, db_obj, obj_in, updater_id)
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise NotFoundException("父机构") from e
|
||||
raise
|
||||
|
||||
def delete_organization(
|
||||
self,
|
||||
db: Session,
|
||||
org_id: int,
|
||||
deleter_id: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
删除机构
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
org_id: 机构ID
|
||||
deleter_id: 删除人ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
|
||||
Raises:
|
||||
NotFoundException: 机构不存在
|
||||
ValueError: 机构下存在子机构
|
||||
"""
|
||||
if not organization.get(db, org_id):
|
||||
raise NotFoundException("机构")
|
||||
|
||||
try:
|
||||
return organization.delete(db, org_id, deleter_id)
|
||||
except ValueError as e:
|
||||
if "子机构" in str(e):
|
||||
raise ValueError("该机构下存在子机构,无法删除") from e
|
||||
raise
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
organization_service = OrganizationService()
|
||||
409
backend_new/app/services/recovery_service.py
Normal file
409
backend_new/app/services/recovery_service.py
Normal file
@@ -0,0 +1,409 @@
|
||||
"""
|
||||
资产回收业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from app.crud.recovery import recovery_order, recovery_item
|
||||
from app.crud.asset import asset
|
||||
from app.schemas.recovery import (
|
||||
AssetRecoveryOrderCreate,
|
||||
AssetRecoveryOrderUpdate
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, BusinessException
|
||||
|
||||
|
||||
class RecoveryService:
|
||||
"""资产回收服务类"""
|
||||
|
||||
async def get_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""获取回收单详情"""
|
||||
# 使用selectinload预加载关联数据,避免N+1查询
|
||||
from app.models.recovery import AssetRecoveryOrder
|
||||
from app.models.user import User
|
||||
from app.models.recovery import AssetRecoveryItem
|
||||
|
||||
obj = db.query(
|
||||
AssetRecoveryOrder
|
||||
).options(
|
||||
selectinload(AssetRecoveryOrder.items),
|
||||
selectinload(AssetRecoveryOrder.applicant.of_type(User)),
|
||||
selectinload(AssetRecoveryOrder.approver.of_type(User)),
|
||||
selectinload(AssetRecoveryOrder.executor.of_type(User))
|
||||
).filter(
|
||||
AssetRecoveryOrder.id == order_id
|
||||
).first()
|
||||
|
||||
if not obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 加载关联信息
|
||||
return self._load_order_relations(db, obj)
|
||||
|
||||
def get_orders(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
recovery_type: Optional[str] = None,
|
||||
approval_status: Optional[str] = None,
|
||||
execute_status: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> tuple:
|
||||
"""获取回收单列表"""
|
||||
items, total = recovery_order.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
recovery_type=recovery_type,
|
||||
approval_status=approval_status,
|
||||
execute_status=execute_status,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
# 加载关联信息
|
||||
items_with_relations = [self._load_order_relations(db, item) for item in items]
|
||||
|
||||
return items_with_relations, total
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: AssetRecoveryOrderCreate,
|
||||
apply_user_id: int
|
||||
):
|
||||
"""创建回收单"""
|
||||
# 验证资产存在性和状态
|
||||
assets = []
|
||||
for asset_id in obj_in.asset_ids:
|
||||
asset_obj = asset.get(db, asset_id)
|
||||
if not asset_obj:
|
||||
raise NotFoundException(f"资产ID {asset_id}")
|
||||
assets.append(asset_obj)
|
||||
|
||||
# 验证资产状态是否允许回收
|
||||
for asset_obj in assets:
|
||||
if not self._can_recover(asset_obj.status, obj_in.recovery_type):
|
||||
raise BusinessException(
|
||||
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许{self._get_recovery_type_name(obj_in.recovery_type)}操作"
|
||||
)
|
||||
|
||||
# 生成回收单号
|
||||
order_code = await self._generate_order_code(db)
|
||||
|
||||
# 创建回收单
|
||||
db_obj = recovery_order.create(
|
||||
db=db,
|
||||
obj_in=obj_in,
|
||||
order_code=order_code,
|
||||
apply_user_id=apply_user_id
|
||||
)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def update_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
obj_in: AssetRecoveryOrderUpdate
|
||||
):
|
||||
"""更新回收单"""
|
||||
db_obj = recovery_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 只有待审批状态可以更新
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("只有待审批状态的回收单可以更新")
|
||||
|
||||
return recovery_order.update(db, db_obj, obj_in)
|
||||
|
||||
def approve_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
approval_status: str,
|
||||
approval_user_id: int,
|
||||
approval_remark: Optional[str] = None
|
||||
):
|
||||
"""审批回收单"""
|
||||
db_obj = recovery_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("该回收单已审批,无法重复审批")
|
||||
|
||||
# 审批
|
||||
db_obj = recovery_order.approve(
|
||||
db=db,
|
||||
db_obj=db_obj,
|
||||
approval_status=approval_status,
|
||||
approval_user_id=approval_user_id,
|
||||
approval_remark=approval_remark
|
||||
)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def start_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
execute_user_id: int
|
||||
):
|
||||
"""开始回收"""
|
||||
db_obj = recovery_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "approved":
|
||||
raise BusinessException("该回收单未审批通过,无法开始执行")
|
||||
if db_obj.execute_status != "pending":
|
||||
raise BusinessException("该回收单已开始或已完成")
|
||||
|
||||
# 开始回收
|
||||
db_obj = recovery_order.start(db, db_obj, execute_user_id)
|
||||
|
||||
# 更新明细状态为回收中
|
||||
recovery_item.batch_update_recovery_status(db, order_id, "recovering")
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
async def complete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
execute_user_id: int
|
||||
):
|
||||
"""完成回收"""
|
||||
db_obj = recovery_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status not in ["pending", "executing"]:
|
||||
raise BusinessException("该回收单状态不允许完成操作")
|
||||
|
||||
# 完成回收单
|
||||
db_obj = recovery_order.complete(db, db_obj, execute_user_id)
|
||||
|
||||
# 更新资产状态
|
||||
await self._execute_recovery_logic(db, db_obj)
|
||||
|
||||
# 更新明细状态为完成
|
||||
recovery_item.batch_update_recovery_status(db, order_id, "completed")
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def cancel_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""取消回收单"""
|
||||
db_obj = recovery_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status == "completed":
|
||||
raise BusinessException("已完成的回收单无法取消")
|
||||
|
||||
recovery_order.cancel(db, db_obj)
|
||||
return True
|
||||
|
||||
def delete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""删除回收单"""
|
||||
db_obj = recovery_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
# 只有已取消或已拒绝的可以删除
|
||||
if db_obj.approval_status not in ["rejected", "cancelled"]:
|
||||
raise BusinessException("只能删除已拒绝或已取消的回收单")
|
||||
|
||||
return recovery_order.delete(db, order_id)
|
||||
|
||||
def get_order_items(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> List:
|
||||
"""获取回收单明细"""
|
||||
# 验证回收单存在
|
||||
if not recovery_order.get(db, order_id):
|
||||
raise NotFoundException("回收单")
|
||||
|
||||
return recovery_item.get_by_order(db, order_id)
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session
|
||||
) -> Dict[str, int]:
|
||||
"""获取回收单统计信息"""
|
||||
return recovery_order.get_statistics(db)
|
||||
|
||||
async def _execute_recovery_logic(
|
||||
self,
|
||||
db: Session,
|
||||
order_obj
|
||||
):
|
||||
"""执行回收逻辑(完成回收时自动执行)"""
|
||||
# 获取明细
|
||||
items = recovery_item.get_by_order(db, order_obj.id)
|
||||
|
||||
# 更新资产状态
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# 根据回收类型确定目标状态
|
||||
if order_obj.recovery_type == "scrap":
|
||||
target_status = "scrapped"
|
||||
remark = f"报废回收: {order_obj.order_code}"
|
||||
else:
|
||||
target_status = "in_stock"
|
||||
remark = f"资产回收: {order_obj.order_code}"
|
||||
|
||||
# 变更资产状态
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=item.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status=target_status,
|
||||
remark=remark
|
||||
),
|
||||
operator_id=order_obj.execute_user_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 记录失败日志
|
||||
print(f"回收资产 {item.asset_code} 失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def _load_order_relations(
|
||||
self,
|
||||
db: Session,
|
||||
obj
|
||||
) -> Dict[str, Any]:
|
||||
"""加载回收单关联信息"""
|
||||
from app.models.user import User
|
||||
|
||||
result = {
|
||||
"id": obj.id,
|
||||
"order_code": obj.order_code,
|
||||
"recovery_type": obj.recovery_type,
|
||||
"title": obj.title,
|
||||
"asset_count": obj.asset_count,
|
||||
"apply_user_id": obj.apply_user_id,
|
||||
"apply_time": obj.apply_time,
|
||||
"approval_status": obj.approval_status,
|
||||
"approval_user_id": obj.approval_user_id,
|
||||
"approval_time": obj.approval_time,
|
||||
"approval_remark": obj.approval_remark,
|
||||
"execute_status": obj.execute_status,
|
||||
"execute_user_id": obj.execute_user_id,
|
||||
"execute_time": obj.execute_time,
|
||||
"remark": obj.remark,
|
||||
"created_at": obj.created_at,
|
||||
"updated_at": obj.updated_at
|
||||
}
|
||||
|
||||
# 加载申请人
|
||||
if obj.apply_user_id:
|
||||
apply_user = db.query(User).filter(User.id == obj.apply_user_id).first()
|
||||
if apply_user:
|
||||
result["apply_user"] = {
|
||||
"id": apply_user.id,
|
||||
"real_name": apply_user.real_name,
|
||||
"username": apply_user.username
|
||||
}
|
||||
|
||||
# 加载审批人
|
||||
if obj.approval_user_id:
|
||||
approval_user = db.query(User).filter(User.id == obj.approval_user_id).first()
|
||||
if approval_user:
|
||||
result["approval_user"] = {
|
||||
"id": approval_user.id,
|
||||
"real_name": approval_user.real_name,
|
||||
"username": approval_user.username
|
||||
}
|
||||
|
||||
# 加载执行人
|
||||
if obj.execute_user_id:
|
||||
execute_user = db.query(User).filter(User.id == obj.execute_user_id).first()
|
||||
if execute_user:
|
||||
result["execute_user"] = {
|
||||
"id": execute_user.id,
|
||||
"real_name": execute_user.real_name,
|
||||
"username": execute_user.username
|
||||
}
|
||||
|
||||
# 加载明细
|
||||
items = recovery_item.get_by_order(db, obj.id)
|
||||
result["items"] = [
|
||||
{
|
||||
"id": item.id,
|
||||
"asset_id": item.asset_id,
|
||||
"asset_code": item.asset_code,
|
||||
"recovery_status": item.recovery_status
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
def _can_recover(self, asset_status: str, recovery_type: str) -> bool:
|
||||
"""判断资产是否可以回收"""
|
||||
# 使用中的资产可以回收
|
||||
if recovery_type in ["user", "org"]:
|
||||
return asset_status == "in_use"
|
||||
# 报废回收可以使用中或维修中的资产
|
||||
elif recovery_type == "scrap":
|
||||
return asset_status in ["in_use", "maintenance", "in_stock"]
|
||||
return False
|
||||
|
||||
def _get_recovery_type_name(self, recovery_type: str) -> str:
|
||||
"""获取回收类型中文名"""
|
||||
type_names = {
|
||||
"user": "使用人回收",
|
||||
"org": "机构回收",
|
||||
"scrap": "报废回收"
|
||||
}
|
||||
return type_names.get(recovery_type, "回收")
|
||||
|
||||
async def _generate_order_code(self, db: Session) -> str:
|
||||
"""生成回收单号"""
|
||||
from datetime import datetime
|
||||
import random
|
||||
import string
|
||||
|
||||
# 日期部分
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# 序号部分(5位随机数)
|
||||
sequence = "".join(random.choices(string.digits, k=5))
|
||||
|
||||
# 组合单号: RO-20250124-00001
|
||||
order_code = f"RO-{date_str}-{sequence}"
|
||||
|
||||
# 检查是否重复,如果重复则重新生成
|
||||
while recovery_order.get_by_code(db, order_code):
|
||||
sequence = "".join(random.choices(string.digits, k=5))
|
||||
order_code = f"RO-{date_str}-{sequence}"
|
||||
|
||||
return order_code
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
recovery_service = RecoveryService()
|
||||
166
backend_new/app/services/state_machine_service.py
Normal file
166
backend_new/app/services/state_machine_service.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
资产状态机服务
|
||||
定义资产状态的转换规则和验证
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AssetStatus(str, Enum):
|
||||
"""资产状态枚举"""
|
||||
PENDING = "pending" # 待入库
|
||||
IN_STOCK = "in_stock" # 库存中
|
||||
IN_USE = "in_use" # 使用中
|
||||
TRANSFERRING = "transferring" # 调拨中
|
||||
MAINTENANCE = "maintenance" # 维修中
|
||||
PENDING_SCRAP = "pending_scrap" # 待报废
|
||||
SCRAPPED = "scrapped" # 已报废
|
||||
LOST = "lost" # 已丢失
|
||||
|
||||
|
||||
class StateMachineService:
|
||||
"""状态机服务类"""
|
||||
|
||||
# 状态转换规则
|
||||
TRANSITIONS: Dict[str, List[str]] = {
|
||||
AssetStatus.PENDING: [
|
||||
AssetStatus.IN_STOCK,
|
||||
AssetStatus.PENDING_SCRAP,
|
||||
],
|
||||
AssetStatus.IN_STOCK: [
|
||||
AssetStatus.IN_USE,
|
||||
AssetStatus.TRANSFERRING,
|
||||
AssetStatus.MAINTENANCE,
|
||||
AssetStatus.PENDING_SCRAP,
|
||||
AssetStatus.LOST,
|
||||
],
|
||||
AssetStatus.IN_USE: [
|
||||
AssetStatus.IN_STOCK,
|
||||
AssetStatus.TRANSFERRING,
|
||||
AssetStatus.MAINTENANCE,
|
||||
AssetStatus.PENDING_SCRAP,
|
||||
AssetStatus.LOST,
|
||||
],
|
||||
AssetStatus.TRANSFERRING: [
|
||||
AssetStatus.IN_STOCK,
|
||||
AssetStatus.IN_USE,
|
||||
],
|
||||
AssetStatus.MAINTENANCE: [
|
||||
AssetStatus.IN_STOCK,
|
||||
AssetStatus.IN_USE,
|
||||
AssetStatus.PENDING_SCRAP,
|
||||
],
|
||||
AssetStatus.PENDING_SCRAP: [
|
||||
AssetStatus.SCRAPPED,
|
||||
AssetStatus.IN_STOCK, # 取消报废
|
||||
],
|
||||
AssetStatus.SCRAPPED: [], # 终态,不可转换
|
||||
AssetStatus.LOST: [], # 终态,不可转换
|
||||
}
|
||||
|
||||
# 状态显示名称
|
||||
STATUS_NAMES: Dict[str, str] = {
|
||||
AssetStatus.PENDING: "待入库",
|
||||
AssetStatus.IN_STOCK: "库存中",
|
||||
AssetStatus.IN_USE: "使用中",
|
||||
AssetStatus.TRANSFERRING: "调拨中",
|
||||
AssetStatus.MAINTENANCE: "维修中",
|
||||
AssetStatus.PENDING_SCRAP: "待报废",
|
||||
AssetStatus.SCRAPPED: "已报废",
|
||||
AssetStatus.LOST: "已丢失",
|
||||
}
|
||||
|
||||
def can_transition(self, current_status: str, target_status: str) -> bool:
|
||||
"""
|
||||
检查状态是否可以转换
|
||||
|
||||
Args:
|
||||
current_status: 当前状态
|
||||
target_status: 目标状态
|
||||
|
||||
Returns:
|
||||
是否可以转换
|
||||
"""
|
||||
allowed_transitions = self.TRANSITIONS.get(current_status, [])
|
||||
return target_status in allowed_transitions
|
||||
|
||||
def validate_transition(
|
||||
self,
|
||||
current_status: str,
|
||||
target_status: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
验证状态转换并返回错误信息
|
||||
|
||||
Args:
|
||||
current_status: 当前状态
|
||||
target_status: 目标状态
|
||||
|
||||
Returns:
|
||||
错误信息,如果转换有效则返回None
|
||||
"""
|
||||
if current_status == target_status:
|
||||
return "当前状态与目标状态相同"
|
||||
|
||||
if current_status not in self.TRANSITIONS:
|
||||
return f"无效的当前状态: {current_status}"
|
||||
|
||||
if target_status not in self.TRANSITIONS:
|
||||
return f"无效的目标状态: {target_status}"
|
||||
|
||||
if not self.can_transition(current_status, target_status):
|
||||
return f"无法从状态 '{self.get_status_name(current_status)}' 转换到 '{self.get_status_name(target_status)}'"
|
||||
|
||||
return None
|
||||
|
||||
def get_status_name(self, status: str) -> str:
|
||||
"""
|
||||
获取状态的显示名称
|
||||
|
||||
Args:
|
||||
status: 状态值
|
||||
|
||||
Returns:
|
||||
状态显示名称
|
||||
"""
|
||||
return self.STATUS_NAMES.get(status, status)
|
||||
|
||||
def get_allowed_transitions(self, current_status: str) -> List[str]:
|
||||
"""
|
||||
获取允许的转换状态列表
|
||||
|
||||
Args:
|
||||
current_status: 当前状态
|
||||
|
||||
Returns:
|
||||
允许转换到的状态列表
|
||||
"""
|
||||
return self.TRANSITIONS.get(current_status, [])
|
||||
|
||||
def is_terminal_state(self, status: str) -> bool:
|
||||
"""
|
||||
判断是否为终态
|
||||
|
||||
Args:
|
||||
status: 状态值
|
||||
|
||||
Returns:
|
||||
是否为终态
|
||||
"""
|
||||
return len(self.TRANSITIONS.get(status, [])) == 0
|
||||
|
||||
def get_available_statuses(self) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取所有可用状态列表
|
||||
|
||||
Returns:
|
||||
状态列表,每个状态包含value和name
|
||||
"""
|
||||
return [
|
||||
{"value": status, "name": name}
|
||||
for status, name in self.STATUS_NAMES.items()
|
||||
]
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
state_machine_service = StateMachineService()
|
||||
546
backend_new/app/services/statistics_service.py
Normal file
546
backend_new/app/services/statistics_service.py
Normal file
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
统计分析服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import select, func, and_, or_, case, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.asset import Asset
|
||||
from app.models.allocation import AssetAllocationOrder
|
||||
from app.models.maintenance import MaintenanceRecord
|
||||
from app.models.organization import Organization
|
||||
from app.models.brand_supplier import Supplier
|
||||
from app.models.device_type import DeviceType
|
||||
|
||||
|
||||
class StatisticsService:
|
||||
"""统计分析服务类"""
|
||||
|
||||
async def get_overview(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取总览统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
总览统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 资产总数
|
||||
total_query = select(func.count(Asset.id))
|
||||
if where_clause:
|
||||
total_query = total_query.where(where_clause)
|
||||
total_result = await db.execute(total_query)
|
||||
total_assets = total_result.scalar() or 0
|
||||
|
||||
# 资产总价值
|
||||
value_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
|
||||
if where_clause:
|
||||
value_query = value_query.where(where_clause)
|
||||
value_result = await db.execute(value_query)
|
||||
total_value = value_result.scalar() or Decimal("0")
|
||||
|
||||
# 各状态数量
|
||||
status_query = select(
|
||||
Asset.status,
|
||||
func.count(Asset.id).label('count')
|
||||
).group_by(Asset.status)
|
||||
if where_clause:
|
||||
status_query = status_query.where(where_clause)
|
||||
status_result = await db.execute(status_query)
|
||||
|
||||
status_counts = {row[0]: row[1] for row in status_result}
|
||||
|
||||
# 今日和本月采购数量
|
||||
today = datetime.utcnow().date()
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
month_start = datetime(today.year, today.month, 1)
|
||||
|
||||
today_query = select(func.count(Asset.id)).where(Asset.created_at >= today_start)
|
||||
if where_clause:
|
||||
today_query = today_query.where(Asset.organization_id == organization_id)
|
||||
today_result = await db.execute(today_query)
|
||||
today_purchase_count = today_result.scalar() or 0
|
||||
|
||||
month_query = select(func.count(Asset.id)).where(Asset.created_at >= month_start)
|
||||
if where_clause:
|
||||
month_query = month_query.where(Asset.organization_id == organization_id)
|
||||
month_result = await db.execute(month_query)
|
||||
this_month_purchase_count = month_result.scalar() or 0
|
||||
|
||||
# 机构网点数
|
||||
org_query = select(func.count(Organization.id))
|
||||
org_result = await db.execute(org_query)
|
||||
organization_count = org_result.scalar() or 0
|
||||
|
||||
# 供应商数
|
||||
supplier_query = select(func.count(Supplier.id))
|
||||
supplier_result = await db.execute(supplier_query)
|
||||
supplier_count = supplier_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"total_assets": total_assets,
|
||||
"total_value": float(total_value),
|
||||
"in_stock_count": status_counts.get("in_stock", 0),
|
||||
"in_use_count": status_counts.get("in_use", 0),
|
||||
"maintenance_count": status_counts.get("maintenance", 0),
|
||||
"scrapped_count": status_counts.get("scrapped", 0) + status_counts.get("pending_scrap", 0),
|
||||
"today_purchase_count": today_purchase_count,
|
||||
"this_month_purchase_count": this_month_purchase_count,
|
||||
"organization_count": organization_count,
|
||||
"supplier_count": supplier_count,
|
||||
}
|
||||
|
||||
async def get_purchase_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取采购统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
采购统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
|
||||
if start_date:
|
||||
conditions.append(Asset.purchase_date >= start_date)
|
||||
if end_date:
|
||||
conditions.append(Asset.purchase_date <= end_date)
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总采购数量和金额
|
||||
count_query = select(func.count(Asset.id))
|
||||
value_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
|
||||
|
||||
if where_clause:
|
||||
count_query = count_query.where(where_clause)
|
||||
value_query = value_query.where(where_clause)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
value_result = await db.execute(value_query)
|
||||
|
||||
total_purchase_count = count_result.scalar() or 0
|
||||
total_purchase_value = value_result.scalar() or Decimal("0")
|
||||
|
||||
# 月度趋势
|
||||
monthly_query = select(
|
||||
func.to_char(Asset.purchase_date, 'YYYY-MM').label('month'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).group_by('month').order_by('month')
|
||||
|
||||
if where_clause:
|
||||
monthly_query = monthly_query.where(where_clause)
|
||||
|
||||
monthly_result = await db.execute(monthly_query)
|
||||
monthly_trend = [
|
||||
{
|
||||
"month": row[0],
|
||||
"count": row[1],
|
||||
"value": float(row[2]) if row[2] else 0
|
||||
}
|
||||
for row in monthly_result
|
||||
]
|
||||
|
||||
# 供应商分布
|
||||
supplier_query = select(
|
||||
Supplier.id.label('supplier_id'),
|
||||
Supplier.name.label('supplier_name'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).join(
|
||||
Asset, Asset.supplier_id == Supplier.id
|
||||
).group_by(
|
||||
Supplier.id, Supplier.name
|
||||
).order_by(func.count(Asset.id).desc())
|
||||
|
||||
if where_clause:
|
||||
supplier_query = supplier_query.where(
|
||||
and_(*[c for c in conditions if not any(x in str(c) for x in ['organization_id'])])
|
||||
)
|
||||
|
||||
supplier_result = await db.execute(supplier_query)
|
||||
supplier_distribution = [
|
||||
{
|
||||
"supplier_id": row[0],
|
||||
"supplier_name": row[1],
|
||||
"count": row[2],
|
||||
"value": float(row[3]) if row[3] else 0
|
||||
}
|
||||
for row in supplier_result
|
||||
]
|
||||
|
||||
return {
|
||||
"total_purchase_count": total_purchase_count,
|
||||
"total_purchase_value": float(total_purchase_value),
|
||||
"monthly_trend": monthly_trend,
|
||||
"supplier_distribution": supplier_distribution,
|
||||
"category_distribution": [],
|
||||
}
|
||||
|
||||
async def get_depreciation_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取折旧统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
折旧统计数据
|
||||
"""
|
||||
# 简化实现,实际需要根据折旧规则计算
|
||||
return {
|
||||
"total_depreciation_value": 0.0,
|
||||
"average_depreciation_rate": 0.05,
|
||||
"depreciation_by_category": [],
|
||||
"assets_near_end_life": [],
|
||||
}
|
||||
|
||||
async def get_value_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取价值统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
价值统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总价值
|
||||
total_query = select(func.coalesce(func.sum(Asset.purchase_price), 0))
|
||||
if where_clause:
|
||||
total_query = total_query.where(where_clause)
|
||||
total_result = await db.execute(total_query)
|
||||
total_value = total_result.scalar() or Decimal("0")
|
||||
|
||||
# 按分类统计
|
||||
category_query = select(
|
||||
DeviceType.id.label('device_type_id'),
|
||||
DeviceType.name.label('device_type_name'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).join(
|
||||
Asset, Asset.device_type_id == DeviceType.id
|
||||
).group_by(
|
||||
DeviceType.id, DeviceType.name
|
||||
).order_by(func.coalesce(func.sum(Asset.purchase_price), 0).desc())
|
||||
|
||||
if where_clause:
|
||||
category_query = category_query.where(where_clause)
|
||||
|
||||
category_result = await db.execute(category_query)
|
||||
value_by_category = [
|
||||
{
|
||||
"device_type_id": row[0],
|
||||
"device_type_name": row[1],
|
||||
"count": row[2],
|
||||
"value": float(row[3]) if row[3] else 0
|
||||
}
|
||||
for row in category_result
|
||||
]
|
||||
|
||||
# 按网点统计
|
||||
org_query = select(
|
||||
Organization.id.label('organization_id'),
|
||||
Organization.name.label('organization_name'),
|
||||
func.count(Asset.id).label('count'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).join(
|
||||
Asset, Asset.organization_id == Organization.id
|
||||
).group_by(
|
||||
Organization.id, Organization.name
|
||||
).order_by(func.coalesce(func.sum(Asset.purchase_price), 0).desc())
|
||||
|
||||
if where_clause:
|
||||
org_query = org_query.where(where_clause)
|
||||
|
||||
org_result = await db.execute(org_query)
|
||||
value_by_organization = [
|
||||
{
|
||||
"organization_id": row[0],
|
||||
"organization_name": row[1],
|
||||
"count": row[2],
|
||||
"value": float(row[3]) if row[3] else 0
|
||||
}
|
||||
for row in org_result
|
||||
]
|
||||
|
||||
# 高价值资产(价值前10)
|
||||
high_value_query = select(
|
||||
Asset.id,
|
||||
Asset.asset_code,
|
||||
Asset.asset_name,
|
||||
Asset.purchase_price,
|
||||
DeviceType.name.label('device_type_name')
|
||||
).join(
|
||||
DeviceType, Asset.device_type_id == DeviceType.id
|
||||
).order_by(
|
||||
Asset.purchase_price.desc()
|
||||
).limit(10)
|
||||
|
||||
if where_clause:
|
||||
high_value_query = high_value_query.where(where_clause)
|
||||
|
||||
high_value_result = await db.execute(high_value_query)
|
||||
high_value_assets = [
|
||||
{
|
||||
"asset_id": row[0],
|
||||
"asset_code": row[1],
|
||||
"asset_name": row[2],
|
||||
"purchase_price": float(row[3]) if row[3] else 0,
|
||||
"device_type_name": row[4]
|
||||
}
|
||||
for row in high_value_result
|
||||
]
|
||||
|
||||
return {
|
||||
"total_value": float(total_value),
|
||||
"net_value": float(total_value * Decimal("0.8")), # 简化计算
|
||||
"depreciation_value": float(total_value * Decimal("0.2")),
|
||||
"value_by_category": value_by_category,
|
||||
"value_by_organization": value_by_organization,
|
||||
"high_value_assets": high_value_assets,
|
||||
}
|
||||
|
||||
async def get_trend_analysis(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取趋势分析
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
趋势分析数据
|
||||
"""
|
||||
# 默认查询最近12个月
|
||||
if not end_date:
|
||||
end_date = datetime.utcnow().date()
|
||||
if not start_date:
|
||||
start_date = end_date - timedelta(days=365)
|
||||
|
||||
# 构建查询条件
|
||||
conditions = [
|
||||
Asset.created_at >= datetime.combine(start_date, datetime.min.time()),
|
||||
Asset.created_at <= datetime.combine(end_date, datetime.max.time())
|
||||
]
|
||||
|
||||
if organization_id:
|
||||
conditions.append(Asset.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions)
|
||||
|
||||
# 资产数量趋势(按月)
|
||||
asset_trend_query = select(
|
||||
func.to_char(Asset.created_at, 'YYYY-MM').label('month'),
|
||||
func.count(Asset.id).label('count')
|
||||
).group_by('month').order_by('month')
|
||||
|
||||
asset_trend_result = await db.execute(asset_trend_query.where(where_clause))
|
||||
asset_trend = [
|
||||
{"month": row[0], "count": row[1]}
|
||||
for row in asset_trend_result
|
||||
]
|
||||
|
||||
# 资产价值趋势
|
||||
value_trend_query = select(
|
||||
func.to_char(Asset.created_at, 'YYYY-MM').label('month'),
|
||||
func.coalesce(func.sum(Asset.purchase_price), 0).label('value')
|
||||
).group_by('month').order_by('month')
|
||||
|
||||
value_trend_result = await db.execute(value_trend_query.where(where_clause))
|
||||
value_trend = [
|
||||
{"month": row[0], "value": float(row[1]) if row[1] else 0}
|
||||
for row in value_trend_result
|
||||
]
|
||||
|
||||
return {
|
||||
"asset_trend": asset_trend,
|
||||
"value_trend": value_trend,
|
||||
"purchase_trend": [],
|
||||
"maintenance_trend": [],
|
||||
"allocation_trend": [],
|
||||
}
|
||||
|
||||
async def get_maintenance_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取维修统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
维修统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
|
||||
if start_date:
|
||||
conditions.append(MaintenanceRecord.created_at >= datetime.combine(start_date, datetime.min.time()))
|
||||
if end_date:
|
||||
conditions.append(MaintenanceRecord.created_at <= datetime.combine(end_date, datetime.max.time()))
|
||||
if organization_id:
|
||||
conditions.append(MaintenanceRecord.organization_id == organization_id)
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总维修次数和费用
|
||||
count_query = select(func.count(MaintenanceRecord.id))
|
||||
cost_query = select(func.coalesce(func.sum(MaintenanceRecord.cost), 0))
|
||||
|
||||
if where_clause:
|
||||
count_query = count_query.where(where_clause)
|
||||
cost_query = cost_query.where(where_clause)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
cost_result = await db.execute(cost_query)
|
||||
|
||||
total_maintenance_count = count_result.scalar() or 0
|
||||
total_maintenance_cost = cost_result.scalar() or Decimal("0")
|
||||
|
||||
# 按状态统计
|
||||
status_query = select(
|
||||
MaintenanceRecord.status,
|
||||
func.count(MaintenanceRecord.id).label('count')
|
||||
).group_by(MaintenanceRecord.status)
|
||||
|
||||
if where_clause:
|
||||
status_query = status_query.where(where_clause)
|
||||
|
||||
status_result = await db.execute(status_query)
|
||||
status_counts = {row[0]: row[1] for row in status_result}
|
||||
|
||||
return {
|
||||
"total_maintenance_count": total_maintenance_count,
|
||||
"total_maintenance_cost": float(total_maintenance_cost),
|
||||
"pending_count": status_counts.get("pending", 0),
|
||||
"in_progress_count": status_counts.get("in_progress", 0),
|
||||
"completed_count": status_counts.get("completed", 0),
|
||||
"monthly_trend": [],
|
||||
"type_distribution": [],
|
||||
"cost_by_category": [],
|
||||
}
|
||||
|
||||
async def get_allocation_statistics(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
organization_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取分配统计
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
organization_id: 网点ID
|
||||
|
||||
Returns:
|
||||
分配统计数据
|
||||
"""
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
|
||||
if start_date:
|
||||
conditions.append(AssetAllocationOrder.created_at >= datetime.combine(start_date, datetime.min.time()))
|
||||
if end_date:
|
||||
conditions.append(AssetAllocationOrder.created_at <= datetime.combine(end_date, datetime.max.time()))
|
||||
|
||||
where_clause = and_(*conditions) if conditions else None
|
||||
|
||||
# 总分配次数
|
||||
count_query = select(func.count(AssetAllocationOrder.id))
|
||||
if where_clause:
|
||||
count_query = count_query.where(where_clause)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
total_allocation_count = count_result.scalar() or 0
|
||||
|
||||
# 按状态统计
|
||||
status_query = select(
|
||||
AssetAllocationOrder.status,
|
||||
func.count(AssetAllocationOrder.id).label('count')
|
||||
).group_by(AssetAllocationOrder.status)
|
||||
|
||||
if where_clause:
|
||||
status_query = status_query.where(where_clause)
|
||||
|
||||
status_result = await db.execute(status_query)
|
||||
status_counts = {row[0]: row[1] for row in status_result}
|
||||
|
||||
return {
|
||||
"total_allocation_count": total_allocation_count,
|
||||
"pending_count": status_counts.get("pending", 0),
|
||||
"approved_count": status_counts.get("approved", 0),
|
||||
"rejected_count": status_counts.get("rejected", 0),
|
||||
"monthly_trend": [],
|
||||
"by_organization": [],
|
||||
"transfer_statistics": [],
|
||||
}
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
statistics_service = StatisticsService()
|
||||
298
backend_new/app/services/system_config_service.py
Normal file
298
backend_new/app/services/system_config_service.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
系统配置服务层
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.crud.system_config import system_config_crud
|
||||
from app.schemas.system_config import SystemConfigCreate, SystemConfigUpdate
|
||||
import json
|
||||
|
||||
|
||||
class SystemConfigService:
|
||||
"""系统配置服务类"""
|
||||
|
||||
async def get_config(self, db: AsyncSession, config_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取配置详情
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
|
||||
Returns:
|
||||
配置信息
|
||||
"""
|
||||
config = await system_config_crud.get(db, config_id)
|
||||
if not config:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": config.id,
|
||||
"config_key": config.config_key,
|
||||
"config_name": config.config_name,
|
||||
"config_value": config.config_value,
|
||||
"value_type": config.value_type,
|
||||
"category": config.category,
|
||||
"description": config.description,
|
||||
"is_system": config.is_system,
|
||||
"is_encrypted": config.is_encrypted,
|
||||
"validation_rule": config.validation_rule,
|
||||
"options": config.options,
|
||||
"default_value": config.default_value,
|
||||
"sort_order": config.sort_order,
|
||||
"is_active": config.is_active,
|
||||
"created_at": config.created_at,
|
||||
"updated_at": config.updated_at,
|
||||
"updated_by": config.updated_by,
|
||||
}
|
||||
|
||||
async def get_config_by_key(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
config_key: str,
|
||||
default: Any = None
|
||||
) -> Any:
|
||||
"""
|
||||
根据键获取配置值
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_key: 配置键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
return await system_config_crud.get_value(db, config_key, default)
|
||||
|
||||
async def get_configs(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
keyword: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_system: Optional[bool] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取配置列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过条数
|
||||
limit: 返回条数
|
||||
keyword: 搜索关键词
|
||||
category: 配置分类
|
||||
is_active: 是否启用
|
||||
is_system: 是否系统配置
|
||||
|
||||
Returns:
|
||||
配置列表和总数
|
||||
"""
|
||||
items, total = await system_config_crud.get_multi(
|
||||
db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
keyword=keyword,
|
||||
category=category,
|
||||
is_active=is_active,
|
||||
is_system=is_system
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": item.id,
|
||||
"config_key": item.config_key,
|
||||
"config_name": item.config_name,
|
||||
"config_value": item.config_value,
|
||||
"value_type": item.value_type,
|
||||
"category": item.category,
|
||||
"description": item.description,
|
||||
"is_system": item.is_system,
|
||||
"is_encrypted": item.is_encrypted,
|
||||
"options": item.options,
|
||||
"default_value": item.default_value,
|
||||
"sort_order": item.sort_order,
|
||||
"is_active": item.is_active,
|
||||
"created_at": item.created_at,
|
||||
"updated_at": item.updated_at,
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
"total": total
|
||||
}
|
||||
|
||||
async def get_configs_by_category(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
category: str,
|
||||
is_active: bool = True
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
根据分类获取配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
category: 配置分类
|
||||
is_active: 是否启用
|
||||
|
||||
Returns:
|
||||
配置列表
|
||||
"""
|
||||
items = await system_config_crud.get_by_category(db, category, is_active=is_active)
|
||||
|
||||
return [
|
||||
{
|
||||
"config_key": item.config_key,
|
||||
"config_name": item.config_name,
|
||||
"config_value": item.config_value,
|
||||
"value_type": item.value_type,
|
||||
"description": item.description,
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
async def get_categories(self, db: AsyncSession) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有配置分类
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
分类列表
|
||||
"""
|
||||
return await system_config_crud.get_categories(db)
|
||||
|
||||
async def create_config(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
obj_in: SystemConfigCreate,
|
||||
creator_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
创建配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
obj_in: 创建数据
|
||||
creator_id: 创建人ID
|
||||
|
||||
Returns:
|
||||
创建的配置信息
|
||||
"""
|
||||
# 检查键是否已存在
|
||||
existing = await system_config_crud.get_by_key(db, obj_in.config_key)
|
||||
if existing:
|
||||
raise ValueError(f"配置键 {obj_in.config_key} 已存在")
|
||||
|
||||
# 转换为字典
|
||||
obj_in_data = obj_in.model_dump()
|
||||
|
||||
# 处理复杂类型
|
||||
if obj_in.options:
|
||||
obj_in_data["options"] = json.loads(obj_in.options.model_dump_json()) if isinstance(obj_in.options, dict) else obj_in.options
|
||||
|
||||
config = await system_config_crud.create(db, obj_in=obj_in_data)
|
||||
|
||||
return {
|
||||
"id": config.id,
|
||||
"config_key": config.config_key,
|
||||
"config_name": config.config_name,
|
||||
"category": config.category,
|
||||
}
|
||||
|
||||
async def update_config(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
config_id: int,
|
||||
obj_in: SystemConfigUpdate,
|
||||
updater_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
更新配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
obj_in: 更新数据
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新的配置信息
|
||||
"""
|
||||
config = await system_config_crud.get(db, config_id)
|
||||
if not config:
|
||||
raise ValueError("配置不存在")
|
||||
|
||||
# 系统配置不允许修改某些字段
|
||||
if config.is_system:
|
||||
if obj_in.config_key and obj_in.config_key != config.config_key:
|
||||
raise ValueError("系统配置不允许修改配置键")
|
||||
if obj_in.value_type and obj_in.value_type != config.value_type:
|
||||
raise ValueError("系统配置不允许修改值类型")
|
||||
if obj_in.category and obj_in.category != config.category:
|
||||
raise ValueError("系统配置不允许修改分类")
|
||||
|
||||
# 转换为字典,过滤None值
|
||||
update_data = obj_in.model_dump(exclude_unset=True)
|
||||
|
||||
# 处理复杂类型
|
||||
if update_data.get("options"):
|
||||
update_data["options"] = json.loads(update_data["options"].model_dump_json()) if isinstance(update_data["options"], dict) else update_data["options"]
|
||||
|
||||
update_data["updated_by"] = updater_id
|
||||
|
||||
config = await system_config_crud.update(db, db_obj=config, obj_in=update_data)
|
||||
|
||||
return {
|
||||
"id": config.id,
|
||||
"config_key": config.config_key,
|
||||
"config_name": config.config_name,
|
||||
"config_value": config.config_value,
|
||||
}
|
||||
|
||||
async def batch_update_configs(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
configs: Dict[str, Any],
|
||||
updater_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量更新配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
configs: 配置键值对
|
||||
updater_id: 更新人ID
|
||||
|
||||
Returns:
|
||||
更新结果
|
||||
"""
|
||||
updated = await system_config_crud.batch_update(
|
||||
db,
|
||||
configs=configs,
|
||||
updater_id=updater_id
|
||||
)
|
||||
|
||||
return {
|
||||
"count": len(updated),
|
||||
"configs": [item.config_key for item in updated]
|
||||
}
|
||||
|
||||
async def delete_config(self, db: AsyncSession, config_id: int) -> None:
|
||||
"""
|
||||
删除配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_id: 配置ID
|
||||
"""
|
||||
await system_config_crud.delete(db, config_id=config_id)
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
system_config_service = SystemConfigService()
|
||||
451
backend_new/app/services/transfer_service.py
Normal file
451
backend_new/app/services/transfer_service.py
Normal file
@@ -0,0 +1,451 @@
|
||||
"""
|
||||
资产调拨业务服务层
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from app.crud.transfer import transfer_order, transfer_item
|
||||
from app.crud.asset import asset
|
||||
from app.schemas.transfer import (
|
||||
AssetTransferOrderCreate,
|
||||
AssetTransferOrderUpdate
|
||||
)
|
||||
from app.core.exceptions import NotFoundException, BusinessException
|
||||
|
||||
|
||||
class TransferService:
|
||||
"""资产调拨服务类"""
|
||||
|
||||
async def get_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""获取调拨单详情"""
|
||||
# 使用selectinload预加载关联数据,避免N+1查询
|
||||
from app.models.transfer import AssetTransferOrder
|
||||
from app.models.organization import Organization
|
||||
from app.models.user import User
|
||||
from app.models.transfer import AssetTransferItem
|
||||
|
||||
obj = db.query(
|
||||
AssetTransferOrder
|
||||
).options(
|
||||
selectinload(AssetTransferOrder.items),
|
||||
selectinload(AssetTransferOrder.source_org.of_type(Organization)),
|
||||
selectinload(AssetTransferOrder.target_org.of_type(Organization)),
|
||||
selectinload(AssetTransferOrder.applicant.of_type(User)),
|
||||
selectinload(AssetTransferOrder.approver.of_type(User)),
|
||||
selectinload(AssetTransferOrder.executor.of_type(User))
|
||||
).filter(
|
||||
AssetTransferOrder.id == order_id
|
||||
).first()
|
||||
|
||||
if not obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 加载关联信息
|
||||
return self._load_order_relations(db, obj)
|
||||
|
||||
def get_orders(
|
||||
self,
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
transfer_type: Optional[str] = None,
|
||||
approval_status: Optional[str] = None,
|
||||
execute_status: Optional[str] = None,
|
||||
source_org_id: Optional[int] = None,
|
||||
target_org_id: Optional[int] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> tuple:
|
||||
"""获取调拨单列表"""
|
||||
items, total = transfer_order.get_multi(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
transfer_type=transfer_type,
|
||||
approval_status=approval_status,
|
||||
execute_status=execute_status,
|
||||
source_org_id=source_org_id,
|
||||
target_org_id=target_org_id,
|
||||
keyword=keyword
|
||||
)
|
||||
|
||||
# 加载关联信息
|
||||
items_with_relations = [self._load_order_relations(db, item) for item in items]
|
||||
|
||||
return items_with_relations, total
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
db: Session,
|
||||
obj_in: AssetTransferOrderCreate,
|
||||
apply_user_id: int
|
||||
):
|
||||
"""创建调拨单"""
|
||||
# 验证资产存在性和状态
|
||||
assets = []
|
||||
for asset_id in obj_in.asset_ids:
|
||||
asset_obj = asset.get(db, asset_id)
|
||||
if not asset_obj:
|
||||
raise NotFoundException(f"资产ID {asset_id}")
|
||||
assets.append(asset_obj)
|
||||
|
||||
# 验证资产状态是否允许调拨
|
||||
for asset_obj in assets:
|
||||
if asset_obj.status not in ["in_stock", "in_use"]:
|
||||
raise BusinessException(
|
||||
f"资产 {asset_obj.asset_code} 当前状态为 {asset_obj.status},不允许调拨操作"
|
||||
)
|
||||
|
||||
# 验证资产所属机构是否为调出机构
|
||||
for asset_obj in assets:
|
||||
if asset_obj.organization_id != obj_in.source_org_id:
|
||||
raise BusinessException(
|
||||
f"资产 {asset_obj.asset_code} 所属机构与调出机构不一致"
|
||||
)
|
||||
|
||||
# 生成调拨单号
|
||||
order_code = await self._generate_order_code(db)
|
||||
|
||||
# 创建调拨单
|
||||
db_obj = transfer_order.create(
|
||||
db=db,
|
||||
obj_in=obj_in,
|
||||
order_code=order_code,
|
||||
apply_user_id=apply_user_id
|
||||
)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def update_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
obj_in: AssetTransferOrderUpdate
|
||||
):
|
||||
"""更新调拨单"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 只有待审批状态可以更新
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("只有待审批状态的调拨单可以更新")
|
||||
|
||||
return transfer_order.update(db, db_obj, obj_in)
|
||||
|
||||
def approve_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
approval_status: str,
|
||||
approval_user_id: int,
|
||||
approval_remark: Optional[str] = None
|
||||
):
|
||||
"""审批调拨单"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "pending":
|
||||
raise BusinessException("该调拨单已审批,无法重复审批")
|
||||
|
||||
# 审批
|
||||
db_obj = transfer_order.approve(
|
||||
db=db,
|
||||
db_obj=db_obj,
|
||||
approval_status=approval_status,
|
||||
approval_user_id=approval_user_id,
|
||||
approval_remark=approval_remark
|
||||
)
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def start_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
execute_user_id: int
|
||||
):
|
||||
"""开始调拨"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.approval_status != "approved":
|
||||
raise BusinessException("该调拨单未审批通过,无法开始执行")
|
||||
if db_obj.execute_status != "pending":
|
||||
raise BusinessException("该调拨单已开始或已完成")
|
||||
|
||||
# 开始调拨
|
||||
db_obj = transfer_order.start(db, db_obj, execute_user_id)
|
||||
|
||||
# 更新明细状态为调拨中
|
||||
transfer_item.batch_update_transfer_status(db, order_id, "transferring")
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
async def complete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int,
|
||||
execute_user_id: int
|
||||
):
|
||||
"""完成调拨"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status not in ["pending", "executing"]:
|
||||
raise BusinessException("该调拨单状态不允许完成操作")
|
||||
|
||||
# 完成调拨单
|
||||
db_obj = transfer_order.complete(db, db_obj, execute_user_id)
|
||||
|
||||
# 更新资产机构和状态
|
||||
await self._execute_transfer_logic(db, db_obj)
|
||||
|
||||
# 更新明细状态为完成
|
||||
transfer_item.batch_update_transfer_status(db, order_id, "completed")
|
||||
|
||||
return self._load_order_relations(db, db_obj)
|
||||
|
||||
def cancel_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""取消调拨单"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 检查状态
|
||||
if db_obj.execute_status == "completed":
|
||||
raise BusinessException("已完成的调拨单无法取消")
|
||||
|
||||
transfer_order.cancel(db, db_obj)
|
||||
return True
|
||||
|
||||
def delete_order(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> bool:
|
||||
"""删除调拨单"""
|
||||
db_obj = transfer_order.get(db, order_id)
|
||||
if not db_obj:
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
# 只有已取消或已拒绝的可以删除
|
||||
if db_obj.approval_status not in ["rejected", "cancelled"]:
|
||||
raise BusinessException("只能删除已拒绝或已取消的调拨单")
|
||||
|
||||
return transfer_order.delete(db, order_id)
|
||||
|
||||
def get_order_items(
|
||||
self,
|
||||
db: Session,
|
||||
order_id: int
|
||||
) -> List:
|
||||
"""获取调拨单明细"""
|
||||
# 验证调拨单存在
|
||||
if not transfer_order.get(db, order_id):
|
||||
raise NotFoundException("调拨单")
|
||||
|
||||
return transfer_item.get_by_order(db, order_id)
|
||||
|
||||
def get_statistics(
|
||||
self,
|
||||
db: Session,
|
||||
source_org_id: Optional[int] = None,
|
||||
target_org_id: Optional[int] = None
|
||||
) -> Dict[str, int]:
|
||||
"""获取调拨单统计信息"""
|
||||
return transfer_order.get_statistics(db, source_org_id, target_org_id)
|
||||
|
||||
async def _execute_transfer_logic(
|
||||
self,
|
||||
db: Session,
|
||||
order_obj
|
||||
):
|
||||
"""执行调拨逻辑(完成调拨时自动执行)"""
|
||||
# 获取明细
|
||||
items = transfer_item.get_by_order(db, order_obj.id)
|
||||
|
||||
# 更新资产机构和状态
|
||||
from app.services.asset_service import asset_service
|
||||
from app.schemas.asset import AssetStatusTransition, AssetUpdate
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# 变更资产状态
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=item.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status="transferring",
|
||||
remark=f"调拨单: {order_obj.order_code},从{item.source_organization_id}到{item.target_organization_id}"
|
||||
),
|
||||
operator_id=order_obj.execute_user_id
|
||||
)
|
||||
|
||||
# 更新资产所属机构
|
||||
asset_obj = asset.get(db, item.asset_id)
|
||||
if asset_obj:
|
||||
asset.update(
|
||||
db=db,
|
||||
db_obj=asset_obj,
|
||||
obj_in=AssetUpdate(
|
||||
organization_id=item.target_organization_id
|
||||
),
|
||||
updater_id=order_obj.execute_user_id
|
||||
)
|
||||
|
||||
# 最终状态变更
|
||||
target_status = "in_stock"
|
||||
await asset_service.change_asset_status(
|
||||
db=db,
|
||||
asset_id=item.asset_id,
|
||||
status_transition=AssetStatusTransition(
|
||||
new_status=target_status,
|
||||
remark=f"调拨完成: {order_obj.order_code}"
|
||||
),
|
||||
operator_id=order_obj.execute_user_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# 记录失败日志
|
||||
print(f"调拨资产 {item.asset_code} 失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def _load_order_relations(
|
||||
self,
|
||||
db: Session,
|
||||
obj
|
||||
) -> Dict[str, Any]:
|
||||
"""加载调拨单关联信息"""
|
||||
from app.models.user import User
|
||||
from app.models.organization import Organization
|
||||
|
||||
result = {
|
||||
"id": obj.id,
|
||||
"order_code": obj.order_code,
|
||||
"source_org_id": obj.source_org_id,
|
||||
"target_org_id": obj.target_org_id,
|
||||
"transfer_type": obj.transfer_type,
|
||||
"title": obj.title,
|
||||
"asset_count": obj.asset_count,
|
||||
"apply_user_id": obj.apply_user_id,
|
||||
"apply_time": obj.apply_time,
|
||||
"approval_status": obj.approval_status,
|
||||
"approval_user_id": obj.approval_user_id,
|
||||
"approval_time": obj.approval_time,
|
||||
"approval_remark": obj.approval_remark,
|
||||
"execute_status": obj.execute_status,
|
||||
"execute_user_id": obj.execute_user_id,
|
||||
"execute_time": obj.execute_time,
|
||||
"remark": obj.remark,
|
||||
"created_at": obj.created_at,
|
||||
"updated_at": obj.updated_at
|
||||
}
|
||||
|
||||
# 加载调出机构
|
||||
if obj.source_org_id:
|
||||
source_org = db.query(Organization).filter(
|
||||
Organization.id == obj.source_org_id
|
||||
).first()
|
||||
if source_org:
|
||||
result["source_organization"] = {
|
||||
"id": source_org.id,
|
||||
"org_name": source_org.org_name,
|
||||
"org_type": source_org.org_type
|
||||
}
|
||||
|
||||
# 加载调入机构
|
||||
if obj.target_org_id:
|
||||
target_org = db.query(Organization).filter(
|
||||
Organization.id == obj.target_org_id
|
||||
).first()
|
||||
if target_org:
|
||||
result["target_organization"] = {
|
||||
"id": target_org.id,
|
||||
"org_name": target_org.org_name,
|
||||
"org_type": target_org.org_type
|
||||
}
|
||||
|
||||
# 加载申请人
|
||||
if obj.apply_user_id:
|
||||
apply_user = db.query(User).filter(User.id == obj.apply_user_id).first()
|
||||
if apply_user:
|
||||
result["apply_user"] = {
|
||||
"id": apply_user.id,
|
||||
"real_name": apply_user.real_name,
|
||||
"username": apply_user.username
|
||||
}
|
||||
|
||||
# 加载审批人
|
||||
if obj.approval_user_id:
|
||||
approval_user = db.query(User).filter(User.id == obj.approval_user_id).first()
|
||||
if approval_user:
|
||||
result["approval_user"] = {
|
||||
"id": approval_user.id,
|
||||
"real_name": approval_user.real_name,
|
||||
"username": approval_user.username
|
||||
}
|
||||
|
||||
# 加载执行人
|
||||
if obj.execute_user_id:
|
||||
execute_user = db.query(User).filter(User.id == obj.execute_user_id).first()
|
||||
if execute_user:
|
||||
result["execute_user"] = {
|
||||
"id": execute_user.id,
|
||||
"real_name": execute_user.real_name,
|
||||
"username": execute_user.username
|
||||
}
|
||||
|
||||
# 加载明细
|
||||
items = transfer_item.get_by_order(db, obj.id)
|
||||
result["items"] = [
|
||||
{
|
||||
"id": item.id,
|
||||
"asset_id": item.asset_id,
|
||||
"asset_code": item.asset_code,
|
||||
"source_organization_id": item.source_organization_id,
|
||||
"target_organization_id": item.target_organization_id,
|
||||
"transfer_status": item.transfer_status
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
async def _generate_order_code(self, db: Session) -> str:
|
||||
"""生成调拨单号"""
|
||||
from datetime import datetime
|
||||
import random
|
||||
import string
|
||||
|
||||
# 日期部分
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
# 序号部分(5位随机数)
|
||||
sequence = "".join(random.choices(string.digits, k=5))
|
||||
|
||||
# 组合单号: TO-20250124-00001
|
||||
order_code = f"TO-{date_str}-{sequence}"
|
||||
|
||||
# 检查是否重复,如果重复则重新生成
|
||||
while transfer_order.get_by_code(db, order_code):
|
||||
sequence = "".join(random.choices(string.digits, k=5))
|
||||
order_code = f"TO-{date_str}-{sequence}"
|
||||
|
||||
return order_code
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
transfer_service = TransferService()
|
||||
Reference in New Issue
Block a user