release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆 Made-with: Cursor
This commit is contained in:
284
api/app/routers/anniversaries.py
Normal file
284
api/app/routers/anniversaries.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.anniversary import Anniversary, AnniversaryCategory
|
||||
from app.schemas.anniversary import (
|
||||
AnniversaryCreate, AnniversaryUpdate, AnniversaryResponse,
|
||||
AnniversaryCategoryCreate, AnniversaryCategoryUpdate, AnniversaryCategoryResponse,
|
||||
)
|
||||
from app.schemas.common import DeleteResponse
|
||||
from app.utils.crud import get_or_404
|
||||
from app.utils.datetime import utcnow
|
||||
from app.utils.logger import logger
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["纪念日"])
|
||||
|
||||
|
||||
def compute_next_info(anniversary: Anniversary, today: date) -> tuple:
|
||||
"""计算纪念日的下一次日期、距今天数、周年数"""
|
||||
month, day = anniversary.date.month, anniversary.date.day
|
||||
|
||||
if anniversary.is_recurring:
|
||||
# 计算今年和明年的日期
|
||||
this_year = today.year
|
||||
next_date = date(this_year, month, day)
|
||||
if next_date < today:
|
||||
next_date = date(this_year + 1, month, day)
|
||||
|
||||
days_until = (next_date - today).days
|
||||
|
||||
year_count = None
|
||||
if anniversary.year:
|
||||
year_count = next_date.year - anniversary.year
|
||||
|
||||
return next_date, days_until, year_count
|
||||
else:
|
||||
# 非重复:使用原始日期(加上年份)
|
||||
if anniversary.year:
|
||||
target = date(anniversary.year, month, day)
|
||||
if target < today:
|
||||
return None, None, None
|
||||
days_until = (target - today).days
|
||||
return target, days_until, 0
|
||||
else:
|
||||
# 无年份的日期按今年算
|
||||
target = date(today.year, month, day)
|
||||
if target < today:
|
||||
return None, None, None
|
||||
days_until = (target - today).days
|
||||
return target, days_until, None
|
||||
|
||||
|
||||
def enrich_anniversary(anniversary: Anniversary, today: date) -> dict:
|
||||
"""将 SQLAlchemy 模型转换为响应字典,附加计算字段"""
|
||||
data = {
|
||||
"id": anniversary.id,
|
||||
"title": anniversary.title,
|
||||
"date": anniversary.date,
|
||||
"year": anniversary.year,
|
||||
"category_id": anniversary.category_id,
|
||||
"description": anniversary.description,
|
||||
"is_recurring": anniversary.is_recurring,
|
||||
"remind_days_before": anniversary.remind_days_before,
|
||||
"created_at": anniversary.created_at,
|
||||
"updated_at": anniversary.updated_at,
|
||||
}
|
||||
|
||||
next_date, days_until, year_count = compute_next_info(anniversary, today)
|
||||
data["next_date"] = next_date
|
||||
data["days_until"] = days_until
|
||||
data["year_count"] = year_count
|
||||
|
||||
if anniversary.category:
|
||||
data["category"] = {
|
||||
"id": anniversary.category.id,
|
||||
"name": anniversary.category.name,
|
||||
"icon": anniversary.category.icon,
|
||||
"color": anniversary.category.color,
|
||||
"sort_order": anniversary.category.sort_order,
|
||||
}
|
||||
else:
|
||||
data["category"] = None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ============ 纪念日分类 API ============
|
||||
|
||||
@router.get("/anniversary-categories", response_model=List[AnniversaryCategoryResponse])
|
||||
def get_categories(db: Session = Depends(get_db)):
|
||||
"""获取纪念日分类列表"""
|
||||
try:
|
||||
categories = db.query(AnniversaryCategory).order_by(
|
||||
AnniversaryCategory.sort_order.asc(),
|
||||
AnniversaryCategory.id.asc()
|
||||
).all()
|
||||
logger.info(f"获取纪念日分类列表成功,总数: {len(categories)}")
|
||||
return categories
|
||||
except Exception as e:
|
||||
logger.error(f"获取纪念日分类列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取纪念日分类列表失败")
|
||||
|
||||
|
||||
@router.post("/anniversary-categories", response_model=AnniversaryCategoryResponse, status_code=201)
|
||||
def create_category(data: AnniversaryCategoryCreate, db: Session = Depends(get_db)):
|
||||
"""创建纪念日分类"""
|
||||
try:
|
||||
db_category = AnniversaryCategory(
|
||||
name=data.name,
|
||||
icon=data.icon,
|
||||
color=data.color,
|
||||
sort_order=data.sort_order,
|
||||
)
|
||||
db.add(db_category)
|
||||
db.commit()
|
||||
db.refresh(db_category)
|
||||
logger.info(f"创建纪念日分类成功: id={db_category.id}, name={db_category.name}")
|
||||
return db_category
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"创建纪念日分类失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="创建纪念日分类失败")
|
||||
|
||||
|
||||
@router.put("/anniversary-categories/{category_id}", response_model=AnniversaryCategoryResponse)
|
||||
def update_category(category_id: int, data: AnniversaryCategoryUpdate, db: Session = Depends(get_db)):
|
||||
"""更新纪念日分类"""
|
||||
try:
|
||||
category = get_or_404(db, AnniversaryCategory, category_id, "纪念日分类")
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(category, field, value)
|
||||
db.commit()
|
||||
db.refresh(category)
|
||||
logger.info(f"更新纪念日分类成功: id={category_id}")
|
||||
return category
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"更新纪念日分类失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="更新纪念日分类失败")
|
||||
|
||||
|
||||
@router.delete("/anniversary-categories/{category_id}")
|
||||
def delete_category(category_id: int, db: Session = Depends(get_db)):
|
||||
"""删除纪念日分类"""
|
||||
try:
|
||||
category = get_or_404(db, AnniversaryCategory, category_id, "纪念日分类")
|
||||
# 删除分类时,将其下纪念日的 category_id 设为 NULL
|
||||
anniversaries = db.query(Anniversary).filter(
|
||||
Anniversary.category_id == category_id
|
||||
).all()
|
||||
for a in anniversaries:
|
||||
a.category_id = None
|
||||
db.delete(category)
|
||||
db.commit()
|
||||
logger.info(f"删除纪念日分类成功: id={category_id}")
|
||||
return DeleteResponse(message="纪念日分类删除成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"删除纪念日分类失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="删除纪念日分类失败")
|
||||
|
||||
|
||||
# ============ 纪念日 API ============
|
||||
|
||||
@router.get("/anniversaries", response_model=List[AnniversaryResponse])
|
||||
def get_anniversaries(
|
||||
category_id: Optional[int] = Query(None, description="分类ID筛选"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取纪念日列表(包含计算字段 next_date, days_until, year_count)"""
|
||||
try:
|
||||
query = db.query(Anniversary)
|
||||
if category_id is not None:
|
||||
query = query.filter(Anniversary.category_id == category_id)
|
||||
|
||||
anniversaries = query.order_by(
|
||||
Anniversary.date.asc(),
|
||||
Anniversary.title.asc()
|
||||
).all()
|
||||
|
||||
today = date.today()
|
||||
result = [enrich_anniversary(a, today) for a in anniversaries]
|
||||
|
||||
# 排序:即将到来的排前面,同天数的按日期排
|
||||
result.sort(key=lambda x: (
|
||||
0 if (x["days_until"] is not None and x["days_until"] >= 0) else 1,
|
||||
x["days_until"] if x["days_until"] is not None else 9999,
|
||||
))
|
||||
|
||||
logger.info(f"获取纪念日列表成功,总数: {len(result)}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"获取纪念日列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取纪念日列表失败")
|
||||
|
||||
|
||||
@router.post("/anniversaries", response_model=AnniversaryResponse, status_code=201)
|
||||
def create_anniversary(data: AnniversaryCreate, db: Session = Depends(get_db)):
|
||||
"""创建纪念日"""
|
||||
try:
|
||||
db_anniversary = Anniversary(
|
||||
title=data.title,
|
||||
date=data.date,
|
||||
year=data.year,
|
||||
category_id=data.category_id,
|
||||
description=data.description,
|
||||
is_recurring=data.is_recurring,
|
||||
remind_days_before=data.remind_days_before,
|
||||
)
|
||||
db.add(db_anniversary)
|
||||
db.commit()
|
||||
db.refresh(db_anniversary)
|
||||
|
||||
today = date.today()
|
||||
next_date, days_until, year_count = compute_next_info(db_anniversary, today)
|
||||
|
||||
logger.info(f"创建纪念日成功: id={db_anniversary.id}, title={db_anniversary.title}")
|
||||
return db_anniversary
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"创建纪念日失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="创建纪念日失败")
|
||||
|
||||
|
||||
@router.get("/anniversaries/{anniversary_id}", response_model=AnniversaryResponse)
|
||||
def get_anniversary(anniversary_id: int, db: Session = Depends(get_db)):
|
||||
"""获取单个纪念日"""
|
||||
try:
|
||||
anniversary = get_or_404(db, Anniversary, anniversary_id, "纪念日")
|
||||
return anniversary
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取纪念日失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="获取纪念日失败")
|
||||
|
||||
|
||||
@router.put("/anniversaries/{anniversary_id}", response_model=AnniversaryResponse)
|
||||
def update_anniversary(anniversary_id: int, data: AnniversaryUpdate, db: Session = Depends(get_db)):
|
||||
"""更新纪念日"""
|
||||
try:
|
||||
anniversary = get_or_404(db, Anniversary, anniversary_id, "纪念日")
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
for field, value in update_data.items():
|
||||
if value is not None or field in data.clearable_fields:
|
||||
setattr(anniversary, field, value)
|
||||
|
||||
anniversary.updated_at = utcnow()
|
||||
db.commit()
|
||||
db.refresh(anniversary)
|
||||
|
||||
logger.info(f"更新纪念日成功: id={anniversary_id}")
|
||||
return anniversary
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"更新纪念日失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="更新纪念日失败")
|
||||
|
||||
|
||||
@router.delete("/anniversaries/{anniversary_id}")
|
||||
def delete_anniversary(anniversary_id: int, db: Session = Depends(get_db)):
|
||||
"""删除纪念日"""
|
||||
try:
|
||||
anniversary = get_or_404(db, Anniversary, anniversary_id, "纪念日")
|
||||
db.delete(anniversary)
|
||||
db.commit()
|
||||
logger.info(f"删除纪念日成功: id={anniversary_id}")
|
||||
return DeleteResponse(message="纪念日删除成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"删除纪念日失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="删除纪念日失败")
|
||||
Reference in New Issue
Block a user