You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
8.9 KiB
8.9 KiB
RFC 137: 积分过期机制
状态: 已完成
创建日期: 2026-01-27
作者: System
类型: 功能增强
概述
为 Credit Service 实现积分过期机制,自动处理赠送积分的过期逻辑,确保积分系统的健康运行。
背景
当前 credit_gifts 表已有 expires_at 字段,但缺少过期处理逻辑:
- 数据完整性: 赠送积分可以设置过期时间,但过期后不会自动扣除
- 用户体验: 用户无法查询即将过期的积分
- 系统健康: 过期积分未被清理,影响数据准确性
问题
- 赠送积分过期后仍然可用
- 用户不知道哪些积分即将过期
- 缺少定时任务自动处理过期积分
- 没有过期通知机制
解决方案
设计原则
- 自动化: 使用 Celery 定时任务自动处理过期积分
- 透明性: 提供 API 查询即将过期的积分
- 可追溯: 所有过期操作记录到交易流水
- 容错性: 处理失败不影响其他记录
实现细节
1. Repository 层 - 数据访问
新增查询方法:
# app/repositories/credit_repository.py
async def get_expiring_gifts(self, days: int = 7) -> List[CreditGift]:
"""获取即将过期的赠送积分"""
expiry_threshold = datetime.utcnow() + timedelta(days=days)
result = await self.session.execute(
select(CreditGift).where(
and_(
CreditGift.expires_at.isnot(None),
CreditGift.expires_at <= expiry_threshold,
CreditGift.expires_at > datetime.utcnow()
)
).order_by(CreditGift.expires_at)
)
return list(result.scalars().all())
async def get_expired_gifts(self) -> List[CreditGift]:
"""获取已过期的赠送积分"""
result = await self.session.execute(
select(CreditGift).where(
and_(
CreditGift.expires_at.isnot(None),
CreditGift.expires_at <= datetime.utcnow()
)
).order_by(CreditGift.expires_at)
)
return list(result.scalars().all())
async def get_user_expiring_gifts(
self,
user_id: UUID,
days: int = 7
) -> List[CreditGift]:
"""获取用户即将过期的赠送积分"""
# 实现逻辑...
2. Service 层 - 业务逻辑
新增服务方法:
# app/services/credit_service.py
async def get_expiring_credits(
self,
user_id: UUID,
days: int = 7
) -> Dict[str, Any]:
"""查询用户即将过期的积分"""
gifts = await self.repository.get_user_expiring_gifts(user_id, days)
total_expiring = sum(gift.credits for gift in gifts)
items = []
for gift in gifts:
items.append({
'gift_id': str(gift.gift_id),
'credits': gift.credits,
'gift_type': GiftType.to_name(gift.gift_type),
'description': gift.description,
'expires_at': gift.expires_at.isoformat(),
'days_until_expiry': (gift.expires_at - datetime.utcnow()).days
})
return {
'total_expiring': total_expiring,
'items': items,
'days': days
}
async def expire_gift_credits(self) -> int:
"""过期赠送积分(定时任务调用)"""
expired_gifts = await self.repository.get_expired_gifts()
count = 0
for gift in expired_gifts:
try:
user = await self._get_user(gift.user_id)
# 余额不足时记录日志但不扣除
if user.ai_credits_balance < gift.credits:
logger.warning(f"用户 {gift.user_id} 余额不足,无法扣除过期积分")
continue
async with self.session.begin_nested():
# 扣除积分
balance_before = user.ai_credits_balance
balance_after = balance_before - gift.credits
user.ai_credits_balance = balance_after
# 创建过期交易记录
transaction = CreditTransaction(
user_id=gift.user_id,
transaction_type=TransactionType.EXPIRE,
amount=-gift.credits,
balance_before=balance_before,
balance_after=balance_after,
description=f"赠送积分过期:{gift.description}"
)
await self.repository.create_transaction(transaction)
await self.session.commit()
count += 1
except Exception as e:
logger.error(f"过期积分处理失败: {e}")
await self.session.rollback()
return count
3. Celery 定时任务
创建定时任务:
# app/tasks/credit_tasks.py
@celery_app.task(name="app.tasks.credit_tasks.expire_gift_credits_task")
def expire_gift_credits_task(self) -> Dict[str, Any]:
"""过期赠送积分定时任务
每天凌晨 3 点执行
"""
async def process_expired_credits():
async with async_session_maker() as session:
service = CreditService(session)
count = await service.expire_gift_credits()
return count
count = asyncio.run(process_expired_credits())
return {
"task_id": self.request.id,
"status": "completed",
"expired_count": count,
"message": f"成功处理 {count} 条过期积分记录"
}
@celery_app.task(name="app.tasks.credit_tasks.notify_expiring_credits_task")
def notify_expiring_credits_task(self, days: int = 7) -> Dict[str, Any]:
"""通知即将过期的积分
每天上午 10 点执行
"""
# 实现通知逻辑...
4. Celery Beat 配置
在 celery_app.py 添加定时调度:
beat_schedule={
# 每天凌晨 3 点处理过期积分
"expire-gift-credits": {
"task": "app.tasks.credit_tasks.expire_gift_credits_task",
"schedule": crontab(hour=3, minute=0),
},
# 每天上午 10 点通知即将过期的积分(7 天内)
"notify-expiring-credits": {
"task": "app.tasks.credit_tasks.notify_expiring_credits_task",
"schedule": crontab(hour=10, minute=0),
"kwargs": {"days": 7},
},
}
5. API 端点
新增查询接口:
# app/api/v1/credits.py
@router.get("/expiring")
async def get_expiring_credits(
user_id: UUID = Query(..., description="用户 ID"),
days: int = Query(7, ge=1, le=30, description="未来多少天内过期"),
service: CreditService = Depends(get_credit_service)
):
"""查询即将过期的积分"""
result = await service.get_expiring_credits(user_id, days)
return success_response(
data=result,
message=f"查询到 {result['total_expiring']} 积分即将在 {days} 天内过期"
)
优势
- 自动化管理: 定时任务自动处理过期积分,无需人工干预
- 用户透明: 用户可以查询即将过期的积分,提前使用
- 数据准确: 过期积分及时清理,保证数据准确性
- 可追溯性: 所有过期操作记录到交易流水,便于审计
- 容错性强: 单条记录失败不影响其他记录处理
风险与缓解
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 用户余额不足 | 无法扣除过期积分 | 记录日志但不中断流程 |
| 定时任务失败 | 过期积分未清理 | 添加监控和告警 |
| 并发问题 | 数据不一致 | 使用数据库事务保证原子性 |
| 性能问题 | 大量过期记录处理慢 | 分批处理,添加索引 |
实施步骤
- ✅ 扩展
CreditRepository添加查询方法 - ✅ 扩展
CreditService添加业务方法 - ✅ 创建
credit_tasks.py添加 Celery 任务 - ✅ 更新
celery_app.py配置定时调度 - ✅ 添加 API 端点
/credits/expiring - ⏳ 测试定时任务执行
- ⏳ 添加监控和告警
测试策略
单元测试
async def test_get_expiring_credits():
"""测试查询即将过期的积分"""
# 创建测试数据
# 调用服务方法
# 验证返回结果
async def test_expire_gift_credits():
"""测试过期积分处理"""
# 创建过期的赠送记录
# 调用过期方法
# 验证积分已扣除
# 验证交易记录已创建
集成测试
def test_expire_gift_credits_task():
"""测试定时任务执行"""
result = expire_gift_credits_task.apply()
assert result.status == "SUCCESS"
监控指标
- 过期处理数量: 每天处理的过期记录数
- 处理失败数: 失败的记录数和原因
- 任务执行时间: 定时任务的执行时长
- 用户余额不足: 无法扣除的记录数
相关文档
变更日志
- 2026-01-27: 初始版本,实现积分过期机制