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