8.2 KiB
8.2 KiB
AI 服务积分扣减公共服务 - 实现计划
版本: v1.0 日期: 2026-02-22 基于设计文档: points-service-integration.md
一、实现概览
当前状态分析
| 组件 | 文件路径 | 当前状态 | 需要修改 |
|---|---|---|---|
| PointRecordDO | muye/pointrecord/dal/PointRecordDO.java |
完整 | 新增 status 字段 |
| MemberUserProfileMapper | muye/memberuserprofile/mapper/MemberUserProfileMapper.java |
仅查询 | 新增原子扣减方法 |
| AiModelConfigMapper | muye/aimodelconfig/mapper/AiModelConfigMapper.java |
仅分页 | 新增按平台查询方法 |
| PointsService | 不存在 | 需新建 | 新建接口+实现 |
| DifyService | 不存在 | 需新建 | 新建服务 |
二、实现步骤
步骤 1: 数据库层修改
1.1 PointRecordDO 新增 status 字段
文件: muye/pointrecord/dal/PointRecordDO.java
// 新增字段
private String status; // 状态:pending(预扣) / confirmed(已确认) / canceled(已取消)
数据库迁移:
ALTER TABLE muey_point_record ADD COLUMN status VARCHAR(20) DEFAULT 'confirmed' COMMENT '状态:pending-预扣 confirmed-已确认 canceled-已取消';
1.2 MemberUserProfileMapper 新增方法
文件: muye/memberuserprofile/mapper/MemberUserProfileMapper.java
/**
* 根据用户ID查询档案
*/
default MemberUserProfileDO selectByUserId(Long userId) {
return selectOne(new LambdaQueryWrapperX<MemberUserProfileDO>()
.eq(MemberUserProfileDO::getUserId, userId));
}
/**
* 原子扣减积分(乐观锁)
* @return 影响行数,0表示余额不足
*/
@Update("UPDATE muey_member_user_profile " +
"SET remaining_points = remaining_points - #{points}, " +
" used_points = used_points + #{points}, " +
" update_time = NOW() " +
"WHERE user_id = #{userId} AND remaining_points >= #{points}")
int updatePointsDeduct(@Param("userId") Long userId, @Param("points") Integer points);
1.3 AiModelConfigMapper 新增方法
文件: muye/aimodelconfig/mapper/AiModelConfigMapper.java
/**
* 根据平台和模型类型查询配置
*/
default AiModelConfigDO selectByPlatformAndModelType(String platform, String modelType) {
return selectOne(new LambdaQueryWrapperX<AiModelConfigDO>()
.eq(AiModelConfigDO::getPlatform, platform)
.eq(AiModelConfigDO::getModelType, modelType)
.eq(AiModelConfigDO::getStatus, 1));
}
1.4 PointRecordMapper 新增方法
文件: muye/pointrecord/mapper/PointRecordMapper.java
/**
* 取消过期的预扣记录(30分钟前)
*/
@Update("UPDATE muey_point_record SET status = 'canceled', update_time = NOW() " +
"WHERE status = 'pending' AND create_time < DATE_SUB(NOW(), INTERVAL 30 MINUTE)")
int cancelExpiredPendingRecords();
步骤 2: 公共积分服务
2.1 新建 PointsService 接口
文件: muye/points/service/PointsService.java(新建)
public interface PointsService {
/**
* 获取积分配置
*/
AiModelConfigDO getConfig(String platform, String modelType);
/**
* 预检积分(余额不足抛异常)
*/
void checkPoints(Long userId, Integer points);
/**
* 即时扣减(同步场景)
*/
Long deductPoints(Long userId, Integer points, String bizType, String bizId);
/**
* 创建预扣(流式/异步场景)
* @return 预扣记录ID
*/
Long createPendingDeduct(Long userId, Integer points, String bizType, String bizId);
/**
* 确认预扣(实际扣减)
*/
void confirmPendingDeduct(Long recordId);
/**
* 取消预扣(不扣费)
*/
void cancelPendingDeduct(Long recordId);
}
2.2 新建 PointsServiceImpl 实现
文件: muye/points/service/PointsServiceImpl.java(新建)
核心逻辑:
deductPoints: 调用 Mapper 原子扣减,失败抛POINTS_DEDUCT_FAILEDcreatePendingDeduct: 创建 status=pending 的记录confirmPendingDeduct: 执行实际扣减 + 更新 status=confirmedcancelPendingDeduct: 更新 status=canceled
2.3 新建错误码常量
文件: ErrorCodeConstants.java(修改)
// 积分相关错误码 1001001-1001003
ErrorCode POINTS_INSUFFICIENT = new ErrorCode(1001001, "积分不足");
ErrorCode POINTS_CONFIG_NOT_FOUND = new ErrorCode(1001002, "积分配置不存在");
ErrorCode POINTS_DEDUCT_FAILED = new ErrorCode(1001003, "积分扣减失败");
步骤 3: Dify 工作流集成
3.1 新建 Dify 配置类
文件: dify/config/DifyProperties.java(新建)
@ConfigurationProperties(prefix = "yudao.dify")
public class DifyProperties {
private String apiUrl;
private Integer timeout = 60;
}
3.2 新建 DifyClient
文件: dify/client/DifyClient.java(新建)
- 调用 Dify 工作流 API
- 支持流式响应
- 传入 sysPrompt 参数
3.3 新建 DifyService
文件: dify/service/DifyService.java(新建)
public interface DifyService {
/**
* 流式聊天(带积分扣减)
*/
Flux<DifyChatRespVO> chatStream(DifyChatReqVO reqVO, Long userId);
}
3.4 新建 DifyController
文件: dify/controller/AppDifyController.java(新建)
@PostMapping("/api/tik/dify/chat/stream")
public Flux<CommonResult<DifyChatRespVO>> chatStream(@RequestBody DifyChatReqVO reqVO);
步骤 4: 集成到现有服务
4.1 AiChatMessageService 集成
文件: service/chat/AiChatMessageServiceImpl.java(修改)
在 sendChatMessageStream 方法中:
- 调用前:
pointsService.checkPoints() - 创建预扣:
pointsService.createPendingDeduct() - 流结束:
pointsService.confirmPendingDeduct() - 出错/取消:
pointsService.cancelPendingDeduct()
步骤 5: 定时任务
5.1 预扣过期清理任务
文件: job/PointsPendingCleanJob.java(新建)
- 每 5 分钟执行
- 调用
pointRecordMapper.cancelExpiredPendingRecords()
三、文件清单
新建文件
| 文件 | 路径 |
|---|---|
| PointsService | muye/points/service/PointsService.java |
| PointsServiceImpl | muye/points/service/PointsServiceImpl.java |
| DifyProperties | dify/config/DifyProperties.java |
| DifyClient | dify/client/DifyClient.java |
| DifyService | dify/service/DifyService.java |
| DifyServiceImpl | dify/service/DifyServiceImpl.java |
| DifyReqVO | dify/vo/DifyChatReqVO.java |
| DifyRespVO | dify/vo/DifyChatRespVO.java |
| AppDifyController | dify/controller/AppDifyController.java |
| PointsPendingCleanJob | job/PointsPendingCleanJob.java |
修改文件
| 文件 | 修改内容 |
|---|---|
| PointRecordDO | 新增 status 字段 |
| MemberUserProfileMapper | 新增 selectByUserId、updatePointsDeduct 方法 |
| AiModelConfigMapper | 新增 selectByPlatformAndModelType 方法 |
| PointRecordMapper | 新增 cancelExpiredPendingRecords 方法 |
| ErrorCodeConstants | 新增积分相关错误码 |
| AiChatMessageServiceImpl | 集成积分扣减逻辑 |
四、数据库变更
-- 1. 积分记录表新增状态字段
ALTER TABLE muey_point_record
ADD COLUMN status VARCHAR(20) DEFAULT 'confirmed'
COMMENT '状态:pending-预扣 confirmed-已确认 canceled-已取消';
-- 2. 添加索引(可选优化)
CREATE INDEX idx_point_record_status_time
ON muey_point_record(status, create_time);
五、依赖关系
业务层
├── DifyService ──────┐
├── AiChatMessageService ──┼──→ PointsService(公共服务)
├── TikHubService ─────────┤ │
├── VoiceService ──────────┤ ├── AiModelConfigMapper(配置查询)
└── DigitalHumanService ───┘ ├── MemberUserProfileMapper(积分扣减)
└── PointRecordMapper(流水记录)
六、验收标准
- PointsService 单元测试通过
- Dify 流式接口正常返回
- 积分不足时抛出正确异常
- 流式中断时预扣正确取消
- 预扣过期定时任务正常运行
- 积分扣减原子性(并发不超扣)