初始化

This commit is contained in:
2026-04-13 14:22:31 +08:00
commit 7cf0a75603
78 changed files with 10702 additions and 0 deletions

Binary file not shown.

Binary file not shown.

View 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
View 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
}
}

View 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)}")

View 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)
}
}

View 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
}

View 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
}
}

View 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
}

View 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)}")

View 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
}
}