初始化

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

1
backend/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
# 使app目录成为Python包

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

40
backend/app/auth.py Normal file
View File

@@ -0,0 +1,40 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.config import get_settings
settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""获取密码哈希"""
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""创建JWT访问令牌"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_token(token: str) -> Optional[dict]:
"""解码JWT令牌"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None

38
backend/app/config.py Normal file
View File

@@ -0,0 +1,38 @@
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
# 应用配置
APP_NAME: str = "销售业绩管理系统"
DEBUG: bool = True
# 数据库配置
DATABASE_URL: str = "sqlite:///./sales_management.db"
# JWT配置
SECRET_KEY: str = "your-secret-key-here-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 # 24小时
# 默认配置
DEFAULT_SALARY_BASE_MIN: float = 4000.0
DEFAULT_SALARY_BASE_MAX: float = 6000.0
DEFAULT_SALARY_PERFORMANCE: float = 1000.0
DEFAULT_SALARY_PERFORMANCE_THRESHOLD: float = 50.0
DEFAULT_COMMISSION_AI_MODEL: float = 0.03
DEFAULT_COMMISSION_CLOUD_BASE: float = 0.10
DEFAULT_COMMISSION_MAIN_PRODUCT: float = 0.08
DEFAULT_AGENT_EMPLOYEE_COMMISSION: float = 0.01
DEFAULT_AGENT_PROFIT_SHARE_MIN: float = 0.60
DEFAULT_AGENT_PROFIT_SHARE_MAX: float = 0.65
class Config:
env_file = ".env"
@lru_cache()
def get_settings():
return Settings()

33
backend/app/database.py Normal file
View File

@@ -0,0 +1,33 @@
from sqlalchemy import create_engine, event
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from app.config import get_settings
settings = get_settings()
# 创建引擎
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {},
echo=settings.DEBUG
)
# 会话工厂
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 模型基类
Base = declarative_base()
def get_db() -> Session:
"""获取数据库会话"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
"""初始化数据库"""
Base.metadata.create_all(bind=engine)

80
backend/app/init_data.py Normal file
View File

@@ -0,0 +1,80 @@
from sqlalchemy.orm import Session
from app.models import User, Setting, ProductCategory
from app.config import get_settings
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
settings = get_settings()
def init_default_data(db: Session):
"""初始化默认数据"""
# 1. 创建默认管理员账号
admin = db.query(User).filter(User.username == "admin").first()
if not admin:
admin = User(
username="admin",
password_hash=pwd_context.hash("admin123"),
role="admin",
name="系统管理员",
status=1
)
db.add(admin)
db.commit()
print("✅ 默认管理员账号已创建")
print(" 用户名: admin")
print(" 密码: admin123")
# 2. 初始化默认配置
default_settings = [
("salary_base_min", str(settings.DEFAULT_SALARY_BASE_MIN), "底薪范围最小值(元)", "salary"),
("salary_base_max", str(settings.DEFAULT_SALARY_BASE_MAX), "底薪范围最大值(元)", "salary"),
("salary_performance", str(settings.DEFAULT_SALARY_PERFORMANCE), "绩效奖金基数(元)", "salary"),
("salary_performance_threshold", str(settings.DEFAULT_SALARY_PERFORMANCE_THRESHOLD), "绩效完成率阈值(%", "salary"),
("commission_ai_model", str(settings.DEFAULT_COMMISSION_AI_MODEL), "大模型类产品提成比例", "commission"),
("commission_cloud_base", str(settings.DEFAULT_COMMISSION_CLOUD_BASE), "云基础类产品提成比例", "commission"),
("commission_main_product", str(settings.DEFAULT_COMMISSION_MAIN_PRODUCT), "主推产品提成比例", "commission"),
("agent_employee_commission", str(settings.DEFAULT_AGENT_EMPLOYEE_COMMISSION), "员工二级代理提成比例", "agent"),
("agent_profit_share_min", str(settings.DEFAULT_AGENT_PROFIT_SHARE_MIN), "二级代理分佣比例最小值", "agent"),
("agent_profit_share_max", str(settings.DEFAULT_AGENT_PROFIT_SHARE_MAX), "二级代理分佣比例最大值", "agent"),
("company_name", "火山引擎渠道合作伙伴", "公司名称", "company"),
]
for key, value, desc, group in default_settings:
existing = db.query(Setting).filter(Setting.setting_key == key).first()
if not existing:
setting = Setting(
setting_key=key,
setting_value=value,
description=desc,
group_name=group
)
db.add(setting)
db.commit()
print("✅ 默认系统配置已初始化")
# 3. 初始化默认产品分类
default_categories = [
("大模型类产品", "ai_model", 0.03, 0.30, 0),
("云基础类产品", "cloud_base", 0.10, 0.25, 0),
("主推产品-直播大师", "main_live_master", 0.08, 0.35, 1),
("主推产品-创作Agent", "main_create_agent", 0.08, 0.35, 1),
]
for name, code, commission, rebate, is_main in default_categories:
existing = db.query(ProductCategory).filter(ProductCategory.code == code).first()
if not existing:
category = ProductCategory(
name=name,
code=code,
commission_rate=commission,
monthly_rebate=rebate,
is_main_product=is_main,
status=1
)
db.add(category)
db.commit()
print("✅ 默认产品分类已初始化")

48
backend/app/main.py Normal file
View File

@@ -0,0 +1,48 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import get_settings
from app.routers import auth, settings as settings_router, employees, agents, categories, performance, calculate, reports, dashboard
settings = get_settings()
app = FastAPI(
title=settings.APP_NAME,
description="销售业绩与收益计算管理系统 API",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境应限制具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(auth.router, prefix="/api/v1")
app.include_router(settings_router.router, prefix="/api/v1")
app.include_router(employees.router, prefix="/api/v1")
app.include_router(agents.router, prefix="/api/v1")
app.include_router(categories.router, prefix="/api/v1")
app.include_router(performance.router, prefix="/api/v1")
app.include_router(calculate.router, prefix="/api/v1")
app.include_router(reports.router, prefix="/api/v1")
app.include_router(dashboard.router, prefix="/api/v1")
@app.get("/")
def root():
return {
"message": settings.APP_NAME,
"version": "1.0.0",
"docs": "/docs"
}
@app.get("/health")
def health_check():
return {"status": "ok"}

198
backend/app/models.py Normal file
View File

@@ -0,0 +1,198 @@
from sqlalchemy import Column, Integer, String, DateTime, Date, DECIMAL, Text, ForeignKey, CheckConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
username = Column(String(50), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False, default="employee") # admin/employee/agent
name = Column(String(50), nullable=False)
phone = Column(String(20))
email = Column(String(100))
status = Column(Integer, nullable=False, default=1) # 0-禁用1-启用
last_login_at = Column(DateTime)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# 关联关系
employee = relationship("Employee", back_populates="user", uselist=False)
agent = relationship("SecondaryAgent", back_populates="user", uselist=False)
__table_args__ = (
CheckConstraint(role.in_(['admin', 'employee', 'agent'])),
)
class Employee(Base):
__tablename__ = "employees"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
base_salary = Column(DECIMAL(10, 2), nullable=False, default=4000.00)
monthly_target = Column(DECIMAL(15, 2), nullable=False, default=0.00)
quarterly_target = Column(DECIMAL(15, 2), nullable=False, default=0.00)
half_year_target = Column(DECIMAL(15, 2), nullable=False, default=0.00)
yearly_target = Column(DECIMAL(15, 2), nullable=False, default=0.00)
hire_date = Column(Date)
department = Column(String(50))
position = Column(String(50))
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# 关联关系
user = relationship("User", back_populates="employee")
agents = relationship("SecondaryAgent", back_populates="employee")
performance_records = relationship("PerformanceRecord", back_populates="employee")
calculation_results = relationship("CalculationResult", back_populates="employee")
class SecondaryAgent(Base):
__tablename__ = "secondary_agents"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
employee_id = Column(Integer, ForeignKey("employees.id", ondelete="CASCADE"), nullable=False)
company_name = Column(String(100), nullable=False)
contact_name = Column(String(50))
contact_phone = Column(String(20))
profit_share_rate = Column(DECIMAL(5, 2), nullable=False, default=0.60)
address = Column(String(255))
remark = Column(Text)
status = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# 关联关系
user = relationship("User", back_populates="agent")
employee = relationship("Employee", back_populates="agents")
performance_records = relationship("PerformanceRecord", back_populates="agent")
class Setting(Base):
__tablename__ = "settings"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
setting_key = Column(String(50), unique=True, nullable=False)
setting_value = Column(Text, nullable=False)
description = Column(String(255))
group_name = Column(String(50), default="general")
sort_order = Column(Integer, default=0)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
class ProductCategory(Base):
__tablename__ = "product_categories"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(50), nullable=False)
code = Column(String(30), unique=True)
parent_id = Column(Integer, ForeignKey("product_categories.id", ondelete="SET NULL"))
commission_rate = Column(DECIMAL(5, 2), nullable=False, default=0.03)
monthly_rebate = Column(DECIMAL(5, 2), nullable=False, default=0.10)
quarterly_rebate = Column(DECIMAL(5, 2))
is_main_product = Column(Integer, nullable=False, default=0)
sort_order = Column(Integer, default=0)
status = Column(Integer, nullable=False, default=1) # 0-下线/禁用1-启用
# 火山引擎导入相关
source_file = Column(String(100))
import_batch = Column(String(50))
last_import_at = Column(DateTime)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# 关联关系
parent = relationship("ProductCategory", remote_side=[id], backref="children")
performance_records = relationship("PerformanceRecord", back_populates="category")
class PerformanceRecord(Base):
__tablename__ = "performance_records"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
record_type = Column(String(20), nullable=False) # employee/agent
employee_id = Column(Integer, ForeignKey("employees.id", ondelete="SET NULL"))
agent_id = Column(Integer, ForeignKey("secondary_agents.id", ondelete="SET NULL"))
category_id = Column(Integer, ForeignKey("product_categories.id", ondelete="RESTRICT"), nullable=False)
amount = Column(DECIMAL(15, 2), nullable=False, default=0.00)
record_date = Column(Date, nullable=False)
customer_name = Column(String(100))
order_no = Column(String(50))
remark = Column(Text)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# 关联关系
employee = relationship("Employee", back_populates="performance_records")
agent = relationship("SecondaryAgent", back_populates="performance_records")
category = relationship("ProductCategory", back_populates="performance_records")
__table_args__ = (
CheckConstraint(record_type.in_(['employee', 'agent'])),
)
class CalculationResult(Base):
__tablename__ = "calculation_results"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
employee_id = Column(Integer, ForeignKey("employees.id", ondelete="CASCADE"), nullable=False)
calc_period = Column(String(20), nullable=False) # monthly/quarterly/half_yearly/yearly
calc_year = Column(Integer, nullable=False)
calc_month = Column(Integer)
calc_quarter = Column(Integer)
period_start_date = Column(Date, nullable=False)
period_end_date = Column(Date, nullable=False)
# 业绩数据
total_performance = Column(DECIMAL(15, 2), nullable=False, default=0.00)
target_amount = Column(DECIMAL(15, 2), nullable=False, default=0.00)
completion_rate = Column(DECIMAL(5, 2), nullable=False, default=0.00)
# 员工收益
base_salary = Column(DECIMAL(10, 2), nullable=False, default=0.00)
performance_bonus = Column(DECIMAL(10, 2), nullable=False, default=0.00)
personal_commission = Column(DECIMAL(15, 2), nullable=False, default=0.00)
agent_commission = Column(DECIMAL(15, 2), nullable=False, default=0.00)
total_income = Column(DECIMAL(15, 2), nullable=False, default=0.00)
# 公司收益
company_rebate = Column(DECIMAL(15, 2), nullable=False, default=0.00)
company_cost = Column(DECIMAL(15, 2), nullable=False, default=0.00)
company_profit = Column(DECIMAL(15, 2), nullable=False, default=0.00)
# 二级代理收益
agent_performance = Column(DECIMAL(15, 2), nullable=False, default=0.00)
agent_share_amount = Column(DECIMAL(15, 2), nullable=False, default=0.00)
# 计算详情JSON格式
detail_json = Column(Text)
created_at = Column(DateTime, server_default=func.now())
# 关联关系
employee = relationship("Employee", back_populates="calculation_results")
__table_args__ = (
CheckConstraint(calc_period.in_(['monthly', 'quarterly', 'half_yearly', 'yearly'])),
)
class OperationLog(Base):
__tablename__ = "operation_logs"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
action = Column(String(50), nullable=False)
target_type = Column(String(50))
target_id = Column(Integer)
old_value = Column(Text)
new_value = Column(Text)
ip_address = Column(String(50))
user_agent = Column(String(255))
created_at = Column(DateTime, server_default=func.now())

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

View File

@@ -0,0 +1,604 @@
from datetime import date, datetime
from decimal import Decimal
from typing import Optional, Dict, List, Any
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
import json
from app.models import (
Employee, SecondaryAgent, ProductCategory, PerformanceRecord,
CalculationResult, User
)
def get_period_dates(period: str, year: int, month: Optional[int] = None,
quarter: Optional[int] = None) -> tuple:
"""
根据周期类型获取开始和结束日期
Returns:
tuple: (start_date, end_date, period_key)
"""
if period == "monthly":
if month is None:
raise ValueError("月度周期需要提供month参数")
start_date = date(year, month, 1)
if month == 12:
end_date = date(year + 1, 1, 1)
else:
end_date = date(year, month + 1, 1)
period_key = f"{year}-{month:02d}"
elif period == "quarterly":
if quarter is None:
raise ValueError("季度周期需要提供quarter参数")
start_month = (quarter - 1) * 3 + 1
end_month = quarter * 3 + 1
start_date = date(year, start_month, 1)
if end_month > 12:
end_date = date(year + 1, 1, 1)
else:
end_date = date(year, end_month, 1)
period_key = f"{year}-Q{quarter}"
elif period == "half_yearly":
start_date = date(year, 1, 1)
end_date = date(year, 7, 1)
period_key = f"{year}-H1"
elif period == "yearly":
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
period_key = f"{year}"
else:
raise ValueError(f"不支持的周期类型: {period}")
return start_date, end_date, period_key
def get_target_by_period(employee: Employee, period: str) -> Decimal:
"""根据周期类型获取员工目标"""
if period == "monthly":
return employee.monthly_target or Decimal("0")
elif period == "quarterly":
return employee.quarterly_target or Decimal("0")
elif period == "half_yearly":
return employee.half_year_target or Decimal("0")
elif period == "yearly":
return employee.yearly_target or Decimal("0")
return Decimal("0")
def get_rebate_rate_by_period(category: ProductCategory, period: str) -> Decimal:
"""根据周期类型获取返点比例"""
if period == "monthly":
return category.monthly_rebate or Decimal("0")
elif period == "quarterly":
return category.quarterly_rebate or category.monthly_rebate or Decimal("0")
elif period == "half_yearly":
return category.quarterly_rebate or category.monthly_rebate or Decimal("0")
elif period == "yearly":
return category.quarterly_rebate or category.monthly_rebate or Decimal("0")
return Decimal("0")
def calculate_employee_income(
db: Session,
employee_id: int,
period: str,
year: int,
month: Optional[int] = None,
quarter: Optional[int] = None,
save_result: bool = True
) -> Dict[str, Any]:
"""
计算员工在指定周期的收益
计算逻辑:
1. 获取员工业绩数据根据period筛选
2. 计算完成率 = 总业绩 / 目标
3. 绩效奖金 = 1000 * min(完成率, 1.0) 如果完成率>=50%否则0
4. 个人提成 = 业绩按分类汇总 * 各分类提成比例
5. 代理提成 = 二级代理业绩总和 * 0.01
6. 总收入 = 底薪 + 绩效奖金 + 个人提成 + 代理提成
Args:
db: 数据库会话
employee_id: 员工ID
period: 周期类型 (monthly/quarterly/half_yearly/yearly)
year: 年份
month: 月份(月度周期需要)
quarter: 季度(季度周期需要)
save_result: 是否保存计算结果到数据库
Returns:
计算结果字典
"""
# 获取员工信息
employee = db.query(Employee).filter(Employee.id == employee_id).first()
if not employee:
raise ValueError(f"员工不存在: {employee_id}")
# 获取周期日期范围
start_date, end_date, period_key = get_period_dates(period, year, month, quarter)
# 获取目标金额
target_amount = get_target_by_period(employee, period)
# 1. 获取员工业绩数据(个人业绩)
personal_records = db.query(PerformanceRecord).filter(
and_(
PerformanceRecord.employee_id == employee_id,
PerformanceRecord.record_type == "employee",
PerformanceRecord.record_date >= start_date,
PerformanceRecord.record_date < end_date
)
).all()
# 按分类汇总个人业绩
category_performance = {}
total_personal_performance = Decimal("0")
for record in personal_records:
category_id = record.category_id
amount = record.amount or Decimal("0")
total_personal_performance += amount
if category_id not in category_performance:
category_performance[category_id] = {
"amount": Decimal("0"),
"category_name": record.category.name if record.category else "未知分类"
}
category_performance[category_id]["amount"] += amount
# 2. 获取二级代理业绩数据
agent_records = db.query(PerformanceRecord).filter(
and_(
PerformanceRecord.employee_id == employee_id,
PerformanceRecord.record_type == "agent",
PerformanceRecord.record_date >= start_date,
PerformanceRecord.record_date < end_date
)
).all()
total_agent_performance = Decimal("0")
for record in agent_records:
total_agent_performance += record.amount or Decimal("0")
# 计算总业绩
total_performance = total_personal_performance + total_agent_performance
# 3. 计算完成率
completion_rate = Decimal("0")
if target_amount > 0:
completion_rate = (total_performance / target_amount) * 100
# 4. 计算绩效奖金
performance_bonus = Decimal("0")
if completion_rate >= 50:
rate = min(completion_rate / 100, Decimal("1.0"))
performance_bonus = Decimal("1000") * rate
# 5. 计算个人提成
personal_commission = Decimal("0")
commission_details = []
for category_id, data in category_performance.items():
category = db.query(ProductCategory).filter(ProductCategory.id == category_id).first()
if category:
commission_rate = category.commission_rate or Decimal("0")
commission = data["amount"] * commission_rate
personal_commission += commission
commission_details.append({
"category_id": category_id,
"category_name": data["category_name"],
"amount": float(data["amount"]),
"commission_rate": float(commission_rate),
"commission": float(commission)
})
# 6. 计算代理提成二级代理业绩的1%
agent_commission_rate = Decimal("0.01")
agent_commission = total_agent_performance * agent_commission_rate
# 7. 计算总收入
base_salary = employee.base_salary or Decimal("0")
total_income = base_salary + performance_bonus + personal_commission + agent_commission
# 计算公司相关数据
company_rebate = Decimal("0")
for category_id, data in category_performance.items():
category = db.query(ProductCategory).filter(ProductCategory.id == category_id).first()
if category:
rebate_rate = get_rebate_rate_by_period(category, period)
company_rebate += data["amount"] * rebate_rate
# 公司成本
company_cost = total_income # 员工成本
# 代理分成成本
agent_share_cost = Decimal("0")
for record in agent_records:
if record.agent:
share_rate = record.agent.profit_share_rate or Decimal("0.60")
agent_share_cost += (record.amount or Decimal("0")) * share_rate
company_cost += agent_share_cost
# 公司利润
company_profit = company_rebate - company_cost
# 构建详情JSON
detail_json = {
"personal_performance": {
"total": float(total_personal_performance),
"by_category": commission_details
},
"agent_performance": {
"total": float(total_agent_performance),
"commission_rate": float(agent_commission_rate),
"commission": float(agent_commission)
},
"target": {
"amount": float(target_amount),
"completion_rate": float(completion_rate)
},
"bonus_calculation": {
"threshold": 50,
"max_bonus": 1000,
"actual_bonus": float(performance_bonus)
}
}
result = {
"employee_id": employee_id,
"employee_name": employee.user.name if employee.user else "",
"period": period,
"year": year,
"month": month,
"quarter": quarter,
"period_key": period_key,
"period_start_date": start_date.isoformat(),
"period_end_date": end_date.isoformat(),
"total_performance": float(total_performance),
"target_amount": float(target_amount),
"completion_rate": float(completion_rate),
"base_salary": float(base_salary),
"performance_bonus": float(performance_bonus),
"personal_commission": float(personal_commission),
"agent_commission": float(agent_commission),
"total_income": float(total_income),
"company_rebate": float(company_rebate),
"company_cost": float(company_cost),
"company_profit": float(company_profit),
"agent_performance": float(total_agent_performance),
"agent_share_amount": float(agent_share_cost),
"detail": detail_json
}
# 保存计算结果到数据库
if save_result:
calc_result = CalculationResult(
employee_id=employee_id,
calc_period=period,
calc_year=year,
calc_month=month,
calc_quarter=quarter,
period_start_date=start_date,
period_end_date=end_date,
total_performance=total_performance,
target_amount=target_amount,
completion_rate=completion_rate,
base_salary=base_salary,
performance_bonus=performance_bonus,
personal_commission=personal_commission,
agent_commission=agent_commission,
total_income=total_income,
company_rebate=company_rebate,
company_cost=company_cost,
company_profit=company_profit,
agent_performance=total_agent_performance,
agent_share_amount=agent_share_cost,
detail_json=json.dumps(detail_json, ensure_ascii=False)
)
db.add(calc_result)
db.commit()
db.refresh(calc_result)
result["calculation_id"] = calc_result.id
return result
def calculate_company_profit(
db: Session,
period: str,
year: int,
month: Optional[int] = None,
quarter: Optional[int] = None,
save_result: bool = False
) -> Dict[str, Any]:
"""
计算公司在指定周期的收益
计算逻辑:
1. 获取所有员工业绩
2. 公司返点 = 业绩按分类汇总 * 各分类返点比例
3. 公司成本 = 所有员工(底薪+绩效+提成) + 代理分成
4. 公司利润 = 返点 - 成本
Args:
db: 数据库会话
period: 周期类型
year: 年份
month: 月份
quarter: 季度
save_result: 是否保存结果
Returns:
计算结果字典
"""
# 获取周期日期范围
start_date, end_date, period_key = get_period_dates(period, year, month, quarter)
# 获取所有员工
employees = db.query(Employee).join(User).filter(User.status == 1).all()
total_company_rebate = Decimal("0")
total_company_cost = Decimal("0")
total_agent_share = Decimal("0")
employee_results = []
# 获取所有业绩记录
all_records = db.query(PerformanceRecord).filter(
and_(
PerformanceRecord.record_date >= start_date,
PerformanceRecord.record_date < end_date
)
).all()
# 按分类汇总业绩
category_totals = {}
for record in all_records:
if record.record_type == "employee":
cat_id = record.category_id
if cat_id not in category_totals:
category_totals[cat_id] = Decimal("0")
category_totals[cat_id] += record.amount or Decimal("0")
# 计算公司返点
rebate_details = []
for cat_id, amount in category_totals.items():
category = db.query(ProductCategory).filter(ProductCategory.id == cat_id).first()
if category:
rebate_rate = get_rebate_rate_by_period(category, period)
rebate = amount * rebate_rate
total_company_rebate += rebate
rebate_details.append({
"category_id": cat_id,
"category_name": category.name,
"amount": float(amount),
"rebate_rate": float(rebate_rate),
"rebate": float(rebate)
})
# 计算每个员工的收益和代理分成
for employee in employees:
try:
emp_result = calculate_employee_income(
db, employee.id, period, year, month, quarter, save_result=False
)
total_company_cost += Decimal(str(emp_result["total_income"]))
total_agent_share += Decimal(str(emp_result["agent_share_amount"]))
employee_results.append({
"employee_id": employee.id,
"employee_name": emp_result["employee_name"],
"total_income": emp_result["total_income"],
"agent_share": emp_result["agent_share_amount"]
})
except Exception as e:
# 跳过计算失败的员工
continue
# 总成本
total_cost = total_company_cost + total_agent_share
# 公司利润
company_profit = total_company_rebate - total_cost
result = {
"period": period,
"year": year,
"month": month,
"quarter": quarter,
"period_key": period_key,
"period_start_date": start_date.isoformat(),
"period_end_date": end_date.isoformat(),
"total_rebate": float(total_company_rebate),
"total_employee_cost": float(total_company_cost),
"total_agent_share": float(total_agent_share),
"total_cost": float(total_cost),
"company_profit": float(company_profit),
"employee_count": len(employee_results),
"rebate_details": rebate_details,
"employee_details": employee_results
}
return result
def calculate_agent_profit(
db: Session,
agent_id: int,
period: str,
year: int,
month: Optional[int] = None,
quarter: Optional[int] = None
) -> Dict[str, Any]:
"""
计算二级代理在指定周期的收益
计算逻辑:
代理分成 = 代理业绩 * 代理分佣比例(默认60%)
Args:
db: 数据库会话
agent_id: 代理ID
period: 周期类型
year: 年份
month: 月份
quarter: 季度
Returns:
计算结果字典
"""
# 获取代理信息
agent = db.query(SecondaryAgent).filter(SecondaryAgent.id == agent_id).first()
if not agent:
raise ValueError(f"代理不存在: {agent_id}")
# 获取周期日期范围
start_date, end_date, period_key = get_period_dates(period, year, month, quarter)
# 获取代理业绩
agent_records = db.query(PerformanceRecord).filter(
and_(
PerformanceRecord.agent_id == agent_id,
PerformanceRecord.record_date >= start_date,
PerformanceRecord.record_date < end_date
)
).all()
total_performance = Decimal("0")
performance_details = []
for record in agent_records:
amount = record.amount or Decimal("0")
total_performance += amount
performance_details.append({
"record_id": record.id,
"date": record.record_date.isoformat() if record.record_date else None,
"amount": float(amount),
"customer_name": record.customer_name,
"order_no": record.order_no
})
# 计算代理分成
profit_share_rate = agent.profit_share_rate or Decimal("0.60")
profit_share_amount = total_performance * profit_share_rate
result = {
"agent_id": agent_id,
"agent_name": agent.company_name,
"contact_name": agent.contact_name,
"employee_id": agent.employee_id,
"employee_name": agent.employee.user.name if agent.employee and agent.employee.user else "",
"period": period,
"year": year,
"month": month,
"quarter": quarter,
"period_key": period_key,
"period_start_date": start_date.isoformat(),
"period_end_date": end_date.isoformat(),
"total_performance": float(total_performance),
"profit_share_rate": float(profit_share_rate),
"profit_share_amount": float(profit_share_amount),
"performance_count": len(agent_records),
"performance_details": performance_details
}
return result
def get_calculation_history(
db: Session,
employee_id: Optional[int] = None,
period: Optional[str] = None,
year: Optional[int] = None,
page: int = 1,
page_size: int = 20
) -> Dict[str, Any]:
"""获取计算历史列表"""
query = db.query(CalculationResult)
if employee_id:
query = query.filter(CalculationResult.employee_id == employee_id)
if period:
query = query.filter(CalculationResult.calc_period == period)
if year:
query = query.filter(CalculationResult.calc_year == year)
total = query.count()
results = query.order_by(
CalculationResult.calc_year.desc(),
CalculationResult.created_at.desc()
).offset((page - 1) * page_size).limit(page_size).all()
items = []
for result in results:
items.append({
"id": result.id,
"employee_id": result.employee_id,
"employee_name": result.employee.user.name if result.employee and result.employee.user else "",
"calc_period": result.calc_period,
"calc_year": result.calc_year,
"calc_month": result.calc_month,
"calc_quarter": result.calc_quarter,
"period_start_date": result.period_start_date.isoformat() if result.period_start_date else None,
"period_end_date": result.period_end_date.isoformat() if result.period_end_date else None,
"total_performance": float(result.total_performance) if result.total_performance else 0,
"target_amount": float(result.target_amount) if result.target_amount else 0,
"completion_rate": float(result.completion_rate) if result.completion_rate else 0,
"total_income": float(result.total_income) if result.total_income else 0,
"company_profit": float(result.company_profit) if result.company_profit else 0,
"created_at": result.created_at.isoformat() if result.created_at else None
})
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
def get_calculation_detail(db: Session, calculation_id: int) -> Optional[Dict[str, Any]]:
"""获取计算历史详情"""
result = db.query(CalculationResult).filter(CalculationResult.id == calculation_id).first()
if not result:
return None
detail = None
if result.detail_json:
try:
detail = json.loads(result.detail_json)
except:
detail = None
return {
"id": result.id,
"employee_id": result.employee_id,
"employee_name": result.employee.user.name if result.employee and result.employee.user else "",
"calc_period": result.calc_period,
"calc_year": result.calc_year,
"calc_month": result.calc_month,
"calc_quarter": result.calc_quarter,
"period_start_date": result.period_start_date.isoformat() if result.period_start_date else None,
"period_end_date": result.period_end_date.isoformat() if result.period_end_date else None,
"total_performance": float(result.total_performance) if result.total_performance else 0,
"target_amount": float(result.target_amount) if result.target_amount else 0,
"completion_rate": float(result.completion_rate) if result.completion_rate else 0,
"base_salary": float(result.base_salary) if result.base_salary else 0,
"performance_bonus": float(result.performance_bonus) if result.performance_bonus else 0,
"personal_commission": float(result.personal_commission) if result.personal_commission else 0,
"agent_commission": float(result.agent_commission) if result.agent_commission else 0,
"total_income": float(result.total_income) if result.total_income else 0,
"company_rebate": float(result.company_rebate) if result.company_rebate else 0,
"company_cost": float(result.company_cost) if result.company_cost else 0,
"company_profit": float(result.company_profit) if result.company_profit else 0,
"agent_performance": float(result.agent_performance) if result.agent_performance else 0,
"agent_share_amount": float(result.agent_share_amount) if result.agent_share_amount else 0,
"detail": detail,
"created_at": result.created_at.isoformat() if result.created_at else None
}

View File

@@ -0,0 +1,369 @@
import pandas as pd
import io
from datetime import date, datetime
from typing import Optional, Dict, List, Any
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
from app.models import Employee, SecondaryAgent, ProductCategory, PerformanceRecord, User
from app.services.calculate_service import (
calculate_employee_income, calculate_company_profit,
get_period_dates, get_rebate_rate_by_period
)
def export_employee_report(
db: Session,
employee_id: int,
period: str,
year: int,
month: Optional[int] = None
) -> tuple:
"""
导出员工收益明细Excel
Returns:
tuple: (excel_bytes, filename)
"""
# 计算员工收益
result = calculate_employee_income(db, employee_id, period, year, month, save_result=False)
# 获取周期日期范围
start_date, end_date, period_key = get_period_dates(period, year, month)
# 获取业绩明细
personal_records = db.query(PerformanceRecord).filter(
and_(
PerformanceRecord.employee_id == employee_id,
PerformanceRecord.record_type == "employee",
PerformanceRecord.record_date >= start_date,
PerformanceRecord.record_date < end_date
)
).all()
agent_records = db.query(PerformanceRecord).filter(
and_(
PerformanceRecord.employee_id == employee_id,
PerformanceRecord.record_type == "agent",
PerformanceRecord.record_date >= start_date,
PerformanceRecord.record_date < end_date
)
).all()
# 创建Excel writer
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
# Sheet 1: 收益汇总
summary_data = {
'项目': [
'员工姓名',
'计算周期',
'业绩总额',
'目标金额',
'完成率(%)',
'底薪',
'绩效奖金',
'个人提成',
'代理提成',
'总收入',
'公司返点',
'公司成本',
'公司利润'
],
'金额': [
result['employee_name'],
result['period_key'],
result['total_performance'],
result['target_amount'],
f"{result['completion_rate']:.2f}",
result['base_salary'],
result['performance_bonus'],
result['personal_commission'],
result['agent_commission'],
result['total_income'],
result['company_rebate'],
result['company_cost'],
result['company_profit']
]
}
df_summary = pd.DataFrame(summary_data)
df_summary.to_excel(writer, sheet_name='收益汇总', index=False)
# Sheet 2: 个人业绩明细
if personal_records:
personal_data = []
for record in personal_records:
personal_data.append({
'日期': record.record_date,
'客户名称': record.customer_name or '',
'订单号': record.order_no or '',
'产品分类': record.category.name if record.category else '',
'业绩金额': float(record.amount or 0),
'提成比例': float(record.category.commission_rate or 0) if record.category else 0,
'提成金额': float(record.amount or 0) * float(record.category.commission_rate or 0) if record.category else 0
})
df_personal = pd.DataFrame(personal_data)
df_personal.to_excel(writer, sheet_name='个人业绩明细', index=False)
else:
pd.DataFrame({'提示': ['该周期内无个人业绩记录']}).to_excel(writer, sheet_name='个人业绩明细', index=False)
# Sheet 3: 代理业绩明细
if agent_records:
agent_data = []
for record in agent_records:
agent = record.agent
agent_data.append({
'日期': record.record_date,
'代理公司': agent.company_name if agent else '',
'客户名称': record.customer_name or '',
'订单号': record.order_no or '',
'产品分类': record.category.name if record.category else '',
'业绩金额': float(record.amount or 0),
'分佣比例': float(agent.profit_share_rate or 0.6) if agent else 0.6,
'分佣金额': float(record.amount or 0) * float(agent.profit_share_rate or 0.6) if agent else float(record.amount or 0) * 0.6
})
df_agent = pd.DataFrame(agent_data)
df_agent.to_excel(writer, sheet_name='代理业绩明细', index=False)
else:
pd.DataFrame({'提示': ['该周期内无代理业绩记录']}).to_excel(writer, sheet_name='代理业绩明细', index=False)
# Sheet 4: 提成明细
if result.get('detail') and result['detail'].get('personal_performance'):
commission_data = []
for item in result['detail']['personal_performance'].get('by_category', []):
commission_data.append({
'产品分类': item['category_name'],
'业绩金额': item['amount'],
'提成比例': item['commission_rate'],
'提成金额': item['commission']
})
if commission_data:
df_commission = pd.DataFrame(commission_data)
df_commission.to_excel(writer, sheet_name='提成明细', index=False)
output.seek(0)
filename = f"employee_report_{employee_id}_{period_key}.xlsx"
return output.getvalue(), filename
def export_company_report(
db: Session,
period: str,
year: int,
month: Optional[int] = None
) -> tuple:
"""
导出公司收益汇总Excel
Returns:
tuple: (excel_bytes, filename)
"""
# 计算公司收益
result = calculate_company_profit(db, period, year, month)
# 获取周期日期范围
start_date, end_date, period_key = get_period_dates(period, year, month)
# 创建Excel writer
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
# Sheet 1: 公司收益汇总
summary_data = {
'项目': [
'计算周期',
'员工数量',
'总返点收入',
'员工成本',
'代理分成',
'总成本',
'公司利润'
],
'金额': [
result['period_key'],
result['employee_count'],
result['total_rebate'],
result['total_employee_cost'],
result['total_agent_share'],
result['total_cost'],
result['company_profit']
]
}
df_summary = pd.DataFrame(summary_data)
df_summary.to_excel(writer, sheet_name='公司收益汇总', index=False)
# Sheet 2: 返点明细
if result.get('rebate_details'):
rebate_data = []
for item in result['rebate_details']:
rebate_data.append({
'产品分类': item['category_name'],
'业绩金额': item['amount'],
'返点比例': item['rebate_rate'],
'返点金额': item['rebate']
})
df_rebate = pd.DataFrame(rebate_data)
df_rebate.to_excel(writer, sheet_name='返点明细', index=False)
# Sheet 3: 员工成本明细
if result.get('employee_details'):
emp_data = []
for item in result['employee_details']:
emp_data.append({
'员工ID': item['employee_id'],
'员工姓名': item['employee_name'],
'员工收入': item['total_income'],
'代理分成': item['agent_share']
})
df_emp = pd.DataFrame(emp_data)
df_emp.to_excel(writer, sheet_name='员工成本明细', index=False)
output.seek(0)
filename = f"company_report_{period_key}.xlsx"
return output.getvalue(), filename
def export_performance_report(
db: Session,
filters: Dict[str, Any]
) -> tuple:
"""
导出业绩报表Excel
Args:
filters: 筛选条件
- start_date: 开始日期
- end_date: 结束日期
- employee_id: 员工ID可选
- agent_id: 代理ID可选
- category_id: 分类ID可选
- record_type: 记录类型employee/agent
Returns:
tuple: (excel_bytes, filename)
"""
# 构建查询
query = db.query(PerformanceRecord)
# 应用筛选条件
if filters.get('start_date'):
query = query.filter(PerformanceRecord.record_date >= filters['start_date'])
if filters.get('end_date'):
query = query.filter(PerformanceRecord.record_date <= filters['end_date'])
if filters.get('employee_id'):
query = query.filter(PerformanceRecord.employee_id == filters['employee_id'])
if filters.get('agent_id'):
query = query.filter(PerformanceRecord.agent_id == filters['agent_id'])
if filters.get('category_id'):
query = query.filter(PerformanceRecord.category_id == filters['category_id'])
if filters.get('record_type'):
query = query.filter(PerformanceRecord.record_type == filters['record_type'])
records = query.order_by(PerformanceRecord.record_date.desc()).all()
# 创建Excel writer
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
# Sheet 1: 业绩明细
if records:
data = []
total_amount = 0
for record in records:
amount = float(record.amount or 0)
total_amount += amount
employee_name = ""
if record.employee and record.employee.user:
employee_name = record.employee.user.name
agent_name = ""
if record.agent:
agent_name = record.agent.company_name
category_name = ""
if record.category:
category_name = record.category.name
data.append({
'ID': record.id,
'记录类型': '员工业绩' if record.record_type == 'employee' else '代理业绩',
'日期': record.record_date,
'员工': employee_name,
'代理': agent_name,
'产品分类': category_name,
'客户名称': record.customer_name or '',
'订单号': record.order_no or '',
'业绩金额': amount,
'备注': record.remark or ''
})
df = pd.DataFrame(data)
df.to_excel(writer, sheet_name='业绩明细', index=False)
# Sheet 2: 汇总统计
summary_data = {
'统计项': [
'记录总数',
'业绩总额',
'平均单笔业绩',
'员工业绩笔数',
'代理业绩笔数'
],
'数值': [
len(records),
total_amount,
round(total_amount / len(records), 2) if records else 0,
len([r for r in records if r.record_type == 'employee']),
len([r for r in records if r.record_type == 'agent'])
]
}
df_summary = pd.DataFrame(summary_data)
df_summary.to_excel(writer, sheet_name='汇总统计', index=False)
# Sheet 3: 按员工汇总
emp_summary = {}
for record in records:
emp_name = record.employee.user.name if record.employee and record.employee.user else '未知'
if emp_name not in emp_summary:
emp_summary[emp_name] = {'count': 0, 'amount': 0}
emp_summary[emp_name]['count'] += 1
emp_summary[emp_name]['amount'] += float(record.amount or 0)
emp_data = []
for name, stats in emp_summary.items():
emp_data.append({
'员工': name,
'业绩笔数': stats['count'],
'业绩总额': stats['amount']
})
df_emp = pd.DataFrame(emp_data)
df_emp.to_excel(writer, sheet_name='按员工汇总', index=False)
# Sheet 4: 按分类汇总
cat_summary = {}
for record in records:
cat_name = record.category.name if record.category else '未知'
if cat_name not in cat_summary:
cat_summary[cat_name] = {'count': 0, 'amount': 0}
cat_summary[cat_name]['count'] += 1
cat_summary[cat_name]['amount'] += float(record.amount or 0)
cat_data = []
for name, stats in cat_summary.items():
cat_data.append({
'产品分类': name,
'业绩笔数': stats['count'],
'业绩总额': stats['amount']
})
df_cat = pd.DataFrame(cat_data)
df_cat.to_excel(writer, sheet_name='按分类汇总', index=False)
else:
pd.DataFrame({'提示': ['无业绩记录']}).to_excel(writer, sheet_name='业绩明细', index=False)
output.seek(0)
# 生成文件名
date_str = datetime.now().strftime('%Y%m%d')
filename = f"performance_report_{date_str}.xlsx"
return output.getvalue(), filename