feat(points): 积分流水表支持时长显示,说明字段简化

后端:
- PointTransaction 模型添加 duration 字段(float, nullable)
- PointTransactionItem schema 添加 duration
- consume() 新增 duration 参数,写入流水记录
- 各业务 description 统一简化为【脚本生成】【配音合成】等格式
- duration 类业务(tts/video)传入实际秒数
- Alembic 迁移: 95eb1a1c0af9_add_duration_to_point_transaction

前端:
- PointTransaction 类型添加 duration
- UsageDetail: 来源列 → 时长列(有值显示 xs,无值显示 -)
- 说明列直接显示后端返回的简化描述
This commit is contained in:
小鱼开发
2026-05-09 17:08:50 +08:00
parent 368fdfa094
commit 0722225c62
11 changed files with 51 additions and 11 deletions
@@ -0,0 +1,27 @@
"""
积分流水表添加 duration 字段
用于记录按秒计费业务的时长(TTS、数字人视频等)。
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "95eb1a1c0af9"
down_revision: Union[str, None] = "ccf61ff6f4bb"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"mjk_point_transactions",
sa.Column("duration", sa.Float(), nullable=True),
)
def downgrade() -> None:
op.drop_column("mjk_point_transactions", "duration")
+1 -1
View File
@@ -444,7 +444,7 @@ async def consume_points(
points=request.points,
source_type=request.source_type,
source_id=request.source_id,
description=request.description,
description=f"{request.description or request.source_type}",
)
await db.commit()
+2 -2
View File
@@ -82,7 +82,7 @@ async def polish_content(
points=points,
source_type="polish",
source_id=f"polish_{current_user.id}_{asyncio.get_event_loop().time()}",
description=f"润色 {request.polish_type}",
description="【文案润色】",
)
await db.commit()
@@ -211,7 +211,7 @@ async def generate_title(
points=points,
source_type="title",
source_id=f"title_{current_user.id}_{asyncio.get_event_loop().time()}",
description=f"生成{request.title_type}标题",
description="【标题生成】",
)
await db.commit()
+5 -3
View File
@@ -231,7 +231,8 @@ async def synthesize_speech(
points=points,
source_type="tts",
source_id=f"tts_{current_user.id}_{asyncio.get_event_loop().time()}",
description=f"TTS 配音 {seconds:.1f}",
description="【配音合成】",
duration=seconds,
)
await db.commit()
except Exception as e:
@@ -317,7 +318,8 @@ async def synthesize_batch(
points=points,
source_type="tts",
source_id=f"tts_batch_{current_user.id}_{asyncio.get_event_loop().time()}",
description=f"批量 TTS 配音 {total_seconds:.1f}",
description="【配音合成】",
duration=total_seconds,
)
await db.commit()
except Exception as e:
@@ -448,7 +450,7 @@ async def submit_clone_task(
points=points,
source_type="voice_clone",
source_id=result.get("voice_id", "unknown"),
description="声音克隆",
description="声音克隆",
)
await db.commit()
except Exception as e:
@@ -73,6 +73,11 @@ class PointTransaction(BaseModelBigInt):
comment="关联的积分批次 ID(消费时记录从哪个批次扣)",
)
duration: Mapped[float | None] = mapped_column(
nullable=True,
comment="时长(秒),按秒计费业务记录",
)
description: Mapped[str | None] = mapped_column(
Text,
nullable=True,
@@ -145,7 +145,7 @@ class ScriptHandler(AsyncHandler):
points=points,
source_type="script",
source_id=task.task_id,
description=f"脚本生成 {category}/{subcategory}",
description="脚本生成",
)
await db.commit()
except Exception as e:
@@ -78,7 +78,8 @@ class VideoHandler(AsyncHandler):
points=points,
source_type="video",
source_id=task.task_id,
description=f"对口型视频 {duration:.1f}",
description="【数字人视频】",
duration=duration,
)
await db.commit()
except Exception as e:
@@ -135,7 +136,8 @@ class VideoHandler(AsyncHandler):
points=points,
source_type="video",
source_id=task.task_id,
description=f"对口型视频 {duration:.1f}",
description="【数字人视频】",
duration=duration,
)
await db.commit()
except Exception as e:
+1
View File
@@ -32,6 +32,7 @@ class PointTransactionItem(BaseModel):
balance_after: int
source_type: str | None = Field(None, description="消费来源类型")
source_id: str | None = Field(None, description="消费来源业务 ID")
duration: float | None = Field(None, description="时长(秒),按秒计费业务记录")
description: str | None = None
created_at: datetime
+2
View File
@@ -272,6 +272,7 @@ async def consume(
source_type: str,
source_id: str,
description: str = "",
duration: float | None = None,
) -> PointTransaction:
"""
直接扣费(后置计费)。
@@ -343,6 +344,7 @@ async def consume(
source_type=source_type,
source_id=source_id,
batch_id=batches[0].id if batches else None,
duration=duration,
description=description or f"消费 {source_type} {points} 积分",
)
db.add(tx)
+1
View File
@@ -40,6 +40,7 @@ export interface PointTransaction {
balanceAfter: number;
sourceType: string | null;
sourceId: string | null;
duration: number | null;
description: string | null;
createdAt: string;
}
+2 -2
View File
@@ -100,7 +100,7 @@ export default function UsageDetail() {
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
@@ -149,7 +149,7 @@ export default function UsageDetail() {
</td>
<td>{tx.balanceBefore}</td>
<td>{tx.balanceAfter}</td>
<td>{tx.sourceType || '-'}</td>
<td>{tx.duration != null ? `${tx.duration.toFixed(1)}s` : '-'}</td>
<td>{tx.description || '-'}</td>
<td>{formatTime(tx.createdAt)}</td>
</tr>