VideoGenerationController.php 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218
  1. <?php
  2. namespace App\Http\Controllers\AIGeneration;
  3. use Illuminate\Routing\Controller as BaseController;
  4. use App\Libs\Utils;
  5. use App\Services\AIGeneration\AIVideoGenerationService;
  6. use App\Transformer\AIGeneration\AIGenerationTransformer;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Http\JsonResponse;
  9. use Illuminate\Support\Facades\Validator;
  10. use Illuminate\Support\Facades\DB;
  11. use App\Libs\ApiResponse;
  12. use App\Models\AIElement;
  13. class VideoGenerationController extends BaseController
  14. {
  15. use ApiResponse;
  16. private $aiVideoGenerationService;
  17. public function __construct(
  18. AIVideoGenerationService $aiVideoGenerationService
  19. ) {
  20. $this->aiVideoGenerationService = $aiVideoGenerationService;
  21. }
  22. /**
  23. * 创建视频生成任务(即梦AI)
  24. *
  25. * @param Request $request
  26. * @return JsonResponse
  27. */
  28. public function createJimengTask(Request $request): JsonResponse
  29. {
  30. $data = $request->all();
  31. // 首先验证基本参数
  32. $baseRules = [
  33. 'prompt' => 'required|string|max:1000',
  34. 'seed' => 'required|integer',
  35. 'video_duration' => 'required|in:5,10',
  36. 'video_resolution' => 'required|in:720P,1080P'
  37. ];
  38. $baseMessages = [
  39. 'prompt.required' => '提示词不能为空',
  40. 'prompt.max' => '提示词不能超过1000个字符',
  41. 'seed.required' => '随机种子不能为空',
  42. 'seed.integer' => '随机种子必须是整数',
  43. 'video_duration.required' => '请选择视频时长',
  44. 'video_duration.in' => '视频时长只能是5s或10s',
  45. 'video_resolution.required' => '请选择视频分辨率',
  46. 'video_resolution.in' => '视频分辨率只能是720P或1080P',
  47. ];
  48. // 动态构建验证规则
  49. $validationRules = $baseRules;
  50. $validationMessages = $baseMessages;
  51. // 检查是否有首帧URL
  52. if (isset($data['first_frame_url'])) {
  53. $validationRules['first_frame_url'] = 'url';
  54. $validationMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  55. }
  56. // 检查是否有尾帧URL
  57. if (isset($data['tail_frame_url'])) {
  58. $validationRules['tail_frame_url'] = 'url';
  59. $validationMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  60. }
  61. // 检查是否有首帧文件
  62. if ($request->hasFile('first_frame_file')) {
  63. $validationRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  64. $validationMessages = array_merge($validationMessages, [
  65. 'first_frame_file.image' => '首帧必须是图片文件',
  66. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  67. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  68. ]);
  69. }
  70. // 检查是否有尾帧文件
  71. if ($request->hasFile('tail_frame_file')) {
  72. $validationRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  73. $validationMessages = array_merge($validationMessages, [
  74. 'tail_frame_file.image' => '尾帧必须是图片文件',
  75. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  76. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  77. ]);
  78. }
  79. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  80. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  81. }
  82. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  83. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  84. }
  85. $validator = Validator::make($data, $validationRules, $validationMessages);
  86. if ($validator->fails()) {
  87. Utils::throwError('1002:'.$validator->errors()->first());
  88. }
  89. $firstFrame = null;
  90. $tailFrame = null;
  91. // 处理首帧图片(文件上传方式)
  92. if ($request->hasFile('first_frame_file')) {
  93. $firstFrameFile = $request->file('first_frame_file');
  94. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  95. $data['first_frame_url'] = $firstFrame['url'];
  96. }
  97. // 处理首帧图片(URL方式)
  98. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  99. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  100. if (!$firstFrame) {
  101. Utils::throwError('1003:首帧图片获取失败');
  102. }
  103. }
  104. // 处理尾帧图片(文件上传方式)
  105. if ($request->hasFile('tail_frame_file')) {
  106. $tailFrameFile = $request->file('tail_frame_file');
  107. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  108. $data['tail_frame_url'] = $tailFrame['url'];
  109. }
  110. // 处理尾帧图片(URL方式)
  111. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  112. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  113. if (!$tailFrame) {
  114. Utils::throwError('1003:尾帧图片获取失败');
  115. }
  116. }
  117. // 验证首帧图片规格(如果存在)
  118. if ($firstFrame) {
  119. $this->validateFrameSpecs($firstFrame, '首帧');
  120. }
  121. // 验证尾帧图片规格(如果存在)
  122. if ($tailFrame) {
  123. $this->validateFrameSpecs($tailFrame, '尾帧');
  124. }
  125. // 如果首尾帧都存在,验证比例是否相同
  126. if ($firstFrame && $tailFrame) {
  127. $firstRatio = round($firstFrame['width'] / $firstFrame['height'], 2);
  128. $tailRatio = round($tailFrame['width'] / $tailFrame['height'], 2);
  129. if ($firstRatio != $tailRatio) {
  130. Utils::throwError('1003:尾帧图片与首帧图片比例必须相同');
  131. }
  132. }
  133. $task = $this->aiVideoGenerationService->createJimengTask($data);
  134. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  135. }
  136. /**
  137. * 查询视频任务状态
  138. *
  139. * @param string $taskId
  140. * @return JsonResponse
  141. */
  142. public function taskStatus(string $taskId): JsonResponse
  143. {
  144. $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
  145. if (!$task) {
  146. return response()->json([
  147. 'success' => false,
  148. 'message' => '任务不存在'
  149. ], 404);
  150. }
  151. return response()->json([
  152. 'success' => true,
  153. 'data' => [
  154. 'task_id' => $task->id,
  155. 'status' => $task->status,
  156. 'result_url' => $task->result_url,
  157. 'error_message' => $task->error_message,
  158. 'created_at' => $task->created_at,
  159. 'started_at' => $task->started_at,
  160. 'completed_at' => $task->completed_at
  161. ]
  162. ]);
  163. }
  164. /**
  165. * 处理帧图片文件
  166. *
  167. * @param \Illuminate\Http\UploadedFile $file
  168. * @param string $frameType 帧类型(first/tail)
  169. * @return array
  170. */
  171. private function processFrameFile($file, string $frameType): array
  172. {
  173. // 验证图片信息
  174. $frameInfo = @getimagesize($file->getPathname());
  175. if (!$frameInfo) {
  176. Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片格式无效');
  177. }
  178. $frameData = [
  179. 'width' => $frameInfo[0],
  180. 'height' => $frameInfo[1],
  181. 'size' => $file->getSize(),
  182. ];
  183. // 上传图片到服务器
  184. try {
  185. $frameData['url'] = uploadFileByTos('image', $file);
  186. } catch (\Exception $e) {
  187. Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片上传失败:' . $e->getMessage());
  188. }
  189. return $frameData;
  190. }
  191. /**
  192. * 验证帧图片规格
  193. *
  194. * @param array $frame
  195. * @param string $frameName
  196. * @return void
  197. */
  198. private function validateFrameSpecs(array $frame, string $frameName): void
  199. {
  200. // 1.图片文件最大4.7MB
  201. if ($frame['size'] > 13 * 1024 * 1024) {
  202. Utils::throwError('1003:' . $frameName . '图片大小不能超过10MB');
  203. }
  204. // 2.图片分辨率最大: 4096*4096, 最短边不低于320
  205. $minDimension = min($frame['width'], $frame['height']);
  206. $maxDimension = max($frame['width'], $frame['height']);
  207. if ($maxDimension > 4096 || $minDimension < 320) {
  208. Utils::throwError('1003:' . $frameName . '图片分辨率不能超过4096*4096,且最短边不能低于320');
  209. }
  210. // 3.图片长边与短边比例在3以内
  211. if ($maxDimension / $minDimension > 3) {
  212. Utils::throwError('1003:' . $frameName . '图片长边与短边比例不能超过3');
  213. }
  214. }
  215. /**
  216. * 创建 Seedance 1.5 Pro 视频生成任务
  217. *
  218. * @param Request $request
  219. * @return JsonResponse
  220. */
  221. public function createSeedanceTask(Request $request): JsonResponse
  222. {
  223. $data = $request->all();
  224. // 验证基本参数
  225. $baseRules = [
  226. 'model' => 'required|string|in:doubao-seedance-1-5-pro-251215,doubao-seedance-1-0-pro-250528,doubao-seedance-1-0-pro-fast-251015',
  227. 'prompt' => 'required|max:1000',
  228. // 'content' => 'required|array|min:1',
  229. ];
  230. $baseMessages = [
  231. 'model.required' => '模型参数不能为空',
  232. 'model.in' => '模型只支持 doubao-seedance-1-5-pro-251215、doubao-seedance-1-0-pro-250528、doubao-seedance-1-0-pro-fast-251015',
  233. 'prompt.required' => '提示词不能为空',
  234. 'prompt.max' => '提示词不能超过1000个字符',
  235. // 'content.required' => '内容参数不能为空',
  236. // 'content.array' => '内容参数必须是数组',
  237. // 'content.min' => '内容参数至少包含一个元素',
  238. ];
  239. // 可选参数验证
  240. $optionalRules = [
  241. 'callback_url' => 'nullable|url',
  242. 'return_last_frame' => 'nullable|boolean',
  243. 'service_tier' => 'nullable|in:default,flex',
  244. 'execution_expires_after' => 'nullable|integer|min:3600|max:259200',
  245. 'generate_audio' => 'nullable|boolean',
  246. 'draft' => 'nullable|boolean',
  247. 'video_resolution' => 'required|in:480P,720P,1080P',
  248. 'ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9,adaptive',
  249. 'video_duration' => 'required|integer|min:2|max:12',
  250. 'frames' => 'nullable|integer|min:29|max:289',
  251. 'seed' => 'nullable|integer|min:-1|max:4294967295',
  252. 'camera_fixed' => 'nullable|boolean',
  253. 'watermark' => 'nullable|boolean',
  254. ];
  255. $optionalMessages = [
  256. 'callback_url.url' => '回调地址格式不正确',
  257. 'service_tier.in' => '服务等级只能是 default 或 flex',
  258. 'execution_expires_after.integer' => '任务超时时间必须是整数',
  259. 'execution_expires_after.min' => '任务超时时间不能少于3600秒',
  260. 'execution_expires_after.max' => '任务超时时间不能超过259200秒',
  261. 'video_resolution.required' => '请选择视频分辨率',
  262. 'video_resolution.in' => '分辨率只能是 480P、720P 或 1080P',
  263. 'ratio.in' => '宽高比参数不正确',
  264. 'video_duration.required' => '请填写视频时长',
  265. 'video_duration.integer' => '视频时长必须是整数',
  266. 'video_duration.min' => '视频时长不能少于2秒',
  267. 'video_duration.max' => '视频时长不能超过12秒',
  268. 'frames.integer' => '帧数必须是整数',
  269. 'frames.min' => '帧数不能少于29',
  270. 'frames.max' => '帧数不能超过289',
  271. 'seed.integer' => '随机种子必须是整数',
  272. 'seed.min' => '随机种子不能小于-1',
  273. 'seed.max' => '随机种子不能超过4294967295',
  274. ];
  275. $validationRules = array_merge($baseRules, $optionalRules);
  276. $validationMessages = array_merge($baseMessages, $optionalMessages);
  277. // 检查是否有首帧URL
  278. if (isset($data['first_frame_url'])) {
  279. $optionalRules['first_frame_url'] = 'url';
  280. $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  281. }
  282. // 检查是否有尾帧URL
  283. if (isset($data['tail_frame_url'])) {
  284. $optionalRules['tail_frame_url'] = 'url';
  285. $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  286. }
  287. // 检查是否有首帧文件
  288. if ($request->hasFile('first_frame_file')) {
  289. $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  290. $optionalMessages = array_merge($optionalMessages, [
  291. 'first_frame_file.image' => '首帧必须是图片文件',
  292. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  293. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  294. ]);
  295. }
  296. // 检查是否有尾帧文件
  297. if ($request->hasFile('tail_frame_file')) {
  298. $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  299. $optionalMessages = array_merge($optionalMessages, [
  300. 'tail_frame_file.image' => '尾帧必须是图片文件',
  301. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  302. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  303. ]);
  304. }
  305. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  306. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  307. }
  308. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  309. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  310. }
  311. $validator = Validator::make($data, $validationRules, $validationMessages);
  312. if ($validator->fails()) {
  313. Utils::throwError('1002:'.$validator->errors()->first());
  314. }
  315. $data['content'] = [
  316. [
  317. 'type' => 'text',
  318. 'text' => $data['prompt'],
  319. ],
  320. ];
  321. $firstFrame = null;
  322. $tailFrame = null;
  323. // 以下首尾帧做模型兼容:
  324. // doubao-seedance-1-0-pro-fast-251015只支持文生视频、图生视频-首帧
  325. // 处理首帧图片(文件上传方式)
  326. if ($request->hasFile('first_frame_file')) {
  327. $firstFrameFile = $request->file('first_frame_file');
  328. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  329. $data['first_frame_url'] = $firstFrame['url'];
  330. $data['content'][] = [
  331. 'type' => 'image_url',
  332. 'image_url' => [
  333. 'url' => $data['first_frame_url'],
  334. ],
  335. 'role' => 'first_frame',
  336. ];
  337. }
  338. // 处理首帧图片(URL方式)
  339. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  340. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  341. if (!$firstFrame) {
  342. Utils::throwError('1003:首帧图片获取失败');
  343. }
  344. $data['content'][] = [
  345. 'type' => 'image_url',
  346. 'image_url' => [
  347. 'url' => $data['first_frame_url'],
  348. ],
  349. 'role' => 'first_frame',
  350. ];
  351. }
  352. // 处理尾帧图片(文件上传方式)
  353. if ($request->hasFile('tail_frame_file')) {
  354. $tailFrameFile = $request->file('tail_frame_file');
  355. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  356. if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) {
  357. $data['tail_frame_url'] = $tailFrame['url'];
  358. $data['content'][] = [
  359. 'type' => 'image_url',
  360. 'image_url' => [
  361. 'url' => $data['tail_frame_url'],
  362. ],
  363. 'role' => 'last_frame',
  364. ];
  365. }
  366. }
  367. // 处理尾帧图片(URL方式)
  368. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  369. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  370. if (!$tailFrame) {
  371. Utils::throwError('1003:尾帧图片获取失败');
  372. }
  373. if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) {
  374. $data['content'][] = [
  375. 'type' => 'image_url',
  376. 'image_url' => [
  377. 'url' => $data['tail_frame_url'],
  378. ],
  379. 'role' => 'last_frame',
  380. ];
  381. }else {
  382. $data['tail_frame_url'] = '';
  383. }
  384. }
  385. // 验证模型参数
  386. if (!in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) {
  387. $data['generate_audio'] = false;
  388. $data['draft'] = false;
  389. }
  390. if (in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) {
  391. if ($data['video_duration'] < 4) $data['video_duration'] = 4;
  392. if (!isset($data['ratio'])) $data['ratio'] = 'adaptive';
  393. }
  394. // 验证 content 数组内容
  395. $this->validateSeedanceContent($data['content']);
  396. $task = $this->aiVideoGenerationService->createSeedanceTask($data);
  397. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  398. }
  399. /**
  400. * 上传素材到快快AI素材库(单个或批量)
  401. * 参数:url 或 urls 二选一;type:image/video/audio(默认image);purpose:素材用途;wait:是否等待素材就绪(默认1)
  402. *
  403. * @param Request $request
  404. * @return JsonResponse
  405. */
  406. public function kuaikuaiAssetUpload(Request $request): JsonResponse
  407. {
  408. $data = $request->all();
  409. $url = getProp($data, 'url');
  410. $urls = getProp($data, 'urls', []);
  411. if (is_string($urls)) {
  412. $urls = json_decode($urls, true) ?: [];
  413. }
  414. if (!is_array($urls)) {
  415. Utils::throwError('20003:urls格式不正确');
  416. }
  417. $type = strtolower(getProp($data, 'type', 'image'));
  418. if (!in_array($type, ['image', 'video', 'audio'], true)) {
  419. Utils::throwError('1002:素材类型只能是image/video/audio');
  420. }
  421. $assetType = $type === 'video'
  422. ? \App\Models\MpAsset::TYPE_VIDEO
  423. : ($type === 'audio' ? \App\Models\MpAsset::TYPE_AUDIO : \App\Models\MpAsset::TYPE_IMAGE);
  424. $purpose = getProp($data, 'purpose', '');
  425. $wait = (int)getProp($data, 'wait', 1) === 1;
  426. if (!empty($urls)) {
  427. $result = $this->aiVideoGenerationService->createKuaikuaiAssetsBatch($urls, $assetType, $purpose, '素材批量上传');
  428. } elseif (!empty($url)) {
  429. $result = $this->aiVideoGenerationService->createKuaikuaiAsset($url, getProp($data, 'name', '素材上传'), $assetType, $purpose, $wait);
  430. } else {
  431. Utils::throwError('1002:url与urls必须二选一');
  432. }
  433. if ($result['code'] !== 0) {
  434. Utils::throwError('20003:' . $result['msg']);
  435. }
  436. return $this->success($result['data']);
  437. }
  438. /**
  439. * 查询快快AI素材是否可用于生成视频
  440. * 参数:asset_id
  441. *
  442. * @param Request $request
  443. * @return JsonResponse
  444. */
  445. public function kuaikuaiAssetStatus(Request $request): JsonResponse
  446. {
  447. $assetId = getProp($request->all(), 'asset_id');
  448. if (empty($assetId)) {
  449. Utils::throwError('1002:asset_id不能为空');
  450. }
  451. $result = $this->aiVideoGenerationService->queryKuaikuaiAssetUsable($assetId);
  452. if ($result['code'] !== 0) {
  453. Utils::throwError('20003:' . $result['msg']);
  454. }
  455. return $this->success($result['data']);
  456. }
  457. /**
  458. * 删除快快AI素材(同时清理本地记录)
  459. * 参数:asset_id
  460. *
  461. * @param Request $request
  462. * @return JsonResponse
  463. */
  464. public function kuaikuaiAssetDelete(Request $request): JsonResponse
  465. {
  466. $assetId = getProp($request->all(), 'asset_id');
  467. if (empty($assetId)) {
  468. Utils::throwError('1002:asset_id不能为空');
  469. }
  470. $result = $this->aiVideoGenerationService->deleteKuaikuaiAsset($assetId);
  471. if ($result['code'] !== 0) {
  472. Utils::throwError('20003:' . $result['msg']);
  473. }
  474. return $this->success($result['data']);
  475. }
  476. /**
  477. * 本地快快AI素材列表
  478. * 参数:status、keyword、page、page_size
  479. *
  480. * @param Request $request
  481. * @return JsonResponse
  482. */
  483. public function kuaikuaiAssetList(Request $request): JsonResponse
  484. {
  485. $data = $request->all();
  486. $query = DB::table('mp_assets')->where('source_type', 'kuaikuai');
  487. if (!empty($data['status'])) {
  488. $query->where('status', $data['status']);
  489. }
  490. if (!empty($data['keyword'])) {
  491. $query->where(function ($q) use ($data) {
  492. $q->where('asset_id', 'like', '%' . $data['keyword'] . '%')
  493. ->orWhere('name', 'like', '%' . $data['keyword'] . '%')
  494. ->orWhere('url', 'like', '%' . $data['keyword'] . '%');
  495. });
  496. }
  497. $list = $query->orderBy('id', 'desc')->paginate((int)getProp($data, 'page_size', 10));
  498. return $this->paginate($list);
  499. }
  500. public function seedanceCallback(Request $request) {
  501. $data = $request->all();
  502. $result = $this->aiVideoGenerationService->seedanceCallback($data);
  503. return $this->success(['success'=> $result ? 1 : 0]);
  504. }
  505. /**
  506. * 查询 Seedance 1.5 Pro 任务状态
  507. *
  508. * @param string $taskId
  509. * @return JsonResponse
  510. */
  511. public function getSeedanceTaskStatus(string $taskId): JsonResponse
  512. {
  513. try {
  514. $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
  515. if (!$task) {
  516. return response()->json([
  517. 'success' => false,
  518. 'message' => '任务不存在'
  519. ], 404);
  520. }
  521. $result = $this->aiVideoGenerationService->querySeedanceTaskStatus($task);
  522. return $this->success($result);
  523. } catch (\Exception $e) {
  524. Utils::throwError('1001:'.$e->getMessage());
  525. }
  526. }
  527. /**
  528. * 验证 Seedance content 内容
  529. *
  530. * @param array $content
  531. * @return void
  532. */
  533. private function validateSeedanceContent(array $content): void
  534. {
  535. $hasText = false;
  536. $hasFirstFrame = false;
  537. $hasLastFrame = false;
  538. $hasReferenceImage = false;
  539. $hasDraftTask = false;
  540. foreach ($content as $index => $item) {
  541. if (!isset($item['type'])) {
  542. Utils::throwError('1002:content[' . $index . '] 缺少 type 字段');
  543. }
  544. switch ($item['type']) {
  545. case 'text':
  546. if (!isset($item['text']) || empty($item['text'])) {
  547. Utils::throwError('1002:text 类型的 content 必须包含非空的 text 字段');
  548. }
  549. if (mb_strlen($item['text']) > 1000) {
  550. Utils::throwError('1002:文本提示词不能超过1000个字符');
  551. }
  552. $hasText = true;
  553. break;
  554. case 'image_url':
  555. if (!isset($item['image_url']['url']) || empty($item['image_url']['url'])) {
  556. Utils::throwError('1002:image_url 类型的 content 必须包含 image_url.url 字段');
  557. }
  558. $role = $item['role'] ?? null;
  559. if ($role === 'first_frame') {
  560. $hasFirstFrame = true;
  561. } elseif ($role === 'last_frame') {
  562. $hasLastFrame = true;
  563. } elseif ($role === 'reference_image') {
  564. $hasReferenceImage = true;
  565. } elseif (empty($role)) {
  566. // 默认为首帧
  567. $hasFirstFrame = true;
  568. }
  569. break;
  570. case 'draft_task':
  571. if (!isset($item['draft_task']['id']) || empty($item['draft_task']['id'])) {
  572. Utils::throwError('1002:draft_task 类型的 content 必须包含 draft_task.id 字段');
  573. }
  574. $hasDraftTask = true;
  575. break;
  576. default:
  577. Utils::throwError('1002:不支持的 content type: ' . $item['type']);
  578. }
  579. }
  580. // 验证组合规则
  581. if ($hasLastFrame && !$hasFirstFrame) {
  582. Utils::throwError('1002:使用尾帧时必须同时提供首帧');
  583. }
  584. if ($hasReferenceImage && ($hasFirstFrame || $hasLastFrame)) {
  585. Utils::throwError('1002:参考图模式不能与首帧/尾帧模式混用');
  586. }
  587. if ($hasDraftTask && ($hasText || $hasFirstFrame || $hasLastFrame || $hasReferenceImage)) {
  588. Utils::throwError('1002:使用样片任务时不能同时提供其他内容');
  589. }
  590. }
  591. /**
  592. * 创建可灵AI Omni视频生成任务
  593. *
  594. * @param Request $request
  595. * @return JsonResponse
  596. */
  597. public function createKelingTask(Request $request): JsonResponse
  598. {
  599. $data = $request->all();
  600. // 验证基本参数
  601. $baseRules = [
  602. 'model' => 'required|string|in:kling-video-o1,kling-v3-omni',
  603. 'prompt' => 'required|max:2500',
  604. 'mode' => 'nullable|in:pro,std',
  605. 'aspect_ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9',
  606. 'video_duration' => 'required|integer|min:3|max:15',
  607. ];
  608. $baseMessages = [
  609. 'model.required' => '模型参数不能为空',
  610. 'model.in' => '模型只支持 kling-video-o1、kling-v3-omni',
  611. 'prompt.required' => '提示词不能为空',
  612. 'prompt.max' => '提示词不能超过2500个字符',
  613. 'mode.in' => '模式只能是 pro 或 std',
  614. 'aspect_ratio.in' => '宽高比只能是 16:9、4:3、1:1、3:4、9:16、21:9',
  615. 'video_duration.required' => '请填写视频时长',
  616. 'video_duration.integer' => '视频时长必须是整数',
  617. 'video_duration.min' => '视频时长不能少于3秒',
  618. 'video_duration.max' => '视频时长不能超过15秒',
  619. ];
  620. // 可选参数验证
  621. $optionalRules = [
  622. 'multi_prompt' => 'nullable|array',
  623. 'multi_prompt.*' => 'string|max:2500',
  624. 'shot_type' => 'nullable|in:customize,single_shot,multi_shot',
  625. 'generate_audio' => 'nullable|boolean',
  626. 'webhook_url' => 'nullable|url',
  627. 'seed' => 'nullable|integer|min:-1|max:4294967295',
  628. 'video_resolution' => 'nullable|in:480P,720P,1080P',
  629. ];
  630. $optionalMessages = [
  631. 'multi_prompt.array' => '多段提示词必须是数组',
  632. 'multi_prompt.*.string' => '每段提示词必须是字符串',
  633. 'multi_prompt.*.max' => '每段提示词不能超过2500个字符',
  634. 'shot_type.in' => '镜头类型只能是 customize、single_shot、multi_shot',
  635. 'webhook_url.url' => '回调地址格式不正确',
  636. 'seed.integer' => '随机种子必须是整数',
  637. 'seed.min' => '随机种子不能小于-1',
  638. 'seed.max' => '随机种子不能超过4294967295',
  639. 'video_resolution.in' => '分辨率只能是 480P、720P 或 1080P',
  640. ];
  641. // 检查是否有首帧URL
  642. if (isset($data['first_frame_url'])) {
  643. $optionalRules['first_frame_url'] = 'url';
  644. $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确';
  645. }
  646. // 检查是否有尾帧URL
  647. if (isset($data['tail_frame_url'])) {
  648. $optionalRules['tail_frame_url'] = 'url';
  649. $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确';
  650. }
  651. // 检查是否有首帧文件
  652. if ($request->hasFile('first_frame_file')) {
  653. $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  654. $optionalMessages = array_merge($optionalMessages, [
  655. 'first_frame_file.image' => '首帧必须是图片文件',
  656. 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式',
  657. 'first_frame_file.max' => '首帧图片大小不能超过4.7MB',
  658. ]);
  659. }
  660. // 检查是否有尾帧文件
  661. if ($request->hasFile('tail_frame_file')) {
  662. $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800';
  663. $optionalMessages = array_merge($optionalMessages, [
  664. 'tail_frame_file.image' => '尾帧必须是图片文件',
  665. 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式',
  666. 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB',
  667. ]);
  668. }
  669. if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) {
  670. Utils::throwError('1002:提供尾帧URL时必须提供首帧URL');
  671. }
  672. if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) {
  673. Utils::throwError('1002:提供尾帧文件时必须提供首帧文件');
  674. }
  675. $validationRules = array_merge($baseRules, $optionalRules);
  676. $validationMessages = array_merge($baseMessages, $optionalMessages);
  677. // 处理图片相关参数
  678. $validator = Validator::make($data, $validationRules, $validationMessages);
  679. if ($validator->fails()) {
  680. Utils::throwError('1002:'.$validator->errors()->first());
  681. }
  682. $firstFrame = null;
  683. $tailFrame = null;
  684. $data['image_list'] = [];
  685. // 处理首帧图片(文件上传方式)
  686. if ($request->hasFile('first_frame_file')) {
  687. $firstFrameFile = $request->file('first_frame_file');
  688. $firstFrame = $this->processFrameFile($firstFrameFile, 'first');
  689. $data['first_frame_url'] = $firstFrame['url'];
  690. $data['image_list'][] = [
  691. 'image_url' => $data['first_frame_url'],
  692. 'type' => 'first_frame',
  693. ];
  694. }
  695. // 处理首帧图片(URL方式)
  696. elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) {
  697. $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true);
  698. if (!$firstFrame) {
  699. Utils::throwError('1003:首帧图片获取失败');
  700. }
  701. $data['image_list'][] = [
  702. 'image_url' => $data['first_frame_url'],
  703. 'type' => 'first_frame',
  704. ];
  705. }
  706. // 处理尾帧图片(文件上传方式)
  707. if ($request->hasFile('tail_frame_file')) {
  708. $tailFrameFile = $request->file('tail_frame_file');
  709. $tailFrame = $this->processFrameFile($tailFrameFile, 'tail');
  710. $data['tail_frame_url'] = $tailFrame['url'];
  711. $data['image_list'][] = [
  712. 'image_url' => $data['tail_frame_url'],
  713. 'type' => 'end_frame',
  714. ];
  715. }
  716. // 处理尾帧图片(URL方式)
  717. elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) {
  718. $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true);
  719. if (!$tailFrame) {
  720. Utils::throwError('1003:尾帧图片获取失败');
  721. }
  722. $data['image_list'][] = [
  723. 'image_url' => $data['tail_frame_url'],
  724. 'type' => 'end_frame',
  725. ];
  726. }
  727. $task = $this->aiVideoGenerationService->createKelingOmniTask($data);
  728. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  729. }
  730. /**
  731. * 查询可灵AI Omni任务状态
  732. *
  733. * @param string $taskId
  734. * @return JsonResponse
  735. */
  736. public function getKelingTaskStatus(string $taskId): JsonResponse
  737. {
  738. try {
  739. $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first();
  740. if (!$task) {
  741. return response()->json([
  742. 'success' => false,
  743. 'message' => '任务不存在'
  744. ], 404);
  745. }
  746. $result = $this->aiVideoGenerationService->queryKelingOmniTaskStatus($task);
  747. return $this->success($result);
  748. } catch (\Exception $e) {
  749. Utils::throwError('1001:'.$e->getMessage());
  750. }
  751. }
  752. /**
  753. * 获取视频任务列表
  754. *
  755. * @param Request $request
  756. * @return mixed
  757. */
  758. public function taskList(Request $request)
  759. {
  760. $data = $request->all();
  761. $result = $this->aiVideoGenerationService->getTaskList($data);
  762. return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
  763. }
  764. // ==================== 主体管理相关接口 ====================
  765. /**
  766. * 创建主体
  767. *
  768. * @param Request $request
  769. * @return JsonResponse
  770. */
  771. public function createElement(Request $request): JsonResponse
  772. {
  773. try {
  774. // 验证请求参数
  775. $validated = $request->validate([
  776. 'element_name' => 'required|string|max:20',
  777. 'element_description' => 'required|string|max:100',
  778. 'reference_type' => 'required|in:image_refer,video_refer',
  779. 'element_image_list' => 'required_if:reference_type,image_refer|array',
  780. 'element_image_list.frontal_image' => 'required_if:reference_type,image_refer|string',
  781. 'element_image_list.refer_images' => 'required_if:reference_type,image_refer|array|min:1|max:3',
  782. 'element_image_list.refer_images.*.image_url' => 'required|string',
  783. 'element_video_list' => 'required_if:reference_type,video_refer|array',
  784. 'element_video_list.refer_videos' => 'required_if:reference_type,video_refer|array|size:1',
  785. 'element_video_list.refer_videos.*.video_url' => 'required|string',
  786. 'element_voice_id' => 'nullable|string',
  787. 'tag_list' => 'nullable|array',
  788. 'tag_list.*.tag_id' => 'required|in:o_101,o_102,o_103,o_104,o_105,o_106,o_107,o_108',
  789. 'callback_url' => 'nullable|url',
  790. 'external_task_id' => 'nullable|string',
  791. ]);
  792. // 调用服务层创建主体
  793. $element = $this->aiVideoGenerationService->createElement($validated);
  794. return response()->json([
  795. 'code' => 0,
  796. 'message' => '主体创建任务已提交',
  797. 'request_id' => uniqid(),
  798. 'data' => [
  799. 'task_id' => $element->id,
  800. 'task_info' => [
  801. 'external_task_id' => $element->external_task_id,
  802. ],
  803. 'task_status' => $element->task_status,
  804. 'created_at' => $element->created_at->timestamp * 1000,
  805. 'updated_at' => $element->updated_at->timestamp * 1000,
  806. ]
  807. ]);
  808. } catch (\Illuminate\Validation\ValidationException $e) {
  809. return response()->json([
  810. 'code' => 400,
  811. 'message' => '参数验证失败',
  812. 'request_id' => uniqid(),
  813. 'errors' => $e->errors()
  814. ], 400);
  815. } catch (\Exception $e) {
  816. dLog('generate')->error('创建主体失败: ' . $e->getMessage());
  817. return response()->json([
  818. 'code' => 500,
  819. 'message' => '创建主体失败: ' . $e->getMessage(),
  820. 'request_id' => uniqid(),
  821. ], 500);
  822. }
  823. }
  824. /**
  825. * 查询单个主体状态
  826. *
  827. * @param string $taskId
  828. * @param Request $request
  829. * @return JsonResponse
  830. */
  831. public function getElementStatus(string $taskId, Request $request): JsonResponse
  832. {
  833. try {
  834. $externalTaskId = $request->get('external_task_id');
  835. // 先查询本地数据库
  836. $query = AIElement::where('id', $taskId);
  837. if ($externalTaskId) {
  838. $query->orWhere('external_task_id', $externalTaskId);
  839. }
  840. $localElement = $query->first();
  841. if (!$localElement) {
  842. return response()->json([
  843. 'code' => 404,
  844. 'message' => '主体任务不存在',
  845. 'request_id' => uniqid(),
  846. ], 404);
  847. }
  848. // 如果任务还在处理中,查询最新状态
  849. if ($localElement->task_status === AIElement::STATUS_PROCESSING) {
  850. $apiResult = $this->aiVideoGenerationService->queryElementStatus($taskId, $externalTaskId);
  851. if ($apiResult['success'] && isset($apiResult['data'])) {
  852. $localElement->refresh(); // 重新加载数据
  853. }
  854. }
  855. // 构建响应数据
  856. $responseData = [
  857. 'task_id' => $localElement->id,
  858. 'task_status' => $localElement->task_status,
  859. 'task_status_msg' => $localElement->task_status_msg,
  860. 'task_info' => [
  861. 'external_task_id' => $localElement->external_task_id,
  862. ],
  863. 'created_at' => $localElement->created_at->timestamp * 1000,
  864. 'updated_at' => $localElement->updated_at->timestamp * 1000,
  865. ];
  866. // 如果任务成功,添加主体结果信息
  867. if ($localElement->task_status === AIElement::STATUS_SUCCEED && $localElement->element_id) {
  868. $responseData['task_result'] = [
  869. 'elements' => [
  870. [
  871. 'element_id' => $localElement->element_id,
  872. 'element_name' => $localElement->element_name,
  873. 'element_description' => $localElement->element_description,
  874. 'reference_type' => $localElement->reference_type,
  875. 'element_image_list' => $localElement->element_image_list,
  876. 'element_video_list' => $localElement->element_video_list,
  877. 'element_voice_info' => $localElement->element_voice_info,
  878. 'tag_list' => $localElement->tag_list,
  879. 'owned_by' => $localElement->owned_by,
  880. 'status' => $localElement->status,
  881. ]
  882. ]
  883. ];
  884. $responseData['final_unit_deduction'] = $localElement->final_unit_deduction;
  885. }
  886. return response()->json([
  887. 'code' => 0,
  888. 'message' => 'success',
  889. 'request_id' => uniqid(),
  890. 'data' => $responseData
  891. ]);
  892. } catch (\Exception $e) {
  893. dLog('generate')->error('查询主体状态失败: ' . $e->getMessage());
  894. return response()->json([
  895. 'code' => 500,
  896. 'message' => '查询主体状态失败: ' . $e->getMessage(),
  897. 'request_id' => uniqid(),
  898. ], 500);
  899. }
  900. }
  901. /**
  902. * 查询所有主体列表(包含自定义和官方)
  903. *
  904. * @param Request $request
  905. * @return JsonResponse
  906. */
  907. public function getElementsList(Request $request): JsonResponse
  908. {
  909. try {
  910. $pageNum = (int)$request->get('pageNum', 1);
  911. $pageSize = (int)$request->get('pageSize', 30);
  912. $type = $request->get('type', 'all'); // all, custom, official
  913. // 限制分页参数范围
  914. $pageNum = max(1, min($pageNum, 1000));
  915. $pageSize = max(1, min($pageSize, 500));
  916. $customElements = [];
  917. $officialElements = [];
  918. // 查询自定义主体
  919. if (in_array($type, ['all', 'custom'])) {
  920. $customResult = $this->aiVideoGenerationService->queryCustomElementsList($pageNum, $pageSize);
  921. if ($customResult['success']) {
  922. $customElements = $customResult['data'];
  923. }
  924. }
  925. // 查询官方主体
  926. if (in_array($type, ['all', 'official'])) {
  927. $officialResult = $this->aiVideoGenerationService->queryOfficialElementsList($pageNum, $pageSize);
  928. if ($officialResult['success']) {
  929. $officialElements = $officialResult['data'];
  930. }
  931. }
  932. // 合并结果
  933. $allElements = array_merge($customElements, $officialElements);
  934. return response()->json([
  935. 'code' => 0,
  936. 'message' => 'success',
  937. 'request_id' => uniqid(),
  938. 'data' => $allElements
  939. ]);
  940. } catch (\Exception $e) {
  941. dLog('generate')->error('查询主体列表失败: ' . $e->getMessage());
  942. return response()->json([
  943. 'code' => 500,
  944. 'message' => '查询主体列表失败: ' . $e->getMessage(),
  945. 'request_id' => uniqid(),
  946. ], 500);
  947. }
  948. }
  949. /**
  950. * 删除自定义主体
  951. *
  952. * @param Request $request
  953. * @return JsonResponse
  954. */
  955. public function deleteElement(Request $request): JsonResponse
  956. {
  957. try {
  958. // 验证请求参数
  959. $validated = $request->validate([
  960. 'element_id' => 'required|string',
  961. ]);
  962. // 检查主体是否存在且为自定义主体
  963. $element = AIElement::where('element_id', $validated['element_id'])->first();
  964. if (!$element) {
  965. return response()->json([
  966. 'code' => 404,
  967. 'message' => '主体不存在',
  968. 'request_id' => uniqid(),
  969. ], 404);
  970. }
  971. if ($element->owned_by === 'kling') {
  972. return response()->json([
  973. 'code' => 400,
  974. 'message' => '不能删除官方主体',
  975. 'request_id' => uniqid(),
  976. ], 400);
  977. }
  978. // 调用服务层删除主体
  979. $result = $this->aiVideoGenerationService->deleteElement($validated['element_id']);
  980. if ($result['success']) {
  981. return response()->json([
  982. 'code' => 0,
  983. 'message' => $result['message'],
  984. 'request_id' => uniqid(),
  985. 'data' => $result['data']
  986. ]);
  987. } else {
  988. return response()->json([
  989. 'code' => 500,
  990. 'message' => $result['message'],
  991. 'request_id' => uniqid(),
  992. ], 500);
  993. }
  994. } catch (\Illuminate\Validation\ValidationException $e) {
  995. return response()->json([
  996. 'code' => 400,
  997. 'message' => '参数验证失败',
  998. 'request_id' => uniqid(),
  999. 'errors' => $e->errors()
  1000. ], 400);
  1001. } catch (\Exception $e) {
  1002. dLog('generate')->error('删除主体失败: ' . $e->getMessage());
  1003. return response()->json([
  1004. 'code' => 500,
  1005. 'message' => '删除主体失败: ' . $e->getMessage(),
  1006. 'request_id' => uniqid(),
  1007. ], 500);
  1008. }
  1009. }
  1010. /**
  1011. * 获取本地主体列表(用于内部管理)
  1012. *
  1013. * @param Request $request
  1014. * @return JsonResponse
  1015. */
  1016. public function getLocalElementsList(Request $request): JsonResponse
  1017. {
  1018. try {
  1019. // 构建筛选条件
  1020. $filters = [
  1021. 'owned_by' => $request->get('owned_by'),
  1022. 'reference_type' => $request->get('reference_type'),
  1023. 'status' => $request->get('status'),
  1024. 'task_status' => $request->get('task_status'),
  1025. 'keyword' => $request->get('keyword'),
  1026. 'page_size' => (int)$request->get('page_size', 20),
  1027. ];
  1028. // 移除空值
  1029. $filters = array_filter($filters, function($value) {
  1030. return $value !== null && $value !== '';
  1031. });
  1032. // 调用服务层获取列表
  1033. $elements = $this->aiVideoGenerationService->getLocalElementsList($filters);
  1034. // 转换数据格式
  1035. $data = $elements->map(function ($element) {
  1036. return [
  1037. 'task_id' => $element->id,
  1038. 'external_task_id' => $element->external_task_id,
  1039. 'task_status' => $element->task_status,
  1040. 'task_status_msg' => $element->task_status_msg,
  1041. 'element_id' => $element->element_id,
  1042. 'element_name' => $element->element_name,
  1043. 'element_description' => $element->element_description,
  1044. 'reference_type' => $element->reference_type,
  1045. 'element_image_list' => $element->element_image_list,
  1046. 'element_video_list' => $element->element_video_list,
  1047. 'element_voice_id' => $element->element_voice_id,
  1048. 'element_voice_info' => $element->element_voice_info,
  1049. 'tag_list' => $element->tag_list,
  1050. 'tag_names' => $element->tag_names, // 使用模型的访问器
  1051. 'owned_by' => $element->owned_by,
  1052. 'status' => $element->status,
  1053. 'final_unit_deduction' => $element->final_unit_deduction,
  1054. 'created_at' => $element->created_at->format('Y-m-d H:i:s'),
  1055. 'updated_at' => $element->updated_at->format('Y-m-d H:i:s'),
  1056. ];
  1057. });
  1058. return response()->json([
  1059. 'code' => 0,
  1060. 'message' => 'success',
  1061. 'request_id' => uniqid(),
  1062. 'data' => [
  1063. 'data' => $data,
  1064. 'current_page' => $elements->currentPage(),
  1065. 'last_page' => $elements->lastPage(),
  1066. 'per_page' => $elements->perPage(),
  1067. 'total' => $elements->total(),
  1068. ]
  1069. ]);
  1070. } catch (\Exception $e) {
  1071. dLog('generate')->error('获取本地主体列表失败: ' . $e->getMessage());
  1072. return response()->json([
  1073. 'code' => 500,
  1074. 'message' => '获取本地主体列表失败: ' . $e->getMessage(),
  1075. 'request_id' => uniqid(),
  1076. ], 500);
  1077. }
  1078. }
  1079. }