Files
ems-core/backend/app/api/v1/carbon.py

435 lines
14 KiB
Python
Raw Normal View History

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)