# RFC 137: 积分过期机制 **状态**: 已完成 **创建日期**: 2026-01-27 **作者**: System **类型**: 功能增强 ## 概述 为 Credit Service 实现积分过期机制,自动处理赠送积分的过期逻辑,确保积分系统的健康运行。 ## 背景 当前 `credit_gifts` 表已有 `expires_at` 字段,但缺少过期处理逻辑: 1. **数据完整性**: 赠送积分可以设置过期时间,但过期后不会自动扣除 2. **用户体验**: 用户无法查询即将过期的积分 3. **系统健康**: 过期积分未被清理,影响数据准确性 ## 问题 1. 赠送积分过期后仍然可用 2. 用户不知道哪些积分即将过期 3. 缺少定时任务自动处理过期积分 4. 没有过期通知机制 ## 解决方案 ### 设计原则 1. **自动化**: 使用 Celery 定时任务自动处理过期积分 2. **透明性**: 提供 API 查询即将过期的积分 3. **可追溯**: 所有过期操作记录到交易流水 4. **容错性**: 处理失败不影响其他记录 ### 实现细节 #### 1. Repository 层 - 数据访问 新增查询方法: ```python # 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 层 - 业务逻辑 新增服务方法: ```python # 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 定时任务 创建定时任务: ```python # 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` 添加定时调度: ```python 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 端点 新增查询接口: ```python # 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} 天内过期" ) ``` ## 优势 1. **自动化管理**: 定时任务自动处理过期积分,无需人工干预 2. **用户透明**: 用户可以查询即将过期的积分,提前使用 3. **数据准确**: 过期积分及时清理,保证数据准确性 4. **可追溯性**: 所有过期操作记录到交易流水,便于审计 5. **容错性强**: 单条记录失败不影响其他记录处理 ## 风险与缓解 | 风险 | 影响 | 缓解措施 | |------|------|----------| | 用户余额不足 | 无法扣除过期积分 | 记录日志但不中断流程 | | 定时任务失败 | 过期积分未清理 | 添加监控和告警 | | 并发问题 | 数据不一致 | 使用数据库事务保证原子性 | | 性能问题 | 大量过期记录处理慢 | 分批处理,添加索引 | ## 实施步骤 1. ✅ 扩展 `CreditRepository` 添加查询方法 2. ✅ 扩展 `CreditService` 添加业务方法 3. ✅ 创建 `credit_tasks.py` 添加 Celery 任务 4. ✅ 更新 `celery_app.py` 配置定时调度 5. ✅ 添加 API 端点 `/credits/expiring` 6. ⏳ 测试定时任务执行 7. ⏳ 添加监控和告警 ## 测试策略 ### 单元测试 ```python async def test_get_expiring_credits(): """测试查询即将过期的积分""" # 创建测试数据 # 调用服务方法 # 验证返回结果 async def test_expire_gift_credits(): """测试过期积分处理""" # 创建过期的赠送记录 # 调用过期方法 # 验证积分已扣除 # 验证交易记录已创建 ``` ### 集成测试 ```python def test_expire_gift_credits_task(): """测试定时任务执行""" result = expire_gift_credits_task.apply() assert result.status == "SUCCESS" ``` ## 监控指标 1. **过期处理数量**: 每天处理的过期记录数 2. **处理失败数**: 失败的记录数和原因 3. **任务执行时间**: 定时任务的执行时长 4. **用户余额不足**: 无法扣除的记录数 ## 相关文档 - [Credit Service 需求文档](../../requirements/backend/04-services/user/credit-service.md) - [Celery 集成文档](./136-celery-integration.md) - [数据库设计规范](../../architecture/tech-stack.md) ## 变更日志 - 2026-01-27: 初始版本,实现积分过期机制