435 lines
14 KiB
Python
435 lines
14 KiB
Python
|
|
from datetime import date, datetime, timedelta, timezone
|
||
|
|
from typing import Optional
|
||
|
|
from fastapi import APIRouter, Depends, Query, HTTPException, Body
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from sqlalchemy import select, func, and_, text
|
||
|
|
from app.core.database import get_db
|
||
|
|
from app.core.config import get_settings
|
||
|
|
from app.core.deps import get_current_user
|
||
|
|
from app.models.carbon import (
|
||
|
|
CarbonEmission, EmissionFactor, CarbonTarget, CarbonReduction,
|
||
|
|
GreenCertificate, CarbonReport, CarbonBenchmark,
|
||
|
|
)
|
||
|
|
from app.models.user import User
|
||
|
|
from app.services import carbon_asset
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/carbon", tags=["碳排放管理"])
|
||
|
|
|
||
|
|
|
||
|
|
# --------------- Pydantic Schemas ---------------
|
||
|
|
|
||
|
|
class TargetCreate(BaseModel):
|
||
|
|
year: int
|
||
|
|
month: Optional[int] = None
|
||
|
|
target_emission_tons: float
|
||
|
|
|
||
|
|
class TargetUpdate(BaseModel):
|
||
|
|
target_emission_tons: Optional[float] = None
|
||
|
|
status: Optional[str] = None
|
||
|
|
|
||
|
|
class CertificateCreate(BaseModel):
|
||
|
|
certificate_type: str
|
||
|
|
certificate_number: str
|
||
|
|
issue_date: date
|
||
|
|
expiry_date: Optional[date] = None
|
||
|
|
energy_mwh: float
|
||
|
|
price_yuan: float = 0
|
||
|
|
status: str = "active"
|
||
|
|
source_device_id: Optional[int] = None
|
||
|
|
notes: Optional[str] = None
|
||
|
|
|
||
|
|
class CertificateUpdate(BaseModel):
|
||
|
|
status: Optional[str] = None
|
||
|
|
price_yuan: Optional[float] = None
|
||
|
|
notes: Optional[str] = None
|
||
|
|
|
||
|
|
class ReportGenerate(BaseModel):
|
||
|
|
report_type: str = Field(..., pattern="^(monthly|quarterly|annual)$")
|
||
|
|
period_start: date
|
||
|
|
period_end: date
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/overview")
|
||
|
|
async def carbon_overview(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||
|
|
"""碳排放总览"""
|
||
|
|
now = datetime.now(timezone.utc)
|
||
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
|
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||
|
|
year_start = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
||
|
|
|
||
|
|
async def sum_carbon(start, end):
|
||
|
|
r = await db.execute(
|
||
|
|
select(func.sum(CarbonEmission.emission), func.sum(CarbonEmission.reduction))
|
||
|
|
.where(and_(CarbonEmission.date >= start, CarbonEmission.date < end))
|
||
|
|
)
|
||
|
|
row = r.first()
|
||
|
|
return {"emission": row[0] or 0, "reduction": row[1] or 0}
|
||
|
|
|
||
|
|
today = await sum_carbon(today_start, now)
|
||
|
|
month = await sum_carbon(month_start, now)
|
||
|
|
year = await sum_carbon(year_start, now)
|
||
|
|
|
||
|
|
# 各scope分布
|
||
|
|
scope_q = await db.execute(
|
||
|
|
select(CarbonEmission.scope, func.sum(CarbonEmission.emission))
|
||
|
|
.where(CarbonEmission.date >= year_start)
|
||
|
|
.group_by(CarbonEmission.scope)
|
||
|
|
)
|
||
|
|
by_scope = {row[0]: round(row[1], 2) for row in scope_q.all()}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"today": {"emission": round(today["emission"], 2), "reduction": round(today["reduction"], 2)},
|
||
|
|
"month": {"emission": round(month["emission"], 2), "reduction": round(month["reduction"], 2)},
|
||
|
|
"year": {"emission": round(year["emission"], 2), "reduction": round(year["reduction"], 2)},
|
||
|
|
"by_scope": by_scope,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/trend")
|
||
|
|
async def carbon_trend(
|
||
|
|
days: int = Query(30, ge=1, le=365),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""碳排放趋势"""
|
||
|
|
start = datetime.now(timezone.utc) - timedelta(days=days)
|
||
|
|
settings = get_settings()
|
||
|
|
if settings.is_sqlite:
|
||
|
|
day_expr = func.strftime('%Y-%m-%d', CarbonEmission.date).label('day')
|
||
|
|
else:
|
||
|
|
day_expr = func.date_trunc('day', CarbonEmission.date).label('day')
|
||
|
|
|
||
|
|
result = await db.execute(
|
||
|
|
select(
|
||
|
|
day_expr,
|
||
|
|
func.sum(CarbonEmission.emission),
|
||
|
|
func.sum(CarbonEmission.reduction),
|
||
|
|
).where(CarbonEmission.date >= start)
|
||
|
|
.group_by(text('day')).order_by(text('day'))
|
||
|
|
)
|
||
|
|
return [{"date": str(r[0]), "emission": round(r[1], 2), "reduction": round(r[2], 2)} for r in result.all()]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/factors")
|
||
|
|
async def list_factors(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||
|
|
result = await db.execute(select(EmissionFactor).order_by(EmissionFactor.id))
|
||
|
|
return [{
|
||
|
|
"id": f.id, "name": f.name, "energy_type": f.energy_type, "factor": f.factor,
|
||
|
|
"unit": f.unit, "scope": f.scope, "region": f.region, "source": f.source,
|
||
|
|
} for f in result.scalars().all()]
|
||
|
|
|
||
|
|
|
||
|
|
# =============== Carbon Dashboard ===============
|
||
|
|
|
||
|
|
@router.get("/dashboard")
|
||
|
|
async def carbon_dashboard(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||
|
|
"""综合碳资产仪表盘"""
|
||
|
|
return await carbon_asset.get_carbon_dashboard(db)
|
||
|
|
|
||
|
|
|
||
|
|
# =============== Carbon Targets ===============
|
||
|
|
|
||
|
|
@router.get("/targets")
|
||
|
|
async def list_targets(
|
||
|
|
year: int = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""碳减排目标列表"""
|
||
|
|
q = select(CarbonTarget).order_by(CarbonTarget.year.desc(), CarbonTarget.month)
|
||
|
|
if year:
|
||
|
|
q = q.where(CarbonTarget.year == year)
|
||
|
|
result = await db.execute(q)
|
||
|
|
targets = result.scalars().all()
|
||
|
|
return [{
|
||
|
|
"id": t.id, "year": t.year, "month": t.month,
|
||
|
|
"target_emission_tons": t.target_emission_tons,
|
||
|
|
"actual_emission_tons": t.actual_emission_tons,
|
||
|
|
"status": t.status,
|
||
|
|
} for t in targets]
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/targets")
|
||
|
|
async def create_target(
|
||
|
|
data: TargetCreate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""创建碳减排目标"""
|
||
|
|
target = CarbonTarget(
|
||
|
|
year=data.year,
|
||
|
|
month=data.month,
|
||
|
|
target_emission_tons=data.target_emission_tons,
|
||
|
|
)
|
||
|
|
db.add(target)
|
||
|
|
await db.flush()
|
||
|
|
return {"id": target.id, "message": "目标创建成功"}
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/targets/{target_id}")
|
||
|
|
async def update_target(
|
||
|
|
target_id: int,
|
||
|
|
data: TargetUpdate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""更新碳减排目标"""
|
||
|
|
result = await db.execute(select(CarbonTarget).where(CarbonTarget.id == target_id))
|
||
|
|
target = result.scalar_one_or_none()
|
||
|
|
if not target:
|
||
|
|
raise HTTPException(404, "目标不存在")
|
||
|
|
if data.target_emission_tons is not None:
|
||
|
|
target.target_emission_tons = data.target_emission_tons
|
||
|
|
if data.status is not None:
|
||
|
|
target.status = data.status
|
||
|
|
return {"message": "更新成功"}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/targets/progress")
|
||
|
|
async def target_progress(
|
||
|
|
year: int = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""碳目标进度"""
|
||
|
|
if year is None:
|
||
|
|
year = datetime.now(timezone.utc).year
|
||
|
|
return await carbon_asset.get_target_progress(db, year)
|
||
|
|
|
||
|
|
|
||
|
|
# =============== Carbon Reductions ===============
|
||
|
|
|
||
|
|
@router.get("/reductions")
|
||
|
|
async def list_reductions(
|
||
|
|
start: date = Query(None),
|
||
|
|
end: date = Query(None),
|
||
|
|
source_type: str = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""碳减排活动列表"""
|
||
|
|
q = select(CarbonReduction).order_by(CarbonReduction.date.desc())
|
||
|
|
if start:
|
||
|
|
q = q.where(CarbonReduction.date >= start)
|
||
|
|
if end:
|
||
|
|
q = q.where(CarbonReduction.date <= end)
|
||
|
|
if source_type:
|
||
|
|
q = q.where(CarbonReduction.source_type == source_type)
|
||
|
|
result = await db.execute(q.limit(500))
|
||
|
|
items = result.scalars().all()
|
||
|
|
return [{
|
||
|
|
"id": r.id, "source_type": r.source_type, "date": str(r.date),
|
||
|
|
"reduction_tons": r.reduction_tons, "equivalent_trees": r.equivalent_trees,
|
||
|
|
"methodology": r.methodology, "verified": r.verified,
|
||
|
|
} for r in items]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/reductions/summary")
|
||
|
|
async def reduction_summary(
|
||
|
|
start: date = Query(None),
|
||
|
|
end: date = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""减排汇总(按来源类型)"""
|
||
|
|
if not start:
|
||
|
|
start = date(datetime.now(timezone.utc).year, 1, 1)
|
||
|
|
if not end:
|
||
|
|
end = datetime.now(timezone.utc).date()
|
||
|
|
return await carbon_asset.get_reduction_summary(db, start, end)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/reductions/calculate")
|
||
|
|
async def calculate_reductions(
|
||
|
|
start: date = Query(None),
|
||
|
|
end: date = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""触发减排量计算"""
|
||
|
|
if not start:
|
||
|
|
start = date(datetime.now(timezone.utc).year, 1, 1)
|
||
|
|
if not end:
|
||
|
|
end = datetime.now(timezone.utc).date()
|
||
|
|
return await carbon_asset.trigger_reduction_calculation(db, start, end)
|
||
|
|
|
||
|
|
|
||
|
|
# =============== Green Certificates ===============
|
||
|
|
|
||
|
|
@router.get("/certificates")
|
||
|
|
async def list_certificates(
|
||
|
|
status: str = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""绿证列表"""
|
||
|
|
q = select(GreenCertificate).order_by(GreenCertificate.issue_date.desc())
|
||
|
|
if status:
|
||
|
|
q = q.where(GreenCertificate.status == status)
|
||
|
|
result = await db.execute(q)
|
||
|
|
certs = result.scalars().all()
|
||
|
|
return [{
|
||
|
|
"id": c.id, "certificate_type": c.certificate_type,
|
||
|
|
"certificate_number": c.certificate_number,
|
||
|
|
"issue_date": str(c.issue_date), "expiry_date": str(c.expiry_date) if c.expiry_date else None,
|
||
|
|
"energy_mwh": c.energy_mwh, "price_yuan": c.price_yuan,
|
||
|
|
"status": c.status, "notes": c.notes,
|
||
|
|
} for c in certs]
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/certificates")
|
||
|
|
async def create_certificate(
|
||
|
|
data: CertificateCreate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""登记绿证"""
|
||
|
|
cert = GreenCertificate(**data.model_dump())
|
||
|
|
db.add(cert)
|
||
|
|
await db.flush()
|
||
|
|
return {"id": cert.id, "message": "绿证登记成功"}
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/certificates/{cert_id}")
|
||
|
|
async def update_certificate(
|
||
|
|
cert_id: int,
|
||
|
|
data: CertificateUpdate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""更新绿证"""
|
||
|
|
result = await db.execute(select(GreenCertificate).where(GreenCertificate.id == cert_id))
|
||
|
|
cert = result.scalar_one_or_none()
|
||
|
|
if not cert:
|
||
|
|
raise HTTPException(404, "绿证不存在")
|
||
|
|
for k, v in data.model_dump(exclude_unset=True).items():
|
||
|
|
setattr(cert, k, v)
|
||
|
|
return {"message": "更新成功"}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/certificates/value")
|
||
|
|
async def certificate_portfolio_value(
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""绿证组合价值"""
|
||
|
|
return await carbon_asset.get_certificate_portfolio_value(db)
|
||
|
|
|
||
|
|
|
||
|
|
# =============== Carbon Reports ===============
|
||
|
|
|
||
|
|
@router.get("/reports")
|
||
|
|
async def list_reports(
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""报告列表"""
|
||
|
|
result = await db.execute(
|
||
|
|
select(CarbonReport).order_by(CarbonReport.generated_at.desc()).limit(50)
|
||
|
|
)
|
||
|
|
reports = result.scalars().all()
|
||
|
|
return [{
|
||
|
|
"id": r.id, "report_type": r.report_type,
|
||
|
|
"period_start": str(r.period_start), "period_end": str(r.period_end),
|
||
|
|
"generated_at": str(r.generated_at),
|
||
|
|
"total_tons": r.total_tons, "reduction_tons": r.reduction_tons,
|
||
|
|
"net_tons": r.net_tons,
|
||
|
|
} for r in reports]
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/reports/generate")
|
||
|
|
async def generate_report(
|
||
|
|
data: ReportGenerate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""生成碳报告"""
|
||
|
|
report = await carbon_asset.generate_carbon_report(
|
||
|
|
db, data.report_type, data.period_start, data.period_end,
|
||
|
|
)
|
||
|
|
await db.flush()
|
||
|
|
return {
|
||
|
|
"id": report.id,
|
||
|
|
"total_tons": report.total_tons,
|
||
|
|
"reduction_tons": report.reduction_tons,
|
||
|
|
"net_tons": report.net_tons,
|
||
|
|
"message": "报告生成成功",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/reports/{report_id}")
|
||
|
|
async def get_report(
|
||
|
|
report_id: int,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""报告详情"""
|
||
|
|
result = await db.execute(select(CarbonReport).where(CarbonReport.id == report_id))
|
||
|
|
r = result.scalar_one_or_none()
|
||
|
|
if not r:
|
||
|
|
raise HTTPException(404, "报告不存在")
|
||
|
|
return {
|
||
|
|
"id": r.id, "report_type": r.report_type,
|
||
|
|
"period_start": str(r.period_start), "period_end": str(r.period_end),
|
||
|
|
"generated_at": str(r.generated_at),
|
||
|
|
"scope1_tons": r.scope1_tons, "scope2_tons": r.scope2_tons,
|
||
|
|
"scope3_tons": r.scope3_tons,
|
||
|
|
"total_tons": r.total_tons, "reduction_tons": r.reduction_tons,
|
||
|
|
"net_tons": r.net_tons, "report_data": r.report_data,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/reports/{report_id}/download")
|
||
|
|
async def download_report(
|
||
|
|
report_id: int,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""下载报告数据"""
|
||
|
|
result = await db.execute(select(CarbonReport).where(CarbonReport.id == report_id))
|
||
|
|
r = result.scalar_one_or_none()
|
||
|
|
if not r:
|
||
|
|
raise HTTPException(404, "报告不存在")
|
||
|
|
return {
|
||
|
|
"report_type": r.report_type,
|
||
|
|
"period": f"{r.period_start} ~ {r.period_end}",
|
||
|
|
"scope1_tons": r.scope1_tons, "scope2_tons": r.scope2_tons,
|
||
|
|
"total_tons": r.total_tons, "reduction_tons": r.reduction_tons,
|
||
|
|
"net_tons": r.net_tons,
|
||
|
|
"detail": r.report_data,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# =============== Carbon Benchmarks ===============
|
||
|
|
|
||
|
|
@router.get("/benchmarks")
|
||
|
|
async def list_benchmarks(
|
||
|
|
year: int = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""行业基准列表"""
|
||
|
|
q = select(CarbonBenchmark).order_by(CarbonBenchmark.year.desc())
|
||
|
|
if year:
|
||
|
|
q = q.where(CarbonBenchmark.year == year)
|
||
|
|
result = await db.execute(q)
|
||
|
|
items = result.scalars().all()
|
||
|
|
return [{
|
||
|
|
"id": b.id, "industry": b.industry, "metric_name": b.metric_name,
|
||
|
|
"benchmark_value": b.benchmark_value, "unit": b.unit,
|
||
|
|
"year": b.year, "source": b.source, "notes": b.notes,
|
||
|
|
} for b in items]
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/benchmarks/comparison")
|
||
|
|
async def benchmark_comparison(
|
||
|
|
year: int = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""行业对标比较"""
|
||
|
|
if year is None:
|
||
|
|
year = datetime.now(timezone.utc).year
|
||
|
|
return await carbon_asset.compare_with_benchmarks(db, year)
|