PointsDisplay.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <?php
  2. namespace App\Libs;
  3. use App\Models\MpUserPointsDetail;
  4. use Illuminate\Support\Facades\DB;
  5. /**
  6. * 积分明细展示辅助类
  7. *
  8. * 统一定义积分流水(列表/导出)对外展示的模型名与分辨率:
  9. * - 模型名:隐藏供应商字样(百度/快快AI/智帧/字节跳动等),如百度Seedance2.0 → Seedance2.0
  10. * 模型展示名优先取配置表(mp_video_models / mp_image_models / mp_text_models)的 name,
  11. * 配置缺失时回退内置映射,仍未命中则仅剥离供应商前缀原样返回。
  12. * 备渠道模型(seed-2 开头)追加“(备)”,避免与主渠道模型同名。
  13. * - 分辨率:视频显示 480p/720p/1080p/4k 与 超分720p/超分1080p;图片显示 实际宽*高(如 1600*2848);
  14. * 其他类型(AI对话/积分发放等)返回空串。
  15. */
  16. class PointsDisplay
  17. {
  18. /**
  19. * 需要从展示名中隐藏的供应商字样(仅处理名称开头的前缀)
  20. */
  21. const VENDOR_WORDS = [
  22. '百度',
  23. '快快AI',
  24. '快快',
  25. '智帧平台',
  26. '智帧',
  27. '字节跳动',
  28. ];
  29. /**
  30. * 英文供应商前缀(模型ID开头)
  31. */
  32. const VENDOR_PREFIXES = [
  33. 'baidu-',
  34. 'kuaikuai-',
  35. ];
  36. /**
  37. * 备渠道模型前缀(模型ID):展示名统一追加“(备)”,与主渠道模型区分
  38. */
  39. const RESERVE_MODEL_PREFIXES = [
  40. 'seed-2',
  41. ];
  42. /**
  43. * 配置表缺失时的内置兜底映射(模型ID => 展示名)
  44. */
  45. const MODEL_NAME_FALLBACK = [
  46. // 视频(Seedance 系列)
  47. 'doubao-seedance-1-5-pro-251215' => 'Seedance1.5pro',
  48. 'doubao-seedance-1-0-pro-250528' => 'Seedance1.0pro',
  49. 'doubao-seedance-1-0-pro-fast-251015' => 'Seedance1.0profast',
  50. 'doubao-seedance-2-0-260128' => 'Seedance2.0',
  51. 'doubao-seedance-2-0-fast-260128' => 'Seedance2.0fast',
  52. 'doubao-seedance-2.0' => 'Seedance2.0',
  53. 'baidu-doubao-seedance-2-0-260128' => 'Seedance2.0',
  54. 'baidu-doubao-seedance-2-0-fast-260128' => 'Seedance2.0fast',
  55. 'baidu-doubao-seedance-2-0-mini-260615' => 'Seedance2.0mini',
  56. 'baidu-doubao-seedance-2-5-260628' => 'Seedance2.5',
  57. 'baidu-dreamina-seedance-2-0-260128' => 'Seedance2.0(海外)',
  58. 'baidu-dreamina-seedance-2-0-fast-260128' => 'Seedance2.0fast(海外)',
  59. 'baidu-dreamina-seedance-2-0-mini-260615' => 'Seedance2.0mini(海外)',
  60. 'baidu-dreamina-seedance-2-5-260628' => 'Seedance2.5(海外)',
  61. 'zhizhen-20' => 'Seedance2.0',
  62. 'zhizhen-20-fast' => 'Seedance2.0fast',
  63. 'zhizhen-20-mini' => 'Seedance2.0mini',
  64. 'seed-2' => 'Seedance2.0',
  65. 'seed-2-fast' => 'Seedance2.0fast',
  66. 'seed-2-mini' => 'Seedance2.0mini',
  67. 'seed-2.5' => 'Seedance2.5',
  68. // 文本
  69. 'baidu-glm-5.2' => 'GLM-5.2',
  70. 'baidu-glm-5.3' => 'GLM-5.3',
  71. 'baidu-gpt-5.6-luna' => 'GPT-5.6-luna',
  72. 'baidu-gpt-5.6-sol' => 'GPT-5.6-sol',
  73. 'baidu-gpt-5.6-terra' => 'GPT-5.6-terra',
  74. 'baidu-gpt-6-astra' => 'GPT-6-astra',
  75. 'baidu-gpt-6-luna' => 'GPT-6-luna',
  76. 'baidu-gpt-6-sol' => 'GPT-6-sol',
  77. 'baidu-gpt-6.1-sol' => 'GPT-6.1-sol',
  78. ];
  79. /**
  80. * 视频分辨率取值 => 展示文案
  81. */
  82. const VIDEO_RESOLUTION_LABELS = [
  83. '480p' => '480p',
  84. '720p' => '720p',
  85. '1080p' => '1080p',
  86. '4k' => '4k',
  87. '2160p' => '4k',
  88. '4096x2160' => '4k',
  89. 'sr_720p' => '超分720p',
  90. 'sr720p' => '超分720p',
  91. '超分720p' => '超分720p',
  92. 'sr_1080p' => '超分1080p',
  93. 'sr1080p' => '超分1080p',
  94. '超分1080p' => '超分1080p',
  95. ];
  96. /**
  97. * 配置表读取的模型展示名缓存(单次请求内只查一次)
  98. *
  99. * @var array|null
  100. */
  101. private static $modelNameMap = null;
  102. /**
  103. * 获取模型对外展示名(隐藏百度/快快等供应商字样)
  104. *
  105. * @param string $model 模型ID(charge_info.model)
  106. * @return string
  107. */
  108. public static function modelName(string $model = ''): string
  109. {
  110. $model = trim($model);
  111. if ($model === '' || $model === 'unknown') {
  112. return '';
  113. }
  114. $map = self::modelNameMap();
  115. $name = $map[$model] ?? self::MODEL_NAME_FALLBACK[$model] ?? '';
  116. if ($name === '') {
  117. $name = $model;
  118. }
  119. return self::markReserveChannel($model, self::stripVendor($name));
  120. }
  121. /**
  122. * 备渠道标记:命中备渠道前缀(seed-2 开头)的模型,展示名追加“(备)”
  123. *
  124. * @param string $model 模型ID
  125. * @param string $name 已剥离供应商字样的展示名
  126. * @return string
  127. */
  128. private static function markReserveChannel(string $model, string $name): string
  129. {
  130. if ($name === '' || mb_strpos($name, '(备用)') !== false) {
  131. return $name;
  132. }
  133. foreach (self::RESERVE_MODEL_PREFIXES as $prefix) {
  134. if ($prefix !== '' && stripos($model, $prefix) === 0) {
  135. return $name . '(备用)';
  136. }
  137. }
  138. return $name;
  139. }
  140. /**
  141. * 获取明细的分辨率展示文案
  142. *
  143. * - 视频:480p/720p/1080p/4k、超分720p、超分1080p(来自 charge_info.video_resolution)
  144. * - 图片:实际宽*高,如 1600*2848(来自 charge_info.width / height)
  145. * - 其他类型或无数据:空串
  146. *
  147. * @param string $type 明细类型(video/image/chat/system/company)
  148. * @param array $chargeInfo 计费信息
  149. * @return string
  150. */
  151. public static function resolution(string $type, array $chargeInfo = []): string
  152. {
  153. if ($type === MpUserPointsDetail::TYPE_VIDEO) {
  154. $resolution = strtolower(trim((string)getProp($chargeInfo, 'video_resolution', '')));
  155. if ($resolution === '') {
  156. return '';
  157. }
  158. return self::VIDEO_RESOLUTION_LABELS[$resolution] ?? $resolution;
  159. }
  160. if ($type === MpUserPointsDetail::TYPE_IMAGE) {
  161. $width = (int)getProp($chargeInfo, 'width', 0);
  162. $height = (int)getProp($chargeInfo, 'height', 0);
  163. if ($width <= 0 || $height <= 0) {
  164. return '';
  165. }
  166. return $width . '*' . $height;
  167. }
  168. return '';
  169. }
  170. /**
  171. * 积分明细来源展示(charge_info.source_from → 中文)
  172. *
  173. * 规则:
  174. * - 1 → 资产
  175. * - 2 → 画布
  176. * - 0 / 缺失:有动漫或剧本归属时显示“动漫/剧本”,否则显示“默认”
  177. *
  178. * @param array $chargeInfo 明细计费信息
  179. * @param int $animeId 明细归属的动漫ID(为 0 时回退 charge_info.anime_id)
  180. * @param int $scriptId 明细归属的剧本ID(为 0 时回退 charge_info.script_id)
  181. * @return string
  182. */
  183. public static function sourceFrom(array $chargeInfo, $animeId = 0, $scriptId = 0): string
  184. {
  185. $sourceFrom = (int)getProp($chargeInfo, 'source_from', 0);
  186. if ($sourceFrom === 1) {
  187. return '资产';
  188. }
  189. if ($sourceFrom === 2) {
  190. return '画布';
  191. }
  192. $animeId = (int)$animeId ?: (int)getProp($chargeInfo, 'anime_id', 0);
  193. $scriptId = (int)$scriptId ?: (int)getProp($chargeInfo, 'script_id', 0);
  194. if ($animeId > 0 || $scriptId > 0) {
  195. return '动漫/剧本';
  196. }
  197. return '';
  198. }
  199. /**
  200. * 仅保留名称主体:剥离供应商字样前缀(百度Seedance2.0 → Seedance2.0,baidu-xxx → xxx)
  201. *
  202. * @param string $name
  203. * @return string
  204. */
  205. private static function stripVendor(string $name): string
  206. {
  207. $name = trim($name);
  208. // 英文前缀(模型ID形态)
  209. foreach (self::VENDOR_PREFIXES as $prefix) {
  210. if (stripos($name, $prefix) === 0) {
  211. $name = trim(substr($name, strlen($prefix)));
  212. }
  213. }
  214. // 中文前缀(配置表 name 形态),可能连续出现(如“百度智帧xxx”)
  215. $changed = true;
  216. while ($changed) {
  217. $changed = false;
  218. foreach (self::VENDOR_WORDS as $word) {
  219. if (mb_strpos($name, $word) === 0) {
  220. $name = trim(mb_substr($name, mb_strlen($word)));
  221. $changed = true;
  222. break;
  223. }
  224. }
  225. }
  226. return $name;
  227. }
  228. /**
  229. * 模型ID => 配置表展示名(读取失败或未命中时返回空数组,由调用方走兜底映射)
  230. *
  231. * @return array
  232. */
  233. private static function modelNameMap(): array
  234. {
  235. if (self::$modelNameMap !== null) {
  236. return self::$modelNameMap;
  237. }
  238. $map = [];
  239. foreach (['mp_video_models', 'mp_image_models', 'mp_text_models'] as $table) {
  240. try {
  241. $rows = DB::table($table)->select('model', 'name')->get();
  242. } catch (\Throwable $e) {
  243. continue;
  244. }
  245. foreach ($rows as $row) {
  246. $model = trim((string)getProp($row, 'model', ''));
  247. $name = trim((string)getProp($row, 'name', ''));
  248. if ($model !== '' && $name !== '') {
  249. $map[$model] = $name;
  250. }
  251. }
  252. }
  253. self::$modelNameMap = $map;
  254. return self::$modelNameMap;
  255. }
  256. }