Files
sionrui/openspec/plans/points-oss-quota-system.md

456 lines
14 KiB
Markdown
Raw Normal View History

2026-02-25 21:30:24 +08:00
# 积分系统与OSS额度系统实施计划
> 创建日期2026-02-25
> 状态:待审批
---
## 一、需求概述
### 1.1 业务需求
| 模块 | 需求描述 |
|------|----------|
| **前端积分系统** | 前端启动时加载 `muye_ai_model_config`,根据 model_code 获取积分消耗,显示在对标分析、热点趋势、智能体、数字人模块 |
| **OSS额度系统** | 后端统一用字节计算,仅统计已上传未删除文件,上传前校验,删除后释放,每日凌晨自动对账 |
| **OSS存储管理** | 素材列表和个人中心显示用户正确的额度 |
### 1.2 验收标准
1. 前端各业务模块正确显示积分消耗数值
2. OSS 上传前校验额度,超额拒绝上传
3. OSS 文件删除后实时释放额度
4. 每日凌晨自动对账,确保数据库记录与实际一致
---
## 二、现有架构分析
### 2.1 已有基础设施
| 组件 | 位置 | 状态 |
|------|------|------|
| **积分服务** | `PointsService` | ✅ 已实现预检、扣减、预扣逻辑 |
| **AI模型配置** | `muye_ai_model_config` 表 | ✅ 已有 `consume_points` 字段 |
| **用户档案服务** | `MemberUserProfileService` | ⚠️ 需增强存储校验和更新 |
| **用户档案表** | `muye_member_user_profile` | ✅ 已有存储和积分字段GB单位 |
| **用户Store** | `stores/user.js` | ✅ 已有积分和存储计算属性 |
| **个人中心** | `views/user/Profile.vue` | ✅ 已显示基本额度信息 |
### 2.2 需要新增的组件
| 组件 | 类型 | 说明 |
|------|------|------|
| `ModelConfigApi` | 前端API | 获取模型配置列表 |
| `PointsConfigStore` | 前端Store | 管理积分配置状态 |
| `PointsTag` | 前端组件 | 积分消耗标签组件 |
| `OssQuotaReconcileJob` | 后端Job | 每日对账任务 |
| `StorageQuotaMixin` | 前端Composable | 存储额度显示逻辑复用 |
---
## 三、详细设计
### 3.1 前端积分系统
#### 3.1.1 数据流
```
前端启动 → 调用 API 获取模型配置 → 存入 Store → 各模块从 Store 读取显示
```
#### 3.1.2 新增文件
```
frontend/app/web-gold/src/
├── api/
│ └── modelConfig.js # 模型配置 API
├── stores/
│ └── pointsConfig.js # 积分配置 Store
└── components/
└── common/
└── PointsTag.vue # 积分消耗标签组件
```
#### 3.1.3 API 设计
```javascript
// api/modelConfig.js
export function getModelConfigList() {
return request({
url: `${BASE_URL}/aimodelconfig/list-enabled`,
method: 'get'
})
}
// 返回格式
{
"dify": {
"agent_chat_pro": { "consumePoints": 10, "modelName": "深度版" },
"agent_chat_standard": { "consumePoints": 5, "modelName": "标准版" }
},
"digital_human": {
"latentsync": { "consumePoints": 50, "modelName": "数字人" }
},
// ...
}
```
#### 3.1.4 Store 设计
```javascript
// stores/pointsConfig.js
export const usePointsConfigStore = defineStore('pointsConfig', () => {
const configMap = ref({}) // 按平台+modelCode组织的配置
const isLoaded = ref(false)
// 根据 platform 和 modelCode 获取积分消耗
const getConsumePoints = (platform, modelCode) => {
return configMap.value[platform]?.[modelCode]?.consumePoints ?? 0
}
// 初始化加载配置
const loadConfig = async () => {
if (isLoaded.value) return
const data = await getModelConfigList()
configMap.value = data
isLoaded.value = true
}
return { configMap, isLoaded, getConsumePoints, loadConfig }
})
```
#### 3.1.5 积分标签组件
```vue
<!-- components/common/PointsTag.vue -->
<template>
<span class="points-tag" :class="{ 'is-loading': loading }">
<ThunderboltOutlined />
<span class="points-value">{{ displayPoints }}</span>
<span class="points-unit">积分</span>
</span>
</template>
<script setup>
import { computed } from 'vue'
import { usePointsConfigStore } from '@/stores/pointsConfig'
const props = defineProps({
platform: { type: String, required: true },
modelCode: { type: String, required: true },
loading: { type: Boolean, default: false }
})
const pointsConfigStore = usePointsConfigStore()
const displayPoints = computed(() =>
pointsConfigStore.getConsumePoints(props.platform, props.modelCode)
)
</script>
```
#### 3.1.6 各模块集成点
| 模块 | 文件位置 | 集成方式 |
|------|----------|----------|
| **智能体** | `ChatDrawer.vue` | 在发送按钮旁显示积分消耗 |
| **数字人** | `Video.vue` | 在生成按钮旁显示积分消耗 |
| **对标分析** | `Benchmark.vue` | 在分析按钮旁显示积分消耗 |
| **热点趋势** | `Forecast.vue` | 在文案生成按钮旁显示积分消耗 |
---
### 3.2 OSS 额度系统
#### 3.2.1 数据模型
**MemberUserProfileDO 字段muye_member_user_profile 表):**
```java
private String userId; // 用户ID
private BigDecimal totalStorage; // 云空间总容量 (GB)
private BigDecimal usedStorage; // 云空间已用容量 (GB)
private BigDecimal remainingStorage;// 云空间剩余容量 (GB)
private Integer totalPoints; // 账户总积分
private Integer usedPoints; // 账户消耗积分
private Integer remainingPoints; // 账户剩余积分
```
> **设计决策**:保持 GB 单位存储(兼容现有数据),后端逻辑统一用字节计算
**存储文件记录表muye_material_file**
```sql
-- 文件大小字段(字节)
file_size BIGINT NOT NULL COMMENT '文件大小(字节)'
```
**单位转换常量:**
```java
// 1 GB = 1024 * 1024 * 1024 字节
public static final long BYTES_PER_GB = 1073741824L;
```
#### 3.2.2 后端增强设计
**修改/新增文件:**
```
yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/
├── muye/
│ └── memberuserprofile/
│ ├── service/
│ │ ├── MemberUserProfileService.java # 增强:添加存储额度方法
│ │ └── MemberUserProfileServiceImpl.java
│ ├── mapper/
│ │ └── MemberUserProfileMapper.java # 增强:添加原子更新方法
│ └── job/
│ └── OssQuotaReconcileJob.java # 新增每日对账Job
```
#### 3.2.3 额度校验流程
```
用户上传文件
检查文件大小(字节)
调用 MemberUserProfileService.validateStorage(userId, fileSizeBytes)
内部逻辑:将 fileSizeBytes 转换为 GB与 remainingStorage 比较
├── 余额充足 → 允许上传
│ ↓
│ 文件上传成功 → 调用 increaseUsedStorage(userId, fileSizeBytes)
└── 余额不足 → 抛出异常,拒绝上传
```
#### 3.2.4 额度释放流程
```
用户删除文件
获取文件大小 fileSizeBytes字节
调用 MemberUserProfileService.decreaseUsedStorage(userId, fileSizeBytes)
内部逻辑字节转GB原子更新 usedStorage/remainingStorage
```
#### 3.2.5 MemberUserProfileService 增强
```java
// 新增方法
/**
* 校验存储空间是否足够
* @param userId 用户ID
* @param fileSizeBytes 文件大小(字节)
*/
void validateStorage(String userId, long fileSizeBytes);
/**
* 增加已使用存储空间
* @param userId 用户ID
* @param fileSizeBytes 文件大小(字节)
*/
void increaseUsedStorage(String userId, long fileSizeBytes);
/**
* 减少已使用存储空间
* @param userId 用户ID
* @param fileSizeBytes 文件大小(字节)
*/
void decreaseUsedStorage(String userId, long fileSizeBytes);
```
#### 3.2.6 Mapper 原子更新
```java
// MemberUserProfileMapper.java 新增
/**
* 原子增加已用存储(乐观锁)
* @param userId 用户ID
* @param storageGb 增加的存储量GBBigDecimal转String
* @return 影响行数0表示余额不足
*/
@Update("UPDATE muye_member_user_profile " +
"SET used_storage = used_storage + #{storageGb}, " +
" remaining_storage = remaining_storage - #{storageGb}, " +
" update_time = NOW() " +
"WHERE user_id = #{userId} AND remaining_storage >= #{storageGb}")
int updateStorageIncrease(@Param("userId") String userId, @Param("storageGb") String storageGb);
/**
* 原子减少已用存储
*/
@Update("UPDATE muye_member_user_profile " +
"SET used_storage = used_storage - #{storageGb}, " +
" remaining_storage = remaining_storage + #{storageGb}, " +
" update_time = NOW() " +
"WHERE user_id = #{userId} AND used_storage >= #{storageGb}")
int updateStorageDecrease(@Param("userId") String userId, @Param("storageGb") String storageGb);
```
#### 3.2.7 每日对账Job
```java
@Component
public class OssQuotaReconcileJob {
@Scheduled(cron = "0 0 3 * * ?") // 每日凌晨3点
public void reconcile() {
// 1. 查询所有用户的档案
// 2. 统计每个用户 muye_material_file 表中文件总大小(字节)
// 3. 转换为 GB与 usedStorage 对比
// 4. 不一致则修正并记录日志
}
}
```
#### 3.2.8 API 设计
使用现有的 `/webApi/api/tik/member-profile/get` 接口,返回数据已包含存储额度信息。
```
---
### 3.3 OSS 存储管理
#### 3.3.1 素材列表集成
`MaterialListNew.vue` 顶部工具栏显示存储额度:
```vue
<div class="storage-quota-info">
<DatabaseOutlined />
<span>{{ formatStorage(usedStorage) }} / {{ formatStorage(totalStorage) }}</span>
<a-progress :percent="storagePercent" size="small" />
</div>
```
#### 3.3.2 个人中心优化
`Profile.vue` 已有存储空间显示,需要确保数据来源正确:
-`userStore.remainingStorage` 读取
- 确保 `getUserProfile()` API 返回正确的字节数据
---
## 四、任务分解
### Phase 1: 后端基础(优先级:高)
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | 新增模型配置列表API | `AppAiModelConfigController.java` | 添加 `/list-enabled` 接口 |
| 1.2 | 增强用户档案服务 | `MemberUserProfileServiceImpl.java` | 添加存储校验和更新方法 |
| 1.3 | 新增Mapper原子更新 | `MemberUserProfileMapper.java` | 添加存储增减的原子操作 |
| 1.4 | 新增每日对账Job | `OssQuotaReconcileJob.java` | 实现自动对账 |
### Phase 2: 前端积分系统(优先级:高)
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 2.1 | 创建模型配置API | `api/modelConfig.js` | 封装配置获取接口 |
| 2.2 | 创建积分配置Store | `stores/pointsConfig.js` | 管理配置状态 |
| 2.3 | 创建积分标签组件 | `components/common/PointsTag.vue` | 可复用的积分显示组件 |
| 2.4 | 应用启动时加载配置 | `App.vue` 或入口文件 | 初始化积分配置 |
### Phase 3: 业务模块集成(优先级:中)
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 3.1 | 智能体模块集成 | `ChatDrawer.vue` | 显示对话积分消耗 |
| 3.2 | 数字人模块集成 | `dh/Video.vue` | 显示生成积分消耗 |
| 3.3 | 对标分析集成 | `Benchmark.vue` | 显示分析积分消耗 |
| 3.4 | 热点趋势集成 | `Forecast.vue` | 显示文案生成积分消耗 |
### Phase 4: OSS存储管理优先级
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 4.1 | 素材列表显示额度 | `MaterialListNew.vue` | 工具栏显示存储额度 |
| 4.2 | 上传前额度校验 | `useUpload.js` | 上传前检查额度 |
| 4.3 | 删除后释放额度 | 后端文件删除接口 | 调用 decreaseUsedStorage |
| 4.4 | 个人中心优化 | `Profile.vue` | 确保显示正确字节值 |
---
## 五、风险分析
| 风险 | 影响 | 对策 |
|------|------|------|
| 积分配置加载失败 | 前端显示0积分 | 添加默认值和重试机制 |
| 并发上传导致额度超限 | 超出配额 | 使用数据库乐观锁或原子操作 |
| 对账Job执行时间过长 | 影响系统性能 | 分批处理,添加超时控制 |
| 历史数据不一致 | 对账修正幅度大 | 先做数据盘点,再逐步修正 |
---
## 六、测试计划
### 6.1 单元测试
- [ ] `PointsConfigStore` 状态管理测试
- [ ] `TikUserQuotaService` 额度计算测试
- [ ] 对账Job逻辑测试
### 6.2 集成测试
- [ ] 前端积分显示正确性
- [ ] OSS上传额度校验
- [ ] OSS删除额度释放
- [ ] 每日对账执行
### 6.3 验收测试
```
Benchark Checklist:
1. ✅ 智能体对话页面显示"消耗 X 积分"
2. ✅ 数字人生成页面显示"消耗 X 积分"
3. ✅ 对标分析页面显示"消耗 X 积分"
4. ✅ 热点趋势页面显示"消耗 X 积分"
5. ✅ 素材列表显示存储额度进度条
6. ✅ 上传大文件超出额度时提示错误
7. ✅ 删除文件后额度正确释放
8. ✅ 个人中心显示正确的存储额度
```
---
## 七、实施顺序建议
```
Week 1: Phase 1 (后端基础) + Phase 2 (前端积分系统)
Week 2: Phase 3 (业务模块集成) + Phase 4 (OSS存储管理)
```
---
## 八、相关文件清单
### 后端文件
| 操作 | 文件路径 |
|------|----------|
| 修改 | `yudao-module-tik/.../aimodelconfig/AiModelConfigController.java` |
| 修改 | `yudao-module-tik/.../memberuserprofile/service/MemberUserProfileService.java` |
| 修改 | `yudao-module-tik/.../memberuserprofile/service/MemberUserProfileServiceImpl.java` |
| 修改 | `yudao-module-tik/.../memberuserprofile/mapper/MemberUserProfileMapper.java` |
| 新增 | `yudao-module-tik/.../memberuserprofile/job/OssQuotaReconcileJob.java` |
### 前端文件
| 操作 | 文件路径 |
|------|----------|
| 新增 | `frontend/app/web-gold/src/api/modelConfig.js` |
| 新增 | `frontend/app/web-gold/src/stores/pointsConfig.js` |
| 新增 | `frontend/app/web-gold/src/components/common/PointsTag.vue` |
| 修改 | `frontend/app/web-gold/src/views/agents/ChatDrawer.vue` |
| 修改 | `frontend/app/web-gold/src/views/dh/Video.vue` |
| 修改 | `frontend/app/web-gold/src/views/content-style/Benchmark.vue` |
| 修改 | `frontend/app/web-gold/src/views/trends/Forecast.vue` |
| 修改 | `frontend/app/web-gold/src/views/material/MaterialListNew.vue` |
| 修改 | `frontend/app/web-gold/src/composables/useUpload.js` |