初始化
This commit is contained in:
BIN
backend/app/routers/__pycache__/agents.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/agents.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/calculate.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/calculate.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/categories.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/categories.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/dashboard.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/dashboard.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/employees.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/employees.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/performance.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/performance.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/reports.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/reports.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/settings.cpython-312.pyc
Normal file
BIN
backend/app/routers/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
367
backend/app/routers/agents.py
Normal file
367
backend/app/routers/agents.py
Normal file
@@ -0,0 +1,367 @@
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User, Employee, SecondaryAgent, OperationLog
|
||||
from app.routers.auth import get_current_user, get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/agents", tags=["二级代理管理"])
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class AgentBase(BaseModel):
|
||||
company_name: str
|
||||
contact_name: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
profit_share_rate: Decimal = Field(default=0.60)
|
||||
address: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
employee_id: int
|
||||
|
||||
|
||||
class AgentCreate(AgentBase):
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
class AgentUpdate(BaseModel):
|
||||
company_name: Optional[str] = None
|
||||
contact_name: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
profit_share_rate: Optional[Decimal] = None
|
||||
address: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
employee_id: Optional[int] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
class AgentResponse(BaseModel):
|
||||
id: int
|
||||
user_id: Optional[int]
|
||||
employee_id: int
|
||||
employee_name: str
|
||||
company_name: str
|
||||
contact_name: Optional[str]
|
||||
contact_phone: Optional[str]
|
||||
profit_share_rate: Decimal
|
||||
address: Optional[str]
|
||||
remark: Optional[str]
|
||||
status: int
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def log_operation(db: Session, user_id: int, action: str, target_type: str, target_id: int,
|
||||
old_value: Optional[str] = None, new_value: Optional[str] = None):
|
||||
"""记录操作日志"""
|
||||
log = OperationLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
old_value=old_value,
|
||||
new_value=new_value
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
def get_agents(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=1000),
|
||||
search: Optional[str] = None,
|
||||
employee_id: Optional[int] = None,
|
||||
status: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取代理列表"""
|
||||
query = db.query(SecondaryAgent).join(Employee).join(User, Employee.user_id == User.id)
|
||||
|
||||
# 搜索条件
|
||||
if search:
|
||||
query = query.filter(
|
||||
(SecondaryAgent.company_name.contains(search)) |
|
||||
(SecondaryAgent.contact_name.contains(search)) |
|
||||
(SecondaryAgent.contact_phone.contains(search))
|
||||
)
|
||||
|
||||
# 员工筛选
|
||||
if employee_id:
|
||||
query = query.filter(SecondaryAgent.employee_id == employee_id)
|
||||
|
||||
# 状态筛选
|
||||
if status is not None:
|
||||
query = query.filter(SecondaryAgent.status == status)
|
||||
|
||||
# 非管理员只能查看自己关联的代理
|
||||
if current_user.role != "admin":
|
||||
# 检查当前用户是否是员工
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if employee:
|
||||
query = query.filter(SecondaryAgent.employee_id == employee.id)
|
||||
|
||||
# 计算总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
agents = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# 构建响应数据
|
||||
items = []
|
||||
for agent in agents:
|
||||
items.append({
|
||||
"id": agent.id,
|
||||
"user_id": agent.user_id,
|
||||
"employee_id": agent.employee_id,
|
||||
"employee_name": agent.employee.user.name if agent.employee else None,
|
||||
"company_name": agent.company_name,
|
||||
"contact_name": agent.contact_name,
|
||||
"contact_phone": agent.contact_phone,
|
||||
"profit_share_rate": str(agent.profit_share_rate),
|
||||
"address": agent.address,
|
||||
"remark": agent.remark,
|
||||
"status": agent.status,
|
||||
"created_at": agent.created_at.isoformat() if agent.created_at else None,
|
||||
"updated_at": agent.updated_at.isoformat() if agent.updated_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{agent_id}", response_model=dict)
|
||||
def get_agent(
|
||||
agent_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取代理详情"""
|
||||
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == agent_id).first()
|
||||
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
|
||||
# 权限检查:非管理员只能查看自己的代理
|
||||
if current_user.role != "admin":
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if not employee or agent.employee_id != employee.id:
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": agent.id,
|
||||
"user_id": agent.user_id,
|
||||
"employee_id": agent.employee_id,
|
||||
"employee_name": agent.employee.user.name if agent.employee else None,
|
||||
"company_name": agent.company_name,
|
||||
"contact_name": agent.contact_name,
|
||||
"contact_phone": agent.contact_phone,
|
||||
"profit_share_rate": str(agent.profit_share_rate),
|
||||
"address": agent.address,
|
||||
"remark": agent.remark,
|
||||
"status": agent.status,
|
||||
"created_at": agent.created_at.isoformat() if agent.created_at else None,
|
||||
"updated_at": agent.updated_at.isoformat() if agent.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
def create_agent(
|
||||
data: AgentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""创建代理"""
|
||||
# 检查员工是否存在
|
||||
employee = db.query(Employee).filter(Employee.id == data.employee_id).first()
|
||||
if not employee:
|
||||
raise HTTPException(status_code=400, detail="所属员工不存在")
|
||||
|
||||
user_id = None
|
||||
|
||||
# 如果提供了用户名和密码,创建用户账号
|
||||
if data.username and data.password:
|
||||
# 检查用户名是否已存在
|
||||
existing_user = db.query(User).filter(User.username == data.username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
# 创建用户
|
||||
from app.auth import get_password_hash
|
||||
user = User(
|
||||
username=data.username,
|
||||
password_hash=get_password_hash(data.password),
|
||||
role="agent",
|
||||
name=data.contact_name or data.company_name,
|
||||
phone=data.contact_phone,
|
||||
status=1
|
||||
)
|
||||
db.add(user)
|
||||
db.flush() # 获取user.id
|
||||
user_id = user.id
|
||||
|
||||
# 创建代理
|
||||
agent = SecondaryAgent(
|
||||
user_id=user_id,
|
||||
employee_id=data.employee_id,
|
||||
company_name=data.company_name,
|
||||
contact_name=data.contact_name,
|
||||
contact_phone=data.contact_phone,
|
||||
profit_share_rate=data.profit_share_rate,
|
||||
address=data.address,
|
||||
remark=data.remark,
|
||||
status=1
|
||||
)
|
||||
db.add(agent)
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "CREATE_AGENT", "agent", agent.id,
|
||||
new_value=f"创建代理: {data.company_name}, 所属员工: {employee.user.name}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "代理创建成功",
|
||||
"data": {
|
||||
"id": agent.id,
|
||||
"user_id": agent.user_id,
|
||||
"employee_id": agent.employee_id,
|
||||
"employee_name": employee.user.name,
|
||||
"company_name": agent.company_name,
|
||||
"contact_name": agent.contact_name,
|
||||
"contact_phone": agent.contact_phone,
|
||||
"profit_share_rate": str(agent.profit_share_rate),
|
||||
"address": agent.address,
|
||||
"remark": agent.remark,
|
||||
"status": agent.status,
|
||||
"created_at": agent.created_at.isoformat() if agent.created_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{agent_id}", response_model=dict)
|
||||
def update_agent(
|
||||
agent_id: int,
|
||||
data: AgentUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""更新代理"""
|
||||
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == agent_id).first()
|
||||
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
|
||||
# 如果更换所属员工,检查新员工是否存在
|
||||
if data.employee_id is not None and data.employee_id != agent.employee_id:
|
||||
employee = db.query(Employee).filter(Employee.id == data.employee_id).first()
|
||||
if not employee:
|
||||
raise HTTPException(status_code=400, detail="所属员工不存在")
|
||||
|
||||
# 记录旧值
|
||||
old_value = f"company={agent.company_name}, contact={agent.contact_name}, profit_rate={agent.profit_share_rate}, employee_id={agent.employee_id}"
|
||||
|
||||
# 更新代理信息
|
||||
if data.company_name is not None:
|
||||
agent.company_name = data.company_name
|
||||
if data.contact_name is not None:
|
||||
agent.contact_name = data.contact_name
|
||||
# 同时更新用户名称
|
||||
if agent.user:
|
||||
agent.user.name = data.contact_name
|
||||
if data.contact_phone is not None:
|
||||
agent.contact_phone = data.contact_phone
|
||||
# 同时更新用户电话
|
||||
if agent.user:
|
||||
agent.user.phone = data.contact_phone
|
||||
if data.profit_share_rate is not None:
|
||||
agent.profit_share_rate = data.profit_share_rate
|
||||
if data.address is not None:
|
||||
agent.address = data.address
|
||||
if data.remark is not None:
|
||||
agent.remark = data.remark
|
||||
if data.employee_id is not None:
|
||||
agent.employee_id = data.employee_id
|
||||
if data.status is not None:
|
||||
agent.status = data.status
|
||||
# 同时更新用户状态
|
||||
if agent.user:
|
||||
agent.user.status = data.status
|
||||
|
||||
db.commit()
|
||||
db.refresh(agent)
|
||||
|
||||
# 记录操作日志
|
||||
new_value = f"company={agent.company_name}, contact={agent.contact_name}, profit_rate={agent.profit_share_rate}, employee_id={agent.employee_id}"
|
||||
log_operation(db, current_admin.id, "UPDATE_AGENT", "agent", agent_id,
|
||||
old_value=old_value, new_value=new_value)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "代理信息更新成功",
|
||||
"data": {
|
||||
"id": agent.id,
|
||||
"user_id": agent.user_id,
|
||||
"employee_id": agent.employee_id,
|
||||
"employee_name": agent.employee.user.name if agent.employee else None,
|
||||
"company_name": agent.company_name,
|
||||
"contact_name": agent.contact_name,
|
||||
"contact_phone": agent.contact_phone,
|
||||
"profit_share_rate": str(agent.profit_share_rate),
|
||||
"address": agent.address,
|
||||
"remark": agent.remark,
|
||||
"status": agent.status,
|
||||
"updated_at": agent.updated_at.isoformat() if agent.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{agent_id}", response_model=dict)
|
||||
def delete_agent(
|
||||
agent_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""删除代理"""
|
||||
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == agent_id).first()
|
||||
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
|
||||
company_name = agent.company_name
|
||||
user_id = agent.user_id
|
||||
|
||||
# 删除代理
|
||||
db.delete(agent)
|
||||
db.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "DELETE_AGENT", "agent", agent_id,
|
||||
old_value=f"删除代理: {company_name}, user_id={user_id}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "代理删除成功",
|
||||
"data": None
|
||||
}
|
||||
152
backend/app/routers/auth.py
Normal file
152
backend/app/routers/auth.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from typing import Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from datetime import timedelta
|
||||
|
||||
from app.database import get_db
|
||||
from app.auth import verify_password, create_access_token, get_password_hash
|
||||
from app.models import User
|
||||
from app.config import get_settings
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
settings = get_settings()
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
expires_in: int
|
||||
user: dict
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
name: str
|
||||
role: str
|
||||
phone: str = None
|
||||
email: str = None
|
||||
last_login_at: str = None
|
||||
|
||||
|
||||
# 依赖函数
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
|
||||
"""获取当前登录用户"""
|
||||
from app.auth import decode_token
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
payload = decode_token(token)
|
||||
if payload is None:
|
||||
raise credentials_exception
|
||||
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise credentials_exception
|
||||
|
||||
user = db.query(User).filter(User.id == int(user_id), User.status == 1).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""验证当前用户是管理员"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,需要管理员权限"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
# 路由
|
||||
@router.post("/login", response_model=dict)
|
||||
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
"""用户登录"""
|
||||
# 查找用户
|
||||
user = db.query(User).filter(User.username == form_data.username).first()
|
||||
|
||||
if not user or not verify_password(form_data.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if user.status != 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="账号已被禁用"
|
||||
)
|
||||
|
||||
# 更新最后登录时间
|
||||
from sqlalchemy import func
|
||||
user.last_login_at = func.now()
|
||||
db.commit()
|
||||
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user.id), "role": user.role},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"role": user.role
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(current_user: User = Depends(get_current_user)):
|
||||
"""用户退出(前端清除token即可)"""
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "退出成功",
|
||||
"data": None
|
||||
}
|
||||
|
||||
|
||||
@router.get("/me", response_model=dict)
|
||||
def get_me(current_user: User = Depends(get_current_user)):
|
||||
"""获取当前用户信息"""
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": current_user.id,
|
||||
"username": current_user.username,
|
||||
"name": current_user.name,
|
||||
"role": current_user.role,
|
||||
"phone": current_user.phone,
|
||||
"email": current_user.email,
|
||||
"last_login_at": current_user.last_login_at.isoformat() if current_user.last_login_at else None
|
||||
}
|
||||
}
|
||||
217
backend/app/routers/calculate.py
Normal file
217
backend/app/routers/calculate.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from typing import Optional, Literal
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User
|
||||
from app.routers.auth import get_current_user, get_current_admin
|
||||
from app.services.calculate_service import (
|
||||
calculate_employee_income,
|
||||
calculate_company_profit,
|
||||
calculate_agent_profit,
|
||||
get_calculation_history,
|
||||
get_calculation_detail
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/calculate", tags=["收益计算"])
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class EmployeeCalculateRequest(BaseModel):
|
||||
period: Literal["monthly", "quarterly", "half_yearly", "yearly"] = Field(..., description="计算周期")
|
||||
year: int = Field(..., description="年份")
|
||||
month: Optional[int] = Field(None, ge=1, le=12, description="月份(月度周期需要)")
|
||||
quarter: Optional[int] = Field(None, ge=1, le=4, description="季度(季度周期需要)")
|
||||
|
||||
|
||||
class CompanyCalculateRequest(BaseModel):
|
||||
period: Literal["monthly", "quarterly", "half_yearly", "yearly"] = Field(..., description="计算周期")
|
||||
year: int = Field(..., description="年份")
|
||||
month: Optional[int] = Field(None, ge=1, le=12, description="月份(月度周期需要)")
|
||||
quarter: Optional[int] = Field(None, ge=1, le=4, description="季度(季度周期需要)")
|
||||
|
||||
|
||||
class AgentCalculateRequest(BaseModel):
|
||||
period: Literal["monthly", "quarterly", "half_yearly", "yearly"] = Field(..., description="计算周期")
|
||||
year: int = Field(..., description="年份")
|
||||
month: Optional[int] = Field(None, ge=1, le=12, description="月份(月度周期需要)")
|
||||
quarter: Optional[int] = Field(None, ge=1, le=4, description="季度(季度周期需要)")
|
||||
|
||||
|
||||
@router.post("/employee/{employee_id}", response_model=dict)
|
||||
def calculate_employee(
|
||||
employee_id: int,
|
||||
data: EmployeeCalculateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""计算员工收益"""
|
||||
# 权限检查:非管理员只能计算自己的收益
|
||||
if current_user.role != "admin":
|
||||
from app.models import Employee
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if not employee or employee.id != employee_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,只能查看自己的收益"
|
||||
)
|
||||
|
||||
try:
|
||||
result = calculate_employee_income(
|
||||
db=db,
|
||||
employee_id=employee_id,
|
||||
period=data.period,
|
||||
year=data.year,
|
||||
month=data.month,
|
||||
quarter=data.quarter,
|
||||
save_result=True
|
||||
)
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "计算成功",
|
||||
"data": result
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"计算失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/history", response_model=dict)
|
||||
def get_history(
|
||||
employee_id: Optional[int] = None,
|
||||
period: Optional[str] = None,
|
||||
year: Optional[int] = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取计算历史列表"""
|
||||
# 权限检查:非管理员只能查看自己的历史
|
||||
if current_user.role != "admin":
|
||||
from app.models import Employee
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if employee:
|
||||
employee_id = employee.id
|
||||
else:
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {"items": [], "total": 0, "page": page, "page_size": page_size}
|
||||
}
|
||||
|
||||
result = get_calculation_history(
|
||||
db=db,
|
||||
employee_id=employee_id,
|
||||
period=period,
|
||||
year=year,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
|
||||
@router.get("/history/{calculation_id}", response_model=dict)
|
||||
def get_history_detail(
|
||||
calculation_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取计算历史详情"""
|
||||
result = get_calculation_detail(db, calculation_id)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="计算记录不存在")
|
||||
|
||||
# 权限检查:非管理员只能查看自己的记录
|
||||
if current_user.role != "admin":
|
||||
from app.models import Employee
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if not employee or result["employee_id"] != employee.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足"
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
|
||||
@router.post("/company", response_model=dict)
|
||||
def calculate_company(
|
||||
data: CompanyCalculateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""计算公司收益汇总(仅管理员)"""
|
||||
try:
|
||||
result = calculate_company_profit(
|
||||
db=db,
|
||||
period=data.period,
|
||||
year=data.year,
|
||||
month=data.month,
|
||||
quarter=data.quarter,
|
||||
save_result=False
|
||||
)
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "计算成功",
|
||||
"data": result
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"计算失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/agent/{agent_id}", response_model=dict)
|
||||
def calculate_agent(
|
||||
agent_id: int,
|
||||
data: AgentCalculateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""计算代理收益"""
|
||||
# 权限检查:非管理员只能查看自己关联的代理
|
||||
if current_user.role != "admin":
|
||||
from app.models import Employee, SecondaryAgent
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if employee:
|
||||
agent = db.query(SecondaryAgent).filter(
|
||||
SecondaryAgent.id == agent_id,
|
||||
SecondaryAgent.employee_id == employee.id
|
||||
).first()
|
||||
if not agent:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足"
|
||||
)
|
||||
|
||||
try:
|
||||
result = calculate_agent_profit(
|
||||
db=db,
|
||||
agent_id=agent_id,
|
||||
period=data.period,
|
||||
year=data.year,
|
||||
month=data.month,
|
||||
quarter=data.quarter
|
||||
)
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "计算成功",
|
||||
"data": result
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"计算失败: {str(e)}")
|
||||
559
backend/app/routers/categories.py
Normal file
559
backend/app/routers/categories.py
Normal file
@@ -0,0 +1,559 @@
|
||||
from typing import Optional, List, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import json
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User, ProductCategory, OperationLog
|
||||
from app.routers.auth import get_current_user, get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/categories", tags=["产品分类管理"])
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class CategoryBase(BaseModel):
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
parent_id: Optional[int] = None
|
||||
commission_rate: Decimal = Field(default=0.03)
|
||||
monthly_rebate: Decimal = Field(default=0.10)
|
||||
quarterly_rebate: Optional[Decimal] = None
|
||||
is_main_product: int = Field(default=0)
|
||||
sort_order: int = Field(default=0)
|
||||
|
||||
|
||||
class CategoryCreate(CategoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
code: Optional[str] = None
|
||||
parent_id: Optional[int] = None
|
||||
commission_rate: Optional[Decimal] = None
|
||||
monthly_rebate: Optional[Decimal] = None
|
||||
quarterly_rebate: Optional[Decimal] = None
|
||||
is_main_product: Optional[int] = None
|
||||
sort_order: Optional[int] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
class CategoryImportItem(BaseModel):
|
||||
name: str
|
||||
code: str
|
||||
parent_code: Optional[str] = None
|
||||
commission_rate: Decimal = Field(default=0.03)
|
||||
monthly_rebate: Decimal = Field(default=0.10)
|
||||
quarterly_rebate: Optional[Decimal] = None
|
||||
is_main_product: int = Field(default=0)
|
||||
|
||||
|
||||
class CategoryImportData(BaseModel):
|
||||
items: List[CategoryImportItem]
|
||||
source_file: str
|
||||
|
||||
|
||||
# 导入预览缓存(实际生产环境应使用Redis等)
|
||||
_import_preview_cache: Dict[str, Any] = {}
|
||||
|
||||
|
||||
def log_operation(db: Session, user_id: int, action: str, target_type: str, target_id: Optional[int] = None,
|
||||
old_value: Optional[str] = None, new_value: Optional[str] = None):
|
||||
"""记录操作日志"""
|
||||
log = OperationLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
old_value=old_value,
|
||||
new_value=new_value
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
|
||||
def build_category_tree(categories: List[ProductCategory], parent_id: Optional[int] = None) -> List[Dict]:
|
||||
"""构建分类树形结构"""
|
||||
tree = []
|
||||
for cat in categories:
|
||||
if cat.parent_id == parent_id:
|
||||
children = build_category_tree(categories, cat.id)
|
||||
node = {
|
||||
"id": cat.id,
|
||||
"name": cat.name,
|
||||
"code": cat.code,
|
||||
"parent_id": cat.parent_id,
|
||||
"commission_rate": str(cat.commission_rate),
|
||||
"monthly_rebate": str(cat.monthly_rebate),
|
||||
"quarterly_rebate": str(cat.quarterly_rebate) if cat.quarterly_rebate else None,
|
||||
"is_main_product": cat.is_main_product,
|
||||
"sort_order": cat.sort_order,
|
||||
"status": cat.status,
|
||||
"source_file": cat.source_file,
|
||||
"import_batch": cat.import_batch,
|
||||
"last_import_at": cat.last_import_at.isoformat() if cat.last_import_at else None,
|
||||
"created_at": cat.created_at.isoformat() if cat.created_at else None,
|
||||
"updated_at": cat.updated_at.isoformat() if cat.updated_at else None,
|
||||
"children": children if children else []
|
||||
}
|
||||
tree.append(node)
|
||||
return sorted(tree, key=lambda x: x["sort_order"])
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
def get_categories(
|
||||
tree: bool = Query(True, description="是否返回树形结构"),
|
||||
status: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取分类列表(树形结构)"""
|
||||
query = db.query(ProductCategory)
|
||||
|
||||
# 状态筛选
|
||||
if status is not None:
|
||||
query = query.filter(ProductCategory.status == status)
|
||||
|
||||
categories = query.order_by(ProductCategory.sort_order, ProductCategory.id).all()
|
||||
|
||||
if tree:
|
||||
data = build_category_tree(categories)
|
||||
else:
|
||||
data = []
|
||||
for cat in categories:
|
||||
data.append({
|
||||
"id": cat.id,
|
||||
"name": cat.name,
|
||||
"code": cat.code,
|
||||
"parent_id": cat.parent_id,
|
||||
"commission_rate": str(cat.commission_rate),
|
||||
"monthly_rebate": str(cat.monthly_rebate),
|
||||
"quarterly_rebate": str(cat.quarterly_rebate) if cat.quarterly_rebate else None,
|
||||
"is_main_product": cat.is_main_product,
|
||||
"sort_order": cat.sort_order,
|
||||
"status": cat.status,
|
||||
"source_file": cat.source_file,
|
||||
"import_batch": cat.import_batch,
|
||||
"last_import_at": cat.last_import_at.isoformat() if cat.last_import_at else None,
|
||||
"created_at": cat.created_at.isoformat() if cat.created_at else None,
|
||||
"updated_at": cat.updated_at.isoformat() if cat.updated_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{category_id}", response_model=dict)
|
||||
def get_category(
|
||||
category_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取分类详情"""
|
||||
category = db.query(ProductCategory).filter(ProductCategory.id == category_id).first()
|
||||
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
|
||||
# 获取父级信息
|
||||
parent_name = None
|
||||
if category.parent_id:
|
||||
parent = db.query(ProductCategory).filter(ProductCategory.id == category.parent_id).first()
|
||||
if parent:
|
||||
parent_name = parent.name
|
||||
|
||||
# 获取子分类
|
||||
children = db.query(ProductCategory).filter(ProductCategory.parent_id == category_id).all()
|
||||
children_data = [{"id": c.id, "name": c.name, "code": c.code} for c in children]
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": category.id,
|
||||
"name": category.name,
|
||||
"code": category.code,
|
||||
"parent_id": category.parent_id,
|
||||
"parent_name": parent_name,
|
||||
"commission_rate": str(category.commission_rate),
|
||||
"monthly_rebate": str(category.monthly_rebate),
|
||||
"quarterly_rebate": str(category.quarterly_rebate) if category.quarterly_rebate else None,
|
||||
"is_main_product": category.is_main_product,
|
||||
"sort_order": category.sort_order,
|
||||
"status": category.status,
|
||||
"source_file": category.source_file,
|
||||
"import_batch": category.import_batch,
|
||||
"last_import_at": category.last_import_at.isoformat() if category.last_import_at else None,
|
||||
"children": children_data,
|
||||
"created_at": category.created_at.isoformat() if category.created_at else None,
|
||||
"updated_at": category.updated_at.isoformat() if category.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
def create_category(
|
||||
data: CategoryCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""创建分类"""
|
||||
# 检查code是否已存在
|
||||
if data.code:
|
||||
existing = db.query(ProductCategory).filter(ProductCategory.code == data.code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="分类编码已存在")
|
||||
|
||||
# 检查父级是否存在
|
||||
if data.parent_id:
|
||||
parent = db.query(ProductCategory).filter(ProductCategory.id == data.parent_id).first()
|
||||
if not parent:
|
||||
raise HTTPException(status_code=400, detail="父级分类不存在")
|
||||
|
||||
category = ProductCategory(
|
||||
name=data.name,
|
||||
code=data.code,
|
||||
parent_id=data.parent_id,
|
||||
commission_rate=data.commission_rate,
|
||||
monthly_rebate=data.monthly_rebate,
|
||||
quarterly_rebate=data.quarterly_rebate,
|
||||
is_main_product=data.is_main_product,
|
||||
sort_order=data.sort_order,
|
||||
status=1
|
||||
)
|
||||
db.add(category)
|
||||
db.commit()
|
||||
db.refresh(category)
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "CREATE_CATEGORY", "category", category.id,
|
||||
new_value=f"创建分类: {data.name}, code={data.code}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "分类创建成功",
|
||||
"data": {
|
||||
"id": category.id,
|
||||
"name": category.name,
|
||||
"code": category.code,
|
||||
"parent_id": category.parent_id,
|
||||
"commission_rate": str(category.commission_rate),
|
||||
"monthly_rebate": str(category.monthly_rebate),
|
||||
"quarterly_rebate": str(category.quarterly_rebate) if category.quarterly_rebate else None,
|
||||
"is_main_product": category.is_main_product,
|
||||
"sort_order": category.sort_order,
|
||||
"status": category.status,
|
||||
"created_at": category.created_at.isoformat() if category.created_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{category_id}", response_model=dict)
|
||||
def update_category(
|
||||
category_id: int,
|
||||
data: CategoryUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""更新分类"""
|
||||
category = db.query(ProductCategory).filter(ProductCategory.id == category_id).first()
|
||||
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
|
||||
# 如果更换父级,检查是否存在且不会形成循环
|
||||
if data.parent_id is not None and data.parent_id != category.parent_id:
|
||||
if data.parent_id == category_id:
|
||||
raise HTTPException(status_code=400, detail="不能将自己设为父级")
|
||||
parent = db.query(ProductCategory).filter(ProductCategory.id == data.parent_id).first()
|
||||
if not parent:
|
||||
raise HTTPException(status_code=400, detail="父级分类不存在")
|
||||
# 检查是否会成为子分类的子级(循环引用)
|
||||
def check_circular(parent_id: int, target_id: int) -> bool:
|
||||
if parent_id == target_id:
|
||||
return True
|
||||
children = db.query(ProductCategory).filter(ProductCategory.parent_id == parent_id).all()
|
||||
for child in children:
|
||||
if check_circular(child.id, target_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
if check_circular(category_id, data.parent_id):
|
||||
raise HTTPException(status_code=400, detail="不能将分类设为其子分类的子级")
|
||||
|
||||
# 检查code是否已存在
|
||||
if data.code and data.code != category.code:
|
||||
existing = db.query(ProductCategory).filter(ProductCategory.code == data.code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="分类编码已存在")
|
||||
|
||||
# 记录旧值
|
||||
old_value = f"name={category.name}, code={category.code}, commission_rate={category.commission_rate}"
|
||||
|
||||
# 更新分类信息
|
||||
if data.name is not None:
|
||||
category.name = data.name
|
||||
if data.code is not None:
|
||||
category.code = data.code
|
||||
if data.parent_id is not None:
|
||||
category.parent_id = data.parent_id
|
||||
if data.commission_rate is not None:
|
||||
category.commission_rate = data.commission_rate
|
||||
if data.monthly_rebate is not None:
|
||||
category.monthly_rebate = data.monthly_rebate
|
||||
if data.quarterly_rebate is not None:
|
||||
category.quarterly_rebate = data.quarterly_rebate
|
||||
if data.is_main_product is not None:
|
||||
category.is_main_product = data.is_main_product
|
||||
if data.sort_order is not None:
|
||||
category.sort_order = data.sort_order
|
||||
if data.status is not None:
|
||||
category.status = data.status
|
||||
|
||||
db.commit()
|
||||
db.refresh(category)
|
||||
|
||||
# 记录操作日志
|
||||
new_value = f"name={category.name}, code={category.code}, commission_rate={category.commission_rate}"
|
||||
log_operation(db, current_admin.id, "UPDATE_CATEGORY", "category", category_id,
|
||||
old_value=old_value, new_value=new_value)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "分类更新成功",
|
||||
"data": {
|
||||
"id": category.id,
|
||||
"name": category.name,
|
||||
"code": category.code,
|
||||
"parent_id": category.parent_id,
|
||||
"commission_rate": str(category.commission_rate),
|
||||
"monthly_rebate": str(category.monthly_rebate),
|
||||
"quarterly_rebate": str(category.quarterly_rebate) if category.quarterly_rebate else None,
|
||||
"is_main_product": category.is_main_product,
|
||||
"sort_order": category.sort_order,
|
||||
"status": category.status,
|
||||
"updated_at": category.updated_at.isoformat() if category.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{category_id}", response_model=dict)
|
||||
def delete_category(
|
||||
category_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""删除分类"""
|
||||
category = db.query(ProductCategory).filter(ProductCategory.id == category_id).first()
|
||||
|
||||
if not category:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
|
||||
# 检查是否有子分类
|
||||
children = db.query(ProductCategory).filter(ProductCategory.parent_id == category_id).first()
|
||||
if children:
|
||||
raise HTTPException(status_code=400, detail="该分类下有子分类,请先删除子分类")
|
||||
|
||||
# 检查是否有关联的业绩记录
|
||||
from app.models import PerformanceRecord
|
||||
records = db.query(PerformanceRecord).filter(PerformanceRecord.category_id == category_id).first()
|
||||
if records:
|
||||
raise HTTPException(status_code=400, detail="该分类已关联业绩记录,无法删除")
|
||||
|
||||
name = category.name
|
||||
|
||||
# 删除分类
|
||||
db.delete(category)
|
||||
db.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "DELETE_CATEGORY", "category", category_id,
|
||||
old_value=f"删除分类: {name}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "分类删除成功",
|
||||
"data": None
|
||||
}
|
||||
|
||||
|
||||
@router.post("/import", response_model=dict)
|
||||
def preview_import_categories(
|
||||
data: CategoryImportData,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""火山引擎清单导入(预览)"""
|
||||
import uuid
|
||||
|
||||
batch_id = str(uuid.uuid4())[:8]
|
||||
preview_items = []
|
||||
errors = []
|
||||
|
||||
# 收集现有分类编码
|
||||
existing_codes = {c.code: c for c in db.query(ProductCategory).all() if c.code}
|
||||
|
||||
# 构建编码到ID的映射(用于父级关联)
|
||||
code_to_id = {}
|
||||
new_code_to_index = {}
|
||||
|
||||
for idx, item in enumerate(data.items):
|
||||
if not item.code:
|
||||
errors.append({"index": idx, "message": "分类编码不能为空"})
|
||||
continue
|
||||
|
||||
if not item.name:
|
||||
errors.append({"index": idx, "message": "分类名称不能为空"})
|
||||
continue
|
||||
|
||||
# 检查编码是否重复(在当前导入数据中)
|
||||
if item.code in new_code_to_index:
|
||||
errors.append({"index": idx, "message": f"编码 '{item.code}' 在导入数据中重复"})
|
||||
continue
|
||||
|
||||
new_code_to_index[item.code] = idx
|
||||
|
||||
# 判断是新增还是更新
|
||||
action = "update" if item.code in existing_codes else "create"
|
||||
existing = existing_codes.get(item.code)
|
||||
|
||||
preview_item = {
|
||||
"index": idx,
|
||||
"code": item.code,
|
||||
"name": item.name,
|
||||
"parent_code": item.parent_code,
|
||||
"commission_rate": str(item.commission_rate),
|
||||
"monthly_rebate": str(item.monthly_rebate),
|
||||
"quarterly_rebate": str(item.quarterly_rebate) if item.quarterly_rebate else None,
|
||||
"is_main_product": item.is_main_product,
|
||||
"action": action,
|
||||
"existing_id": existing.id if existing else None,
|
||||
"existing_name": existing.name if existing else None
|
||||
}
|
||||
preview_items.append(preview_item)
|
||||
|
||||
# 检查父级编码是否存在
|
||||
for item in preview_items:
|
||||
if item["parent_code"]:
|
||||
if item["parent_code"] not in existing_codes and item["parent_code"] not in new_code_to_index:
|
||||
errors.append({"index": item["index"], "message": f"父级编码 '{item['parent_code']}' 不存在"})
|
||||
|
||||
# 保存预览数据到缓存
|
||||
preview_data = {
|
||||
"batch_id": batch_id,
|
||||
"source_file": data.source_file,
|
||||
"items": data.items,
|
||||
"preview": preview_items,
|
||||
"errors": errors,
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
_import_preview_cache[batch_id] = preview_data
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "预览生成成功",
|
||||
"data": {
|
||||
"batch_id": batch_id,
|
||||
"total": len(data.items),
|
||||
"create_count": len([p for p in preview_items if p["action"] == "create"]),
|
||||
"update_count": len([p for p in preview_items if p["action"] == "update"]),
|
||||
"error_count": len(errors),
|
||||
"preview": preview_items,
|
||||
"errors": errors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/import/confirm", response_model=dict)
|
||||
def confirm_import_categories(
|
||||
batch_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""确认导入"""
|
||||
if batch_id not in _import_preview_cache:
|
||||
raise HTTPException(status_code=404, detail="导入批次不存在或已过期")
|
||||
|
||||
preview_data = _import_preview_cache[batch_id]
|
||||
|
||||
if preview_data["errors"]:
|
||||
raise HTTPException(status_code=400, detail="导入数据存在错误,无法确认导入")
|
||||
|
||||
items = preview_data["items"]
|
||||
source_file = preview_data["source_file"]
|
||||
|
||||
# 收集现有分类
|
||||
existing_codes = {c.code: c for c in db.query(ProductCategory).all() if c.code}
|
||||
|
||||
# 第一步:创建/更新所有分类(不处理parent_id)
|
||||
created_items = {}
|
||||
updated_count = 0
|
||||
created_count = 0
|
||||
|
||||
for item in items:
|
||||
if item.code in existing_codes:
|
||||
# 更新现有分类
|
||||
category = existing_codes[item.code]
|
||||
category.name = item.name
|
||||
category.commission_rate = item.commission_rate
|
||||
category.monthly_rebate = item.monthly_rebate
|
||||
category.quarterly_rebate = item.quarterly_rebate
|
||||
category.is_main_product = item.is_main_product
|
||||
category.source_file = source_file
|
||||
category.import_batch = batch_id
|
||||
category.last_import_at = datetime.now()
|
||||
updated_count += 1
|
||||
else:
|
||||
# 创建新分类
|
||||
category = ProductCategory(
|
||||
name=item.name,
|
||||
code=item.code,
|
||||
commission_rate=item.commission_rate,
|
||||
monthly_rebate=item.monthly_rebate,
|
||||
quarterly_rebate=item.quarterly_rebate,
|
||||
is_main_product=item.is_main_product,
|
||||
source_file=source_file,
|
||||
import_batch=batch_id,
|
||||
last_import_at=datetime.now(),
|
||||
status=1
|
||||
)
|
||||
db.add(category)
|
||||
db.flush() # 获取ID
|
||||
created_count += 1
|
||||
|
||||
created_items[item.code] = category
|
||||
|
||||
# 第二步:处理父级关联
|
||||
for item in items:
|
||||
if item.parent_code:
|
||||
category = created_items[item.code]
|
||||
if item.parent_code in created_items:
|
||||
category.parent_id = created_items[item.parent_code].id
|
||||
elif item.parent_code in existing_codes:
|
||||
category.parent_id = existing_codes[item.parent_code].id
|
||||
|
||||
db.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "IMPORT_CATEGORIES", "category", None,
|
||||
new_value=f"导入分类: 创建{created_count}个, 更新{updated_count}个, 来源:{source_file}")
|
||||
|
||||
# 清理缓存
|
||||
del _import_preview_cache[batch_id]
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "导入成功",
|
||||
"data": {
|
||||
"batch_id": batch_id,
|
||||
"created": created_count,
|
||||
"updated": updated_count,
|
||||
"total": len(items)
|
||||
}
|
||||
}
|
||||
172
backend/app/routers/dashboard.py
Normal file
172
backend/app/routers/dashboard.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from typing import Optional, Literal
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_, extract
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import (
|
||||
User, Employee, SecondaryAgent, ProductCategory,
|
||||
PerformanceRecord, CalculationResult
|
||||
)
|
||||
from app.routers.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/dashboard", tags=["数据看板"])
|
||||
|
||||
|
||||
@router.get("/summary", response_model=dict)
|
||||
def get_dashboard_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取仪表盘汇总数据"""
|
||||
# 本月业绩总额
|
||||
now = datetime.now()
|
||||
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
monthly_performance = db.query(
|
||||
func.coalesce(func.sum(PerformanceRecord.amount), Decimal("0"))
|
||||
).filter(
|
||||
PerformanceRecord.record_date >= start_of_month.date()
|
||||
).scalar()
|
||||
|
||||
# 员工收益总额(本月)
|
||||
monthly_income = db.query(
|
||||
func.coalesce(func.sum(CalculationResult.total_income), Decimal("0"))
|
||||
).filter(
|
||||
CalculationResult.calc_year == now.year,
|
||||
CalculationResult.calc_month == now.month
|
||||
).scalar()
|
||||
|
||||
# 员工总数
|
||||
employee_count = db.query(Employee).join(User).filter(User.status == 1).count()
|
||||
|
||||
# 二级代理总数
|
||||
agent_count = db.query(SecondaryAgent).join(User).filter(User.status == 1).count()
|
||||
|
||||
# 产品分类数
|
||||
category_count = db.query(ProductCategory).filter(ProductCategory.status == 1).count()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"total_performance": str(monthly_performance),
|
||||
"total_income": str(monthly_income),
|
||||
"employee_count": employee_count,
|
||||
"agent_count": agent_count,
|
||||
"category_count": category_count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/chart", response_model=dict)
|
||||
def get_dashboard_chart(
|
||||
type: Literal["performance", "category"] = Query(..., description="图表类型"),
|
||||
months: int = Query(6, ge=1, le=12, description="显示月数"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取仪表盘图表数据"""
|
||||
|
||||
if type == "performance":
|
||||
# 业绩趋势数据
|
||||
end_date = datetime.now()
|
||||
data = []
|
||||
|
||||
for i in range(months - 1, -1, -1):
|
||||
month_date = end_date - timedelta(days=i * 30)
|
||||
start_of_month = month_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
if month_date.month == 12:
|
||||
end_of_month = month_date.replace(year=month_date.year + 1, month=1, day=1)
|
||||
else:
|
||||
end_of_month = month_date.replace(month=month_date.month + 1, day=1)
|
||||
|
||||
amount = db.query(
|
||||
func.coalesce(func.sum(PerformanceRecord.amount), Decimal("0"))
|
||||
).filter(
|
||||
PerformanceRecord.record_date >= start_of_month.date(),
|
||||
PerformanceRecord.record_date < end_of_month.date()
|
||||
).scalar()
|
||||
|
||||
data.append({
|
||||
"month": month_date.strftime("%Y-%m"),
|
||||
"amount": float(amount)
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
elif type == "category":
|
||||
# 产品分类占比数据
|
||||
results = db.query(
|
||||
ProductCategory.name,
|
||||
func.coalesce(func.sum(PerformanceRecord.amount), Decimal("0")).label("amount")
|
||||
).outerjoin(
|
||||
PerformanceRecord, ProductCategory.id == PerformanceRecord.category_id
|
||||
).filter(
|
||||
ProductCategory.status == 1
|
||||
).group_by(ProductCategory.id).all()
|
||||
|
||||
data = [
|
||||
{"name": name, "amount": float(amount)}
|
||||
for name, amount in results if amount > 0
|
||||
]
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
return {
|
||||
"code": 400,
|
||||
"message": "不支持的图表类型",
|
||||
"data": []
|
||||
}
|
||||
|
||||
|
||||
@router.get("/recent-performance", response_model=dict)
|
||||
def get_recent_performance(
|
||||
limit: int = Query(5, ge=1, le=20),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取最近业绩记录"""
|
||||
records = db.query(PerformanceRecord).order_by(
|
||||
PerformanceRecord.record_date.desc()
|
||||
).limit(limit).all()
|
||||
|
||||
data = []
|
||||
for record in records:
|
||||
employee_name = None
|
||||
if record.employee_id:
|
||||
emp = db.query(Employee).filter(Employee.id == record.employee_id).first()
|
||||
if emp and emp.user:
|
||||
employee_name = emp.user.name
|
||||
|
||||
category_name = None
|
||||
if record.category_id:
|
||||
cat = db.query(ProductCategory).filter(ProductCategory.id == record.category_id).first()
|
||||
if cat:
|
||||
category_name = cat.name
|
||||
|
||||
data.append({
|
||||
"id": record.id,
|
||||
"record_date": record.record_date.isoformat() if record.record_date else None,
|
||||
"employee_name": employee_name,
|
||||
"category_name": category_name,
|
||||
"amount": str(record.amount),
|
||||
"customer_name": record.customer_name,
|
||||
"order_no": record.order_no
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
421
backend/app/routers/employees.py
Normal file
421
backend/app/routers/employees.py
Normal file
@@ -0,0 +1,421 @@
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User, Employee, OperationLog
|
||||
from app.routers.auth import get_current_user, get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/employees", tags=["员工管理"])
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class EmployeeTarget(BaseModel):
|
||||
monthly_target: Decimal = Field(default=0)
|
||||
quarterly_target: Decimal = Field(default=0)
|
||||
half_year_target: Decimal = Field(default=0)
|
||||
yearly_target: Decimal = Field(default=0)
|
||||
|
||||
|
||||
class EmployeeBase(BaseModel):
|
||||
name: str
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
department: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
base_salary: Decimal = Field(default=4000)
|
||||
hire_date: Optional[date] = None
|
||||
|
||||
|
||||
class EmployeeCreate(EmployeeBase):
|
||||
username: str
|
||||
password: str
|
||||
targets: Optional[EmployeeTarget] = None
|
||||
|
||||
|
||||
class EmployeeUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
department: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
base_salary: Optional[Decimal] = None
|
||||
hire_date: Optional[date] = None
|
||||
status: Optional[int] = None
|
||||
|
||||
|
||||
class EmployeeResponse(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
username: str
|
||||
name: str
|
||||
phone: Optional[str]
|
||||
email: Optional[str]
|
||||
department: Optional[str]
|
||||
position: Optional[str]
|
||||
base_salary: Decimal
|
||||
monthly_target: Decimal
|
||||
quarterly_target: Decimal
|
||||
half_year_target: Decimal
|
||||
yearly_target: Decimal
|
||||
hire_date: Optional[date]
|
||||
status: int
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EmployeeListResponse(BaseModel):
|
||||
items: List[EmployeeResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
def log_operation(db: Session, user_id: int, action: str, target_type: str, target_id: int,
|
||||
old_value: Optional[str] = None, new_value: Optional[str] = None):
|
||||
"""记录操作日志"""
|
||||
log = OperationLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
old_value=old_value,
|
||||
new_value=new_value
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
def get_employees(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=1000),
|
||||
search: Optional[str] = None,
|
||||
department: Optional[str] = None,
|
||||
status: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取员工列表(支持分页、搜索)"""
|
||||
query = db.query(Employee).join(User)
|
||||
|
||||
# 搜索条件
|
||||
if search:
|
||||
query = query.filter(
|
||||
(User.name.contains(search)) |
|
||||
(User.username.contains(search)) |
|
||||
(User.phone.contains(search))
|
||||
)
|
||||
|
||||
# 部门筛选
|
||||
if department:
|
||||
query = query.filter(Employee.department == department)
|
||||
|
||||
# 状态筛选
|
||||
if status is not None:
|
||||
query = query.filter(User.status == status)
|
||||
|
||||
# 计算总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
employees = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# 构建响应数据
|
||||
items = []
|
||||
for emp in employees:
|
||||
items.append({
|
||||
"id": emp.id,
|
||||
"user_id": emp.user_id,
|
||||
"username": emp.user.username,
|
||||
"name": emp.user.name,
|
||||
"phone": emp.user.phone,
|
||||
"email": emp.user.email,
|
||||
"department": emp.department,
|
||||
"position": emp.position,
|
||||
"base_salary": str(emp.base_salary),
|
||||
"monthly_target": str(emp.monthly_target),
|
||||
"quarterly_target": str(emp.quarterly_target),
|
||||
"half_year_target": str(emp.half_year_target),
|
||||
"yearly_target": str(emp.yearly_target),
|
||||
"hire_date": emp.hire_date.isoformat() if emp.hire_date else None,
|
||||
"status": emp.user.status,
|
||||
"created_at": emp.created_at.isoformat() if emp.created_at else None,
|
||||
"updated_at": emp.updated_at.isoformat() if emp.updated_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{employee_id}", response_model=dict)
|
||||
def get_employee(
|
||||
employee_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取员工详情"""
|
||||
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
||||
|
||||
if not employee:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": employee.id,
|
||||
"user_id": employee.user_id,
|
||||
"username": employee.user.username,
|
||||
"name": employee.user.name,
|
||||
"phone": employee.user.phone,
|
||||
"email": employee.user.email,
|
||||
"department": employee.department,
|
||||
"position": employee.position,
|
||||
"base_salary": str(employee.base_salary),
|
||||
"monthly_target": str(employee.monthly_target),
|
||||
"quarterly_target": str(employee.quarterly_target),
|
||||
"half_year_target": str(employee.half_year_target),
|
||||
"yearly_target": str(employee.yearly_target),
|
||||
"hire_date": employee.hire_date.isoformat() if employee.hire_date else None,
|
||||
"status": employee.user.status,
|
||||
"created_at": employee.created_at.isoformat() if employee.created_at else None,
|
||||
"updated_at": employee.updated_at.isoformat() if employee.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
def create_employee(
|
||||
data: EmployeeCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""创建员工(同时创建用户账号)"""
|
||||
# 检查用户名是否已存在
|
||||
existing_user = db.query(User).filter(User.username == data.username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
# 检查手机号是否已存在
|
||||
if data.phone:
|
||||
existing_phone = db.query(User).filter(User.phone == data.phone).first()
|
||||
if existing_phone:
|
||||
raise HTTPException(status_code=400, detail="手机号已存在")
|
||||
|
||||
# 创建用户
|
||||
from app.auth import get_password_hash
|
||||
user = User(
|
||||
username=data.username,
|
||||
password_hash=get_password_hash(data.password),
|
||||
role="employee",
|
||||
name=data.name,
|
||||
phone=data.phone,
|
||||
email=data.email,
|
||||
status=1
|
||||
)
|
||||
db.add(user)
|
||||
db.flush() # 获取user.id
|
||||
|
||||
# 创建员工记录
|
||||
targets = data.targets or EmployeeTarget()
|
||||
employee = Employee(
|
||||
user_id=user.id,
|
||||
base_salary=data.base_salary,
|
||||
monthly_target=targets.monthly_target,
|
||||
quarterly_target=targets.quarterly_target,
|
||||
half_year_target=targets.half_year_target,
|
||||
yearly_target=targets.yearly_target,
|
||||
hire_date=data.hire_date,
|
||||
department=data.department,
|
||||
position=data.position
|
||||
)
|
||||
db.add(employee)
|
||||
db.commit()
|
||||
db.refresh(employee)
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "CREATE_EMPLOYEE", "employee", employee.id,
|
||||
new_value=f"创建员工: {data.name}, 用户名: {data.username}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "员工创建成功",
|
||||
"data": {
|
||||
"id": employee.id,
|
||||
"user_id": employee.user_id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"phone": user.phone,
|
||||
"email": user.email,
|
||||
"department": employee.department,
|
||||
"position": employee.position,
|
||||
"base_salary": str(employee.base_salary),
|
||||
"monthly_target": str(employee.monthly_target),
|
||||
"quarterly_target": str(employee.quarterly_target),
|
||||
"half_year_target": str(employee.half_year_target),
|
||||
"yearly_target": str(employee.yearly_target),
|
||||
"hire_date": employee.hire_date.isoformat() if employee.hire_date else None,
|
||||
"status": user.status,
|
||||
"created_at": employee.created_at.isoformat() if employee.created_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{employee_id}", response_model=dict)
|
||||
def update_employee(
|
||||
employee_id: int,
|
||||
data: EmployeeUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""更新员工信息"""
|
||||
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
||||
|
||||
if not employee:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
user = employee.user
|
||||
|
||||
# 记录旧值
|
||||
old_value = f"name={user.name}, phone={user.phone}, department={employee.department}, position={employee.position}"
|
||||
|
||||
# 更新用户信息
|
||||
if data.name is not None:
|
||||
user.name = data.name
|
||||
if data.phone is not None:
|
||||
user.phone = data.phone
|
||||
if data.email is not None:
|
||||
user.email = data.email
|
||||
if data.status is not None:
|
||||
user.status = data.status
|
||||
|
||||
# 更新员工信息
|
||||
if data.department is not None:
|
||||
employee.department = data.department
|
||||
if data.position is not None:
|
||||
employee.position = data.position
|
||||
if data.base_salary is not None:
|
||||
employee.base_salary = data.base_salary
|
||||
if data.hire_date is not None:
|
||||
employee.hire_date = data.hire_date
|
||||
|
||||
db.commit()
|
||||
db.refresh(employee)
|
||||
|
||||
# 记录操作日志
|
||||
new_value = f"name={user.name}, phone={user.phone}, department={employee.department}, position={employee.position}"
|
||||
log_operation(db, current_admin.id, "UPDATE_EMPLOYEE", "employee", employee.id,
|
||||
old_value=old_value, new_value=new_value)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "员工信息更新成功",
|
||||
"data": {
|
||||
"id": employee.id,
|
||||
"user_id": employee.user_id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"phone": user.phone,
|
||||
"email": user.email,
|
||||
"department": employee.department,
|
||||
"position": employee.position,
|
||||
"base_salary": str(employee.base_salary),
|
||||
"monthly_target": str(employee.monthly_target),
|
||||
"quarterly_target": str(employee.quarterly_target),
|
||||
"half_year_target": str(employee.half_year_target),
|
||||
"yearly_target": str(employee.yearly_target),
|
||||
"hire_date": employee.hire_date.isoformat() if employee.hire_date else None,
|
||||
"status": user.status,
|
||||
"updated_at": employee.updated_at.isoformat() if employee.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{employee_id}", response_model=dict)
|
||||
def delete_employee(
|
||||
employee_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""删除员工"""
|
||||
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
||||
|
||||
if not employee:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
user_id = employee.user_id
|
||||
user_name = employee.user.name
|
||||
|
||||
# 删除员工(级联删除用户)
|
||||
db.delete(employee)
|
||||
db.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "DELETE_EMPLOYEE", "employee", employee_id,
|
||||
old_value=f"删除员工: {user_name}, user_id={user_id}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "员工删除成功",
|
||||
"data": None
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{employee_id}/targets", response_model=dict)
|
||||
def update_employee_targets(
|
||||
employee_id: int,
|
||||
data: EmployeeTarget,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""更新员工目标"""
|
||||
employee = db.query(Employee).filter(Employee.id == employee_id).first()
|
||||
|
||||
if not employee:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
# 记录旧值
|
||||
old_value = f"monthly={employee.monthly_target}, quarterly={employee.quarterly_target}, half_year={employee.half_year_target}, yearly={employee.yearly_target}"
|
||||
|
||||
# 更新目标
|
||||
employee.monthly_target = data.monthly_target
|
||||
employee.quarterly_target = data.quarterly_target
|
||||
employee.half_year_target = data.half_year_target
|
||||
employee.yearly_target = data.yearly_target
|
||||
|
||||
db.commit()
|
||||
db.refresh(employee)
|
||||
|
||||
# 记录操作日志
|
||||
new_value = f"monthly={data.monthly_target}, quarterly={data.quarterly_target}, half_year={data.half_year_target}, yearly={data.yearly_target}"
|
||||
log_operation(db, current_admin.id, "UPDATE_EMPLOYEE_TARGETS", "employee", employee_id,
|
||||
old_value=old_value, new_value=new_value)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "员工目标更新成功",
|
||||
"data": {
|
||||
"id": employee.id,
|
||||
"name": employee.user.name,
|
||||
"monthly_target": str(employee.monthly_target),
|
||||
"quarterly_target": str(employee.quarterly_target),
|
||||
"half_year_target": str(employee.half_year_target),
|
||||
"yearly_target": str(employee.yearly_target),
|
||||
"updated_at": employee.updated_at.isoformat() if employee.updated_at else None
|
||||
}
|
||||
}
|
||||
406
backend/app/routers/performance.py
Normal file
406
backend/app/routers/performance.py
Normal file
@@ -0,0 +1,406 @@
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User, Employee, SecondaryAgent, ProductCategory, PerformanceRecord, OperationLog
|
||||
from app.routers.auth import get_current_user, get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/performance", tags=["业绩管理"])
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class PerformanceBase(BaseModel):
|
||||
record_type: str = Field(default="employee", description="记录类型: employee/agent")
|
||||
employee_id: Optional[int] = None
|
||||
agent_id: Optional[int] = None
|
||||
category_id: int
|
||||
amount: Decimal = Field(gt=0)
|
||||
record_date: date
|
||||
customer_name: Optional[str] = None
|
||||
order_no: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class PerformanceCreate(PerformanceBase):
|
||||
pass
|
||||
|
||||
|
||||
class PerformanceUpdate(BaseModel):
|
||||
record_type: Optional[str] = None
|
||||
employee_id: Optional[int] = None
|
||||
agent_id: Optional[int] = None
|
||||
category_id: Optional[int] = None
|
||||
amount: Optional[Decimal] = None
|
||||
record_date: Optional[date] = None
|
||||
customer_name: Optional[str] = None
|
||||
order_no: Optional[str] = None
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class PerformanceResponse(BaseModel):
|
||||
id: int
|
||||
record_type: str
|
||||
employee_id: Optional[int]
|
||||
employee_name: Optional[str]
|
||||
agent_id: Optional[int]
|
||||
agent_name: Optional[str]
|
||||
category_id: int
|
||||
category_name: str
|
||||
amount: str
|
||||
record_date: str
|
||||
customer_name: Optional[str]
|
||||
order_no: Optional[str]
|
||||
remark: Optional[str]
|
||||
created_by: int
|
||||
created_by_name: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PerformanceListResponse(BaseModel):
|
||||
list: List[PerformanceResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
def log_operation(db: Session, user_id: int, action: str, target_type: str, target_id: int,
|
||||
old_value: Optional[str] = None, new_value: Optional[str] = None):
|
||||
"""记录操作日志"""
|
||||
log = OperationLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
old_value=old_value,
|
||||
new_value=new_value
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
def get_performance_list(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
employee_id: Optional[int] = None,
|
||||
agent_id: Optional[int] = None,
|
||||
category_id: Optional[int] = None,
|
||||
record_type: Optional[str] = None,
|
||||
start_date: Optional[date] = None,
|
||||
end_date: Optional[date] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取业绩列表"""
|
||||
query = db.query(PerformanceRecord)
|
||||
|
||||
# 筛选条件
|
||||
if employee_id:
|
||||
query = query.filter(PerformanceRecord.employee_id == employee_id)
|
||||
if agent_id:
|
||||
query = query.filter(PerformanceRecord.agent_id == agent_id)
|
||||
if category_id:
|
||||
query = query.filter(PerformanceRecord.category_id == category_id)
|
||||
if record_type:
|
||||
query = query.filter(PerformanceRecord.record_type == record_type)
|
||||
if start_date:
|
||||
query = query.filter(PerformanceRecord.record_date >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(PerformanceRecord.record_date <= end_date)
|
||||
|
||||
# 普通员工只能看自己的业绩
|
||||
if current_user.role == "employee":
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if employee:
|
||||
query = query.filter(PerformanceRecord.employee_id == employee.id)
|
||||
|
||||
# 计算总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
records = query.order_by(PerformanceRecord.record_date.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# 构建响应数据
|
||||
list_data = []
|
||||
for record in records:
|
||||
employee_name = None
|
||||
if record.employee_id:
|
||||
emp = db.query(Employee).filter(Employee.id == record.employee_id).first()
|
||||
if emp:
|
||||
employee_name = emp.user.name if emp.user else None
|
||||
|
||||
agent_name = None
|
||||
if record.agent_id:
|
||||
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == record.agent_id).first()
|
||||
if agent:
|
||||
agent_name = agent.company_name
|
||||
|
||||
category_name = ""
|
||||
if record.category_id:
|
||||
cat = db.query(ProductCategory).filter(ProductCategory.id == record.category_id).first()
|
||||
if cat:
|
||||
category_name = cat.name
|
||||
|
||||
created_by_name = ""
|
||||
if record.created_by:
|
||||
creator = db.query(User).filter(User.id == record.created_by).first()
|
||||
if creator:
|
||||
created_by_name = creator.name or creator.username
|
||||
|
||||
list_data.append({
|
||||
"id": record.id,
|
||||
"record_type": record.record_type,
|
||||
"employee_id": record.employee_id,
|
||||
"employee_name": employee_name,
|
||||
"agent_id": record.agent_id,
|
||||
"agent_name": agent_name,
|
||||
"category_id": record.category_id,
|
||||
"category_name": category_name,
|
||||
"amount": str(record.amount),
|
||||
"record_date": record.record_date.isoformat() if record.record_date else None,
|
||||
"customer_name": record.customer_name,
|
||||
"order_no": record.order_no,
|
||||
"remark": record.remark,
|
||||
"created_by": record.created_by,
|
||||
"created_by_name": created_by_name,
|
||||
"created_at": record.created_at.isoformat() if record.created_at else None,
|
||||
"updated_at": record.updated_at.isoformat() if record.updated_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": list_data,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{record_id}", response_model=dict)
|
||||
def get_performance_detail(
|
||||
record_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""获取业绩详情"""
|
||||
record = db.query(PerformanceRecord).filter(PerformanceRecord.id == record_id).first()
|
||||
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="业绩记录不存在")
|
||||
|
||||
# 普通员工只能看自己的业绩
|
||||
if current_user.role == "employee":
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if employee and record.employee_id != employee.id:
|
||||
raise HTTPException(status_code=403, detail="无权查看该业绩记录")
|
||||
|
||||
employee_name = None
|
||||
if record.employee_id:
|
||||
emp = db.query(Employee).filter(Employee.id == record.employee_id).first()
|
||||
if emp:
|
||||
employee_name = emp.user.name if emp.user else None
|
||||
|
||||
agent_name = None
|
||||
if record.agent_id:
|
||||
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == record.agent_id).first()
|
||||
if agent:
|
||||
agent_name = agent.company_name
|
||||
|
||||
category_name = ""
|
||||
if record.category_id:
|
||||
cat = db.query(ProductCategory).filter(ProductCategory.id == record.category_id).first()
|
||||
if cat:
|
||||
category_name = cat.name
|
||||
|
||||
created_by_name = ""
|
||||
if record.created_by:
|
||||
creator = db.query(User).filter(User.id == record.created_by).first()
|
||||
if creator:
|
||||
created_by_name = creator.name or creator.username
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": record.id,
|
||||
"record_type": record.record_type,
|
||||
"employee_id": record.employee_id,
|
||||
"employee_name": employee_name,
|
||||
"agent_id": record.agent_id,
|
||||
"agent_name": agent_name,
|
||||
"category_id": record.category_id,
|
||||
"category_name": category_name,
|
||||
"amount": str(record.amount),
|
||||
"record_date": record.record_date.isoformat() if record.record_date else None,
|
||||
"customer_name": record.customer_name,
|
||||
"order_no": record.order_no,
|
||||
"remark": record.remark,
|
||||
"created_by": record.created_by,
|
||||
"created_by_name": created_by_name,
|
||||
"created_at": record.created_at.isoformat() if record.created_at else None,
|
||||
"updated_at": record.updated_at.isoformat() if record.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
def create_performance(
|
||||
data: PerformanceCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建业绩记录"""
|
||||
# 验证员工或代理是否存在
|
||||
if data.record_type == "employee" and data.employee_id:
|
||||
employee = db.query(Employee).filter(Employee.id == data.employee_id).first()
|
||||
if not employee:
|
||||
raise HTTPException(status_code=400, detail="员工不存在")
|
||||
elif data.record_type == "agent" and data.agent_id:
|
||||
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == data.agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=400, detail="代理不存在")
|
||||
|
||||
# 验证产品分类是否存在
|
||||
category = db.query(ProductCategory).filter(ProductCategory.id == data.category_id).first()
|
||||
if not category:
|
||||
raise HTTPException(status_code=400, detail="产品分类不存在")
|
||||
|
||||
# 创建记录
|
||||
record = PerformanceRecord(
|
||||
record_type=data.record_type,
|
||||
employee_id=data.employee_id,
|
||||
agent_id=data.agent_id,
|
||||
category_id=data.category_id,
|
||||
amount=data.amount,
|
||||
record_date=data.record_date,
|
||||
customer_name=data.customer_name,
|
||||
order_no=data.order_no,
|
||||
remark=data.remark,
|
||||
created_by=current_user.id
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_user.id, "CREATE_PERFORMANCE", "performance", record.id,
|
||||
new_value=f"创建业绩记录: 金额={data.amount}, 日期={data.record_date}")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "业绩记录创建成功",
|
||||
"data": {
|
||||
"id": record.id,
|
||||
"record_type": record.record_type,
|
||||
"employee_id": record.employee_id,
|
||||
"agent_id": record.agent_id,
|
||||
"category_id": record.category_id,
|
||||
"amount": str(record.amount),
|
||||
"record_date": record.record_date.isoformat() if record.record_date else None,
|
||||
"created_at": record.created_at.isoformat() if record.created_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{record_id}", response_model=dict)
|
||||
def update_performance(
|
||||
record_id: int,
|
||||
data: PerformanceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新业绩记录"""
|
||||
record = db.query(PerformanceRecord).filter(PerformanceRecord.id == record_id).first()
|
||||
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="业绩记录不存在")
|
||||
|
||||
# 记录旧值
|
||||
old_value = f"amount={record.amount}, date={record.record_date}, category_id={record.category_id}"
|
||||
|
||||
# 更新字段
|
||||
if data.record_type is not None:
|
||||
record.record_type = data.record_type
|
||||
if data.employee_id is not None:
|
||||
record.employee_id = data.employee_id
|
||||
if data.agent_id is not None:
|
||||
record.agent_id = data.agent_id
|
||||
if data.category_id is not None:
|
||||
# 验证产品分类是否存在
|
||||
category = db.query(ProductCategory).filter(ProductCategory.id == data.category_id).first()
|
||||
if not category:
|
||||
raise HTTPException(status_code=400, detail="产品分类不存在")
|
||||
record.category_id = data.category_id
|
||||
if data.amount is not None:
|
||||
record.amount = data.amount
|
||||
if data.record_date is not None:
|
||||
record.record_date = data.record_date
|
||||
if data.customer_name is not None:
|
||||
record.customer_name = data.customer_name
|
||||
if data.order_no is not None:
|
||||
record.order_no = data.order_no
|
||||
if data.remark is not None:
|
||||
record.remark = data.remark
|
||||
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
# 记录操作日志
|
||||
new_value = f"amount={record.amount}, date={record.record_date}, category_id={record.category_id}"
|
||||
log_operation(db, current_user.id, "UPDATE_PERFORMANCE", "performance", record_id,
|
||||
old_value=old_value, new_value=new_value)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "业绩记录更新成功",
|
||||
"data": {
|
||||
"id": record.id,
|
||||
"record_type": record.record_type,
|
||||
"employee_id": record.employee_id,
|
||||
"agent_id": record.agent_id,
|
||||
"category_id": record.category_id,
|
||||
"amount": str(record.amount),
|
||||
"record_date": record.record_date.isoformat() if record.record_date else None,
|
||||
"updated_at": record.updated_at.isoformat() if record.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{record_id}", response_model=dict)
|
||||
def delete_performance(
|
||||
record_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""删除业绩记录"""
|
||||
record = db.query(PerformanceRecord).filter(PerformanceRecord.id == record_id).first()
|
||||
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="业绩记录不存在")
|
||||
|
||||
# 记录旧值
|
||||
old_value = f"删除业绩记录: 金额={record.amount}, 日期={record.record_date}"
|
||||
|
||||
db.delete(record)
|
||||
db.commit()
|
||||
|
||||
# 记录操作日志
|
||||
log_operation(db, current_admin.id, "DELETE_PERFORMANCE", "performance", record_id,
|
||||
old_value=old_value)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "业绩记录删除成功",
|
||||
"data": None
|
||||
}
|
||||
150
backend/app/routers/reports.py
Normal file
150
backend/app/routers/reports.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User
|
||||
from app.routers.auth import get_current_user, get_current_admin
|
||||
from app.services.report_service import (
|
||||
export_employee_report,
|
||||
export_company_report,
|
||||
export_performance_report
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/reports", tags=["报表导出"])
|
||||
|
||||
|
||||
@router.get("/employee/{employee_id}/excel")
|
||||
def export_employee_excel(
|
||||
employee_id: int,
|
||||
period: str = Query(..., description="计算周期: monthly/quarterly/half_yearly/yearly"),
|
||||
year: int = Query(..., description="年份"),
|
||||
month: Optional[int] = Query(None, description="月份(月度周期需要)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""导出员工收益Excel报表"""
|
||||
# 权限检查:非管理员只能导出自己的报表
|
||||
if current_user.role != "admin":
|
||||
from app.models import Employee
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if not employee or employee.id != employee_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,只能导出自己的报表"
|
||||
)
|
||||
|
||||
try:
|
||||
excel_bytes, filename = export_employee_report(
|
||||
db=db,
|
||||
employee_id=employee_id,
|
||||
period=period,
|
||||
year=year,
|
||||
month=month
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
iter([excel_bytes]),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}"
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/company/excel")
|
||||
def export_company_excel(
|
||||
period: str = Query(..., description="计算周期: monthly/quarterly/half_yearly/yearly"),
|
||||
year: int = Query(..., description="年份"),
|
||||
month: Optional[int] = Query(None, description="月份(月度周期需要)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin)
|
||||
):
|
||||
"""导出公司收益Excel报表(仅管理员)"""
|
||||
try:
|
||||
excel_bytes, filename = export_company_report(
|
||||
db=db,
|
||||
period=period,
|
||||
year=year,
|
||||
month=month
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
iter([excel_bytes]),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}"
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/performance/excel")
|
||||
def export_performance_excel(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
employee_id: Optional[int] = Query(None, description="员工ID"),
|
||||
agent_id: Optional[int] = Query(None, description="代理ID"),
|
||||
category_id: Optional[int] = Query(None, description="分类ID"),
|
||||
record_type: Optional[str] = Query(None, description="记录类型: employee/agent"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""导出业绩报表Excel"""
|
||||
from datetime import datetime
|
||||
|
||||
# 构建筛选条件
|
||||
filters = {}
|
||||
|
||||
if start_date:
|
||||
try:
|
||||
filters['start_date'] = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="开始日期格式错误,应为YYYY-MM-DD")
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
filters['end_date'] = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="结束日期格式错误,应为YYYY-MM-DD")
|
||||
|
||||
# 权限检查:非管理员只能查看自己的业绩
|
||||
if current_user.role != "admin":
|
||||
from app.models import Employee
|
||||
employee = db.query(Employee).filter(Employee.user_id == current_user.id).first()
|
||||
if employee:
|
||||
filters['employee_id'] = employee.id
|
||||
else:
|
||||
if employee_id:
|
||||
filters['employee_id'] = employee_id
|
||||
|
||||
if agent_id:
|
||||
filters['agent_id'] = agent_id
|
||||
if category_id:
|
||||
filters['category_id'] = category_id
|
||||
if record_type:
|
||||
filters['record_type'] = record_type
|
||||
|
||||
try:
|
||||
excel_bytes, filename = export_performance_report(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
iter([excel_bytes]),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"导出失败: {str(e)}")
|
||||
88
backend/app/routers/settings.py
Normal file
88
backend/app/routers/settings.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Setting
|
||||
from app.routers.auth import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["系统设置"])
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class SettingItem(BaseModel):
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
settings: List[SettingItem]
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
def get_settings(db: Session = Depends(get_db), current_admin=Depends(get_current_admin)):
|
||||
"""获取所有设置(按分组)"""
|
||||
settings = db.query(Setting).order_by(Setting.group_name, Setting.sort_order).all()
|
||||
|
||||
# 按分组组织
|
||||
result = {}
|
||||
for setting in settings:
|
||||
group = setting.group_name or "general"
|
||||
if group not in result:
|
||||
result[group] = {}
|
||||
result[group][setting.setting_key] = setting.setting_value
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": result
|
||||
}
|
||||
|
||||
|
||||
@router.put("", response_model=dict)
|
||||
def update_settings(data: SettingsUpdate, db: Session = Depends(get_db), current_admin=Depends(get_current_admin)):
|
||||
"""批量更新设置"""
|
||||
updated_count = 0
|
||||
|
||||
for item in data.settings:
|
||||
setting = db.query(Setting).filter(Setting.setting_key == item.key).first()
|
||||
if setting:
|
||||
setting.setting_value = item.value
|
||||
updated_count += 1
|
||||
else:
|
||||
# 如果不存在则创建
|
||||
new_setting = Setting(
|
||||
setting_key=item.key,
|
||||
setting_value=item.value,
|
||||
description="",
|
||||
group_name="general"
|
||||
)
|
||||
db.add(new_setting)
|
||||
updated_count += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": f"成功更新 {updated_count} 项设置",
|
||||
"data": None
|
||||
}
|
||||
|
||||
|
||||
@router.get("/value/{key}", response_model=dict)
|
||||
def get_setting_value(key: str, db: Session = Depends(get_db), current_admin=Depends(get_current_admin)):
|
||||
"""获取单个设置值"""
|
||||
setting = db.query(Setting).filter(Setting.setting_key == key).first()
|
||||
if not setting:
|
||||
raise HTTPException(status_code=404, detail="设置项不存在")
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"key": setting.setting_key,
|
||||
"value": setting.setting_value,
|
||||
"description": setting.description
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user