|
|
@@ -148,7 +148,7 @@ class DeepSeekService
|
|
|
Utils::throwError('20003:仅Gemini模型支持视频输入');
|
|
|
}
|
|
|
|
|
|
- // Gemini 多模态(图片/视频 -> 文本)按模型预检积分;纯文本不扣费
|
|
|
+ // 计费预检:纯文生文按 normal 档(3);有参考图/视频输入按模型固定价格(图生文)
|
|
|
$uid = Site::getUid();
|
|
|
$chargeModality = '';
|
|
|
$chargePoints = 0;
|
|
|
@@ -156,15 +156,15 @@ class DeepSeekService
|
|
|
|| $this->messagesContainMedia($messages, ['image_url', 'image']);
|
|
|
$hasVideoMedia = !empty($videos) || !empty($videoUrls)
|
|
|
|| $this->messagesContainMedia($messages, ['video_url', 'video']);
|
|
|
- if ($isGeminiModel && ($hasImageMedia || $hasVideoMedia)) {
|
|
|
+ if ($hasImageMedia || $hasVideoMedia) {
|
|
|
$chargeModality = $hasVideoMedia ? 'video' : 'image';
|
|
|
- $pricingModel = $this->geminiMultimodalPricingModel($model);
|
|
|
- if ($pricingModel !== '') {
|
|
|
- $chargePoints = $this->pointsService->getVideoChargePoints(['model' => $pricingModel]);
|
|
|
- if ($chargePoints > 0) {
|
|
|
- $this->pointsService->checkUserPointsEnough($chargePoints, $uid);
|
|
|
- }
|
|
|
- }
|
|
|
+ $chargePoints = $this->pointsService->getImageChatChargePoints($model);
|
|
|
+ } else {
|
|
|
+ $chargeModality = 'text';
|
|
|
+ $chargePoints = $this->pointsService->getChatChargePoints('', 'normal');
|
|
|
+ }
|
|
|
+ if ($chargePoints > 0) {
|
|
|
+ $this->pointsService->checkUserPointsEnough($chargePoints, $uid);
|
|
|
}
|
|
|
|
|
|
// 如果没有提供messages,则根据system_prompt、content和images构建
|
|
|
@@ -335,7 +335,7 @@ class DeepSeekService
|
|
|
return $this->wrapGenerateTextCharge($streamGenerator, $uid, $model, $chargePoints, $chargeModality);
|
|
|
}
|
|
|
|
|
|
- // 纯文本(不扣积分):流式生成成功后仅记录 token 使用明细
|
|
|
+ // 免费场景(0 积分):流式生成成功后仅记录 token 使用明细
|
|
|
return $this->wrapGenerateTextTokenRecord($streamGenerator, $uid, $model);
|
|
|
}
|
|
|
|
|
|
@@ -363,7 +363,7 @@ class DeepSeekService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 流式生成成功后再按 Gemini 多模态规则扣费
|
|
|
+ * 流式生成成功后再按档位/模型规则扣费(文生文 normal 档 / 图生文按模型固定价格)
|
|
|
*
|
|
|
* @param \Generator $generator
|
|
|
* @param int $uid
|
|
|
@@ -379,7 +379,7 @@ class DeepSeekService
|
|
|
if (!$charged && isset($chunk['type']) && $chunk['type'] === 'done') {
|
|
|
$fullContent = (string)getProp($chunk, 'full_content', getProp($chunk, 'answer', ''));
|
|
|
if ($fullContent !== '') {
|
|
|
- $this->chargeGeminiMultimodal($uid, $model, $points, $modality, getProp($chunk, 'usage', []));
|
|
|
+ $this->chargeMultimodal($uid, $model, $points, $modality, getProp($chunk, 'usage', []));
|
|
|
$charged = true;
|
|
|
}
|
|
|
}
|
|
|
@@ -388,7 +388,7 @@ class DeepSeekService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Gemini 多模态(图片/视频 -> 文本)成功后扣费并记录 token 明细
|
|
|
+ * AI 对话(含图片/视频 -> 文本)成功后扣费并记录积分与 token 明细
|
|
|
*
|
|
|
* @param int $uid
|
|
|
* @param string $model
|
|
|
@@ -397,7 +397,7 @@ class DeepSeekService
|
|
|
* @param mixed $usage
|
|
|
* @return void
|
|
|
*/
|
|
|
- private function chargeGeminiMultimodal(int $uid, string $model, int $points, string $modality, $usage): void
|
|
|
+ private function chargeMultimodal(int $uid, string $model, int $points, string $modality, $usage): void
|
|
|
{
|
|
|
$result = $this->pointsService->recordChatCharge(
|
|
|
$uid,
|
|
|
@@ -412,29 +412,11 @@ class DeepSeekService
|
|
|
$model
|
|
|
);
|
|
|
if (empty($result['charged'])) {
|
|
|
- throw new \Exception('Gemini多模态计费失败: ' . (string)($result['reason'] ?? 'unknown'));
|
|
|
+ throw new \Exception('AI对话计费失败: ' . (string)($result['reason'] ?? 'unknown'));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Gemini 文本模型 ID 转 mp_video_models 中的按次计费模型 ID
|
|
|
- *
|
|
|
- * @param string $model
|
|
|
- * @return string
|
|
|
- */
|
|
|
- private function geminiMultimodalPricingModel(string $model): string
|
|
|
- {
|
|
|
- $map = [
|
|
|
- 'gemini-3.1-flash-image-preview-t' => 'gemini-3.1-flash',
|
|
|
- 'gemini-3.1-pro-preview' => 'gemini-3.1-pro',
|
|
|
- 'gemini-3-flash-preview' => 'gemini-3.0-flash',
|
|
|
- 'gemini-3-pro-preview' => 'gemini-3.0-pro',
|
|
|
- 'gemini-3.6-flash' => 'gemini-3.6-flash',
|
|
|
- ];
|
|
|
- return $map[$model] ?? '';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
* 检查 OpenAI 风格消息中是否包含指定媒体类型
|
|
|
*
|
|
|
* @param array $messages
|
|
|
@@ -524,13 +506,13 @@ class DeepSeekService
|
|
|
* @param array $chargeInfo
|
|
|
* @return void
|
|
|
*/
|
|
|
- private function chargeChatSuccess(int $uid, int $tokens, array $chargeInfo): void
|
|
|
+ private function chargeChatSuccess(int $uid, int $tokens, array $chargeInfo, string $chargeType = 'special'): void
|
|
|
{
|
|
|
// api_type 记录真实模型名(缺失时回退 deepseek 大类),保证明细与统计口径准确
|
|
|
$model = (string)getProp($chargeInfo, 'model', '');
|
|
|
$result = $this->pointsService->recordChatCharge(
|
|
|
$uid,
|
|
|
- $this->pointsService->getChatChargePoints($model),
|
|
|
+ $this->pointsService->getChatChargePoints($model, $chargeType),
|
|
|
$tokens,
|
|
|
$chargeInfo,
|
|
|
'',
|
|
|
@@ -3503,6 +3485,10 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
|
|
|
// 与推理模型对话
|
|
|
public function chatWithReasoner($data) {
|
|
|
+ // 文生文计费预检(normal 档)
|
|
|
+ $uid = Site::getUid();
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', 'normal'), $uid);
|
|
|
+
|
|
|
$content = getProp($data, 'content');
|
|
|
$model = getProp($data, 'model', 'r1');
|
|
|
|
|
|
@@ -3598,11 +3584,11 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
'words' => getProp($script_content, 'words'),
|
|
|
];
|
|
|
|
|
|
- // 仅记录 token 使用明细(不扣积分)
|
|
|
- $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ // 文生文成功计费(normal 档):同时记录积分与 token 明细
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'model' => $model,
|
|
|
'source' => 'chatWithReasoner',
|
|
|
- ], $model);
|
|
|
+ ], 'normal');
|
|
|
|
|
|
return $result;
|
|
|
}
|
|
|
@@ -4462,6 +4448,10 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
* @return \Generator 返回生成器,用于流式输出
|
|
|
*/
|
|
|
public function generateEpisodes($data) {
|
|
|
+ // 文生文计费预检(special 档,每次调用扣 10)
|
|
|
+ $uid = Site::getUid();
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', 'special'), $uid);
|
|
|
+
|
|
|
$script_id = getProp($data, 'script_id');
|
|
|
$group_id = getProp($data, 'group_id');
|
|
|
// $file = getProp($data, 'file');
|
|
|
@@ -4686,20 +4676,20 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}else {
|
|
|
Utils::throwError('20003:分集剧本解析失败');
|
|
|
}
|
|
|
+
|
|
|
+ // 文生文成功计费(special 档):与落库同事务
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'group_id' => $group_id,
|
|
|
+ 'source' => 'generateEpisodes',
|
|
|
+ ]);
|
|
|
}catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
|
Utils::throwError('20003:'.$e->getMessage());
|
|
|
}
|
|
|
DB::commit();
|
|
|
|
|
|
- // 仅记录 token 使用明细(不扣积分)
|
|
|
- $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
- 'model' => $model,
|
|
|
- 'script_id' => $script_id,
|
|
|
- 'group_id' => $group_id,
|
|
|
- 'source' => 'generateEpisodes',
|
|
|
- ], $model);
|
|
|
-
|
|
|
yield [
|
|
|
'type' => 'done',
|
|
|
'script' => $script_arr,
|
|
|
@@ -4711,6 +4701,10 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
|
|
|
|
public function chatWithFileStream($data) {
|
|
|
+ // 剧本解析资产表计费预检(special 档)
|
|
|
+ $uid = Site::getUid();
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', 'special'), $uid);
|
|
|
+
|
|
|
$script_id = getProp($data, 'script_id');
|
|
|
$group_id = getProp($data, 'group_id');
|
|
|
$file = getProp($data, 'file');
|
|
|
@@ -4964,6 +4958,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
Utils::throwError('20003:保存分集失败');
|
|
|
}
|
|
|
|
|
|
+ // 剧本解析资产表成功计费(special 档):与落库同事务
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'model' => $model,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'group_id' => $group_id,
|
|
|
+ 'source' => 'chatWithFileStream',
|
|
|
+ ], 'special');
|
|
|
+
|
|
|
|
|
|
// $episodes = [];
|
|
|
// foreach ($script_arr['episodes'] as $item) {
|
|
|
@@ -4991,14 +4993,6 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
|
DB::commit();
|
|
|
|
|
|
- // 仅记录 token 使用明细(不扣积分)
|
|
|
- $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
- 'model' => $model,
|
|
|
- 'script_id' => $script_id,
|
|
|
- 'group_id' => $group_id,
|
|
|
- 'source' => 'chatWithFileStream',
|
|
|
- ], $model);
|
|
|
-
|
|
|
yield [
|
|
|
'type' => 'done',
|
|
|
'script' => $script_arr,
|
|
|
@@ -5018,6 +5012,10 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
* @return array
|
|
|
*/
|
|
|
public function chatWithFile($data) {
|
|
|
+ // 剧本解析资产表计费预检(special 档)
|
|
|
+ $uid = Site::getUid();
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', 'special'), $uid);
|
|
|
+
|
|
|
$script_id = getProp($data, 'script_id');
|
|
|
$file = getProp($data, 'file');
|
|
|
$content = getProp($data, 'content', '');
|
|
|
@@ -5138,12 +5136,12 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
|
]);
|
|
|
|
|
|
- // 仅记录 token 使用明细(不扣积分)
|
|
|
- $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ // 剧本解析资产表成功计费(special 档):同时记录积分与 token 明细
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'model' => $model,
|
|
|
'script_id' => $script_id,
|
|
|
'source' => 'chatWithFile',
|
|
|
- ], $model);
|
|
|
+ ], 'special');
|
|
|
|
|
|
return [
|
|
|
'script' => $script_arr,
|
|
|
@@ -12301,11 +12299,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
Utils::throwError('20003:片段ID和剧集ID只能选择一个');
|
|
|
}
|
|
|
|
|
|
- // 积分余额预检(仅剧集ID模式收费)
|
|
|
- if (!empty($episode_id)) {
|
|
|
+ // 积分余额预检:act_id 单片段模式按 normal 档,episode_id 剧集模式按 special 档
|
|
|
+ $chargeType = $isActIdMode ? 'normal' : 'special';
|
|
|
+ if ($isActIdMode) {
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', $chargeType), $uid);
|
|
|
+ } else {
|
|
|
$is_generated = DB::table('mp_anime_episodes')->where('id', $episode_id)->value('is_generated');
|
|
|
if ($is_generated) Utils::throwError('20003:该分集已不支持重新生成片段,请创建副本后重试!');
|
|
|
- $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints(), $uid);
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', $chargeType), $uid);
|
|
|
}
|
|
|
|
|
|
// 如果提供了template_id,获取模板提示词
|
|
|
@@ -12878,6 +12879,14 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
|
|
|
// 预览确认模式(check_mode=1):不落库,组装 editAct 入参返回,由前端确认后调用 editAct 更新
|
|
|
if ($check_mode === 1) {
|
|
|
+ // 单片段预览生成即按 normal 档扣费
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'anime_id' => $target_anime_id,
|
|
|
+ 'episode_id' => $target_episode_id,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'regenerateSegmentScript',
|
|
|
+ ], 'normal');
|
|
|
+
|
|
|
return [
|
|
|
'check_mode' => 1,
|
|
|
'act_id' => $act_id,
|
|
|
@@ -12924,18 +12933,26 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
|
|
|
|
dLog('deepseek')->info('单个片段重新生成成功', ['act_id' => $act_id]);
|
|
|
+
|
|
|
+ // 单片段模式成功扣费(normal 档)
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ 'anime_id' => $target_anime_id,
|
|
|
+ 'episode_id' => $target_episode_id,
|
|
|
+ 'model' => $model,
|
|
|
+ 'source' => 'regenerateSegmentScript',
|
|
|
+ ], 'normal');
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
// 预览确认模式(check_mode=1):不落库,返回展示参数供前端确认后调用 saveEpisodeActs 保存
|
|
|
if ($check_mode === 1) {
|
|
|
- // 预览生成即扣除积分并记录 token 明细(与 check_mode=0 保存时扣费一致)
|
|
|
+ // 剧集模式预览生成即扣除积分并记录 token 明细(special 档)
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $target_anime_id,
|
|
|
'episode_id' => $target_episode_id,
|
|
|
'model' => $model,
|
|
|
'source' => 'regenerateSegmentScript',
|
|
|
- ]);
|
|
|
+ ], 'special');
|
|
|
|
|
|
// 为每个片段生成虚拟 act_id 并写入 redis(1天有效),供后续按虚拟 act_id 恢复片段数据
|
|
|
$previewActs = [];
|
|
|
@@ -13011,13 +13028,13 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
DB::table('mp_episode_segments')->insert($segments_to_insert);
|
|
|
}
|
|
|
|
|
|
- // 剧集ID模式调用成功,返回结果前扣除积分并记录明细(与保存同事务)
|
|
|
+ // 剧集ID模式调用成功,返回结果前扣除积分并记录明细(special 档,与保存同事务)
|
|
|
$this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'anime_id' => $target_anime_id,
|
|
|
'episode_id' => $target_episode_id,
|
|
|
'model' => $model,
|
|
|
'source' => 'regenerateSegmentScript',
|
|
|
- ]);
|
|
|
+ ], 'special');
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
@@ -14879,6 +14896,10 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
}
|
|
|
|
|
|
public function chatChangeImg($data) {
|
|
|
+ // 文生文计费预检(normal 档)
|
|
|
+ $uid = Site::getUid();
|
|
|
+ $this->pointsService->checkUserPointsEnough($this->pointsService->getChatChargePoints('', 'normal'), $uid);
|
|
|
+
|
|
|
$segment = getProp($data, 'segment');
|
|
|
$segment_content = getProp($segment, 'segment_content');
|
|
|
$prompt = getProp($data, 'prompt');
|
|
|
@@ -14942,11 +14963,11 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
|
|
|
Utils::throwError('20003: 接口调用失败!');
|
|
|
}
|
|
|
|
|
|
- // 仅记录 token 使用明细(不扣积分)
|
|
|
- $this->recordTextTokenOnly($this->getCurrentUid(), $this->pointsService->getTokensFromUsage($usage), [
|
|
|
+ // 文生文成功计费(normal 档):同时记录积分与 token 明细
|
|
|
+ $this->chargeChatSuccess($uid, $this->pointsService->getTokensFromUsage($usage), [
|
|
|
'model' => $model,
|
|
|
'source' => 'chatChangeImg',
|
|
|
- ], $model);
|
|
|
+ ], 'normal');
|
|
|
|
|
|
return [
|
|
|
'type' => 'done',
|