|
|
@@ -12,6 +12,7 @@ use think\Log;
|
|
|
use think\Queue;
|
|
|
use think\queue\job\Redis;
|
|
|
use think\Request;
|
|
|
+use app\api\controller\Common;
|
|
|
|
|
|
class WorkOrder extends Api{
|
|
|
protected $noNeedLogin = ['*'];
|
|
|
@@ -34,21 +35,6 @@ class WorkOrder extends Api{
|
|
|
* task_id:查询获取任务图片
|
|
|
*/
|
|
|
public function GetImageStatus(){
|
|
|
-// //测试模拟返回前端图片数据
|
|
|
-// $taskInfo = [
|
|
|
-// 'status' => 'completed',
|
|
|
-// 'image' => '/uploads/Product/img2img-20260317152818-69b902924548d.png',
|
|
|
-// 'image_url' => '/uploads/Product/img2img-20260317152818-69b902924548d.png',
|
|
|
-// 'completed_at' => date('Y-m-d H:i:s')
|
|
|
-// ];
|
|
|
-// $taskInfo['image'] = Common::ossFullUrl((string)$taskInfo['image']);
|
|
|
-// $taskInfo['image_url'] = Common::ossFullUrl((string)$taskInfo['image_url']);
|
|
|
-// $res = [
|
|
|
-// 'code' => 0,
|
|
|
-// 'msg' => '查询成功',
|
|
|
-// 'data' => $taskInfo
|
|
|
-// ];
|
|
|
-// return json($res);die;
|
|
|
|
|
|
$params = $this->request->param();
|
|
|
$taskId = $params['task_id'];
|
|
|
@@ -108,9 +94,7 @@ class WorkOrder extends Api{
|
|
|
try {
|
|
|
// 1. 获取并校验入参
|
|
|
$params = $this->request->param();
|
|
|
-// echo "<pre>";
|
|
|
-// print_r($params);
|
|
|
-// echo "<pre>";die;
|
|
|
+
|
|
|
$statusVal = $this->validateAndGetStatusVal($params);
|
|
|
|
|
|
// 2. 映射并校验处理方法
|
|
|
@@ -222,9 +206,7 @@ class WorkOrder extends Api{
|
|
|
*/
|
|
|
private function handleAiTextToText(array $params): \think\response\Json
|
|
|
{
|
|
|
-// echo "<pre>";
|
|
|
-// print_r($params);
|
|
|
-// echo "<pre>";die;
|
|
|
+
|
|
|
//构造生成提示词
|
|
|
$promptTemplate = "\n请根据上述内容生成一段完整的话术,要求:\n"
|
|
|
. "1. 内容必须是连贯的一段话,不要使用列表、分隔线或其他结构化格式\n"
|
|
|
@@ -279,73 +261,57 @@ class WorkOrder extends Api{
|
|
|
|
|
|
/**
|
|
|
* 即梦AI--创建视频任务接口
|
|
|
- * 首帧图 + 尾帧图 = 新效果视频
|
|
|
+ * 支持:单张首帧图 / 首尾双帧图
|
|
|
+ * 图片入参:form-data 文件(first_image/last_image)、base64、或 http(s) URL
|
|
|
*/
|
|
|
public function Create_ImgToVideo()
|
|
|
{
|
|
|
- $apiUrl = '';
|
|
|
- $apiKey = '';
|
|
|
+ $apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
|
|
|
+ $apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
|
|
|
|
|
|
$params = $this->request->param();
|
|
|
- $modelParams = [
|
|
|
- 'resolution' => $params['resolution'] ?? '720p', // 分辨率:480p, 720p, 1080p
|
|
|
- 'duration' => $params['duration'] ?? 5, // 视频时长(秒)
|
|
|
- 'camerafixed' => $params['camerafixed'] ?? false, // 相机固定
|
|
|
- 'watermark' => $params['watermark'] ?? true, // 水印
|
|
|
- 'aspect_ratio' => $params['aspect_ratio'] ?? '16:9' // 视频比例:16:9, 4:3, 1:1等
|
|
|
- ];
|
|
|
- // 构建提示词
|
|
|
- $prompt = $params['prompt'] ?? '';
|
|
|
- $prompt .= ' --resolution ' . $modelParams['resolution'];
|
|
|
- $prompt .= ' --duration ' . $modelParams['duration'];
|
|
|
- $prompt .= ' --camerafixed ' . ($modelParams['camerafixed'] ? 'true' : 'false');
|
|
|
- $prompt .= ' --watermark ' . ($modelParams['watermark'] ? 'true' : 'false');
|
|
|
- $prompt .= ' --aspect_ratio ' . $modelParams['aspect_ratio'];
|
|
|
-
|
|
|
- // 构建请求数据
|
|
|
+ $prompt = trim((string)($params['prompt'] ?? ''));
|
|
|
+ if ($prompt === '') {
|
|
|
+ return json(['code' => 0, 'msg' => 'prompt 不能为空']);
|
|
|
+ }
|
|
|
+
|
|
|
+ $firstError = '';
|
|
|
+ $firstFrame = $this->resolveFrameImagePayload($params, 'first', $firstError);
|
|
|
+ if (($firstFrame['api_url'] ?? '') === '') {
|
|
|
+ $hint = Common::isOssEnabled()
|
|
|
+ ? '请用 form-data 上传 first_image(类型选文件),并查看 runtime/log'
|
|
|
+ : '请在 application/config.php 配置 oss(accessKeyId、endpoint、bucket、host)';
|
|
|
+ $detail = $firstError !== '' ? '(' . $firstError . ')' : '';
|
|
|
+ return json(['code' => 0, 'msg' => '首帧图片无效或上传 OSS 失败。' . $hint . $detail]);
|
|
|
+ }
|
|
|
+
|
|
|
+ $lastFrame = $this->resolveFrameImagePayload($params, 'last');
|
|
|
+ $firstImageUrl = $firstFrame['api_url'];
|
|
|
+ $lastImageUrl = $lastFrame['api_url'];
|
|
|
+ $content = $this->buildImgToVideoContent($prompt, $firstImageUrl, $lastImageUrl);
|
|
|
+
|
|
|
$data = [
|
|
|
- 'model' => 'doubao-seedance-1-0-pro-250528',//模型
|
|
|
- 'content' => [
|
|
|
- [
|
|
|
- 'type' => 'text',
|
|
|
- 'text' => $prompt
|
|
|
- ],
|
|
|
- [
|
|
|
- 'type' => 'image_url',
|
|
|
- 'image_url' => [
|
|
|
- 'url' => $params['first_image_url']// 首帧图片URL(参数)
|
|
|
- ],
|
|
|
- 'role' => 'first_image'
|
|
|
- ],
|
|
|
- [
|
|
|
- 'type' => 'image_url',
|
|
|
- 'image_url' => [
|
|
|
- 'url' => $params['last_image_url'] // 尾帧图片URL(参数)
|
|
|
- ],
|
|
|
- 'role' => 'last_image'
|
|
|
- ]
|
|
|
- ]
|
|
|
+ 'model' => 'doubao-seedance-1-5-pro-251215',
|
|
|
+ 'content' => $content,
|
|
|
+ 'generate_audio' => filter_var($params['generate_audio'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
|
|
+ 'ratio' => $params['ratio'] ?? $params['aspect_ratio'] ?? 'adaptive',
|
|
|
+ 'duration' => (int)($params['duration'] ?? 5),
|
|
|
+ 'watermark' => filter_var($params['watermark'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
|
|
];
|
|
|
- // 转换为 JSON 字符串
|
|
|
- $jsonData = json_encode($data);
|
|
|
|
|
|
- // 初始化 cURL
|
|
|
$ch = curl_init();
|
|
|
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
|
- curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
|
|
|
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
|
'Content-Type: application/json',
|
|
|
'Authorization: Bearer ' . $apiKey
|
|
|
]);
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 开发环境临时关闭SSL验证
|
|
|
- curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 超时时间
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
+ curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
|
|
|
|
|
- // 执行请求
|
|
|
$response = curl_exec($ch);
|
|
|
-
|
|
|
- // 检查 cURL 错误
|
|
|
if (curl_errno($ch)) {
|
|
|
$error = curl_error($ch);
|
|
|
curl_close($ch);
|
|
|
@@ -353,27 +319,55 @@ class WorkOrder extends Api{
|
|
|
}
|
|
|
curl_close($ch);
|
|
|
|
|
|
- // 解析响应
|
|
|
$responseData = json_decode($response, true);
|
|
|
-
|
|
|
- // 检查 API 错误
|
|
|
if (isset($responseData['error'])) {
|
|
|
- return json(['code' => 0, 'msg' => 'API 错误: ' . $responseData['error']['message']]);
|
|
|
+ $msg = $responseData['error']['message'] ?? 'API 请求失败';
|
|
|
+ return json(['code' => 0, 'msg' => 'API 错误: ' . $msg]);
|
|
|
}
|
|
|
|
|
|
- // 获取任务 ID
|
|
|
$taskId = $responseData['id'] ?? '';
|
|
|
- if (empty($taskId)) {
|
|
|
+ if ($taskId === '') {
|
|
|
return json(['code' => 0, 'msg' => '获取任务 ID 失败']);
|
|
|
}
|
|
|
|
|
|
- // 返回结果
|
|
|
+ $firstImageUrl = $this->finalizeFrameImageToOss($firstFrame, $taskId);
|
|
|
+ $lastImageUrl = $this->finalizeFrameImageToOss($lastFrame, $taskId);
|
|
|
+
|
|
|
+ $videoData = [
|
|
|
+ 'video_id' => $taskId,
|
|
|
+ 'prompt' => $prompt,
|
|
|
+ 'first_image_url' => $firstImageUrl,
|
|
|
+ 'last_image_url' => $lastImageUrl,
|
|
|
+ 'model' => $data['model'],
|
|
|
+ 'seconds' => (string)$data['duration'],
|
|
|
+ 'size' => (string)$data['ratio'],
|
|
|
+ 'sys_rq' => date('Y-m-d H:i:s'),
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ Db::name('video')->insert($videoData);
|
|
|
+ } catch (Exception $e) {
|
|
|
+ return json([
|
|
|
+ 'code' => 0,
|
|
|
+ 'msg' => '任务已创建,但数据库保存失败',
|
|
|
+ 'data' => [
|
|
|
+ 'task_id' => $taskId,
|
|
|
+ 'error_message' => $e->getMessage(),
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
return json([
|
|
|
'code' => 1,
|
|
|
+ 'msg' => '任务创建成功',
|
|
|
'data' => [
|
|
|
'task_id' => $taskId,
|
|
|
+ 'video_id' => $taskId,
|
|
|
'status' => $responseData['status'] ?? '',
|
|
|
- 'created_at' => $responseData['created_at'] ?? ''
|
|
|
+ 'created_at' => $responseData['created_at'] ?? '',
|
|
|
+ 'mode' => $lastImageUrl !== '' ? 'first_last_frame' : 'single_frame',
|
|
|
+ 'first_image_url' => $firstImageUrl,
|
|
|
+ 'last_image_url' => $lastImageUrl,
|
|
|
]
|
|
|
]);
|
|
|
}
|
|
|
@@ -384,12 +378,12 @@ class WorkOrder extends Api{
|
|
|
*/
|
|
|
public function Get_ImgToVideo()
|
|
|
{
|
|
|
- $apiUrl = '';
|
|
|
- $apiKey = '';
|
|
|
+ $apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
|
|
|
+ $apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
|
|
|
|
|
|
$params = $this->request->param();
|
|
|
- $taskId = $params['task_id'] ?? '';
|
|
|
- if (empty($taskId)) {
|
|
|
+ $taskId = $params['task_id'] ?? $params['video_id'] ?? '';
|
|
|
+ if ($taskId === '') {
|
|
|
return json(['code' => 0, 'msg' => '任务 ID 不能为空']);
|
|
|
}
|
|
|
|
|
|
@@ -420,9 +414,10 @@ class WorkOrder extends Api{
|
|
|
// print_r($queryData);die;
|
|
|
|
|
|
// 轮询任务状态,直到完成
|
|
|
- $maxPolls = 30; // 最大轮询次数
|
|
|
+ $maxPolls = 30;
|
|
|
$pollCount = 0;
|
|
|
- $taskStatus = $queryData['status'] ?? '';
|
|
|
+ $pollData = is_array($queryData) ? $queryData : [];
|
|
|
+ $taskStatus = $pollData['status'] ?? '';
|
|
|
|
|
|
while (!in_array($taskStatus, ['completed', 'succeeded']) && $pollCount < $maxPolls) {
|
|
|
sleep(5); // 每5秒轮询一次
|
|
|
@@ -461,51 +456,96 @@ class WorkOrder extends Api{
|
|
|
return json(['code' => 0, 'msg' => '任务执行超时']);
|
|
|
}
|
|
|
|
|
|
- // 获取视频 URL
|
|
|
- $videoUrl = $pollData['content']['video_url'] ?? '';
|
|
|
- if (empty($videoUrl)) {
|
|
|
+ // 获取视频 URL(兼容不同响应结构)
|
|
|
+ $videoUrl = $pollData['content']['video_url']
|
|
|
+ ?? $pollData['output']['video_url']
|
|
|
+ ?? $pollData['video_url']
|
|
|
+ ?? '';
|
|
|
+ if ($videoUrl === '') {
|
|
|
return json(['code' => 0, 'msg' => '获取视频 URL 失败', 'data' => ['pollData' => $pollData]]);
|
|
|
}
|
|
|
|
|
|
- // 确保保存目录存在
|
|
|
- $saveDir = ROOT_PATH . 'public/uploads/ceshi/';
|
|
|
+ $fileName = $this->sanitizeTaskIdSegment($taskId) . '.mp4';
|
|
|
+ $saveDir = $this->buildTaskMediaLocalDir($taskId);
|
|
|
if (!is_dir($saveDir)) {
|
|
|
mkdir($saveDir, 0755, true);
|
|
|
}
|
|
|
|
|
|
- // 生成保存文件名
|
|
|
- $fileName = $taskId . '.mp4';
|
|
|
- $savePath = $saveDir . '/' . $fileName;
|
|
|
-
|
|
|
- // 下载视频
|
|
|
- // 设置超时时间为300秒(5分钟)
|
|
|
- $context = stream_context_create(['http' => ['timeout' => 300]]);
|
|
|
- $videoContent = file_get_contents($videoUrl, false, $context);
|
|
|
+ $savePath = $saveDir . $fileName;
|
|
|
+ $videoContent = $this->downloadRemoteFile($videoUrl);
|
|
|
if ($videoContent === false) {
|
|
|
return json(['code' => 0, 'msg' => '下载视频失败', 'data' => ['videoUrl' => $videoUrl]]);
|
|
|
}
|
|
|
|
|
|
- // 保存视频到文件
|
|
|
- $saveResult = file_put_contents($savePath, $videoContent);
|
|
|
- if ($saveResult === false) {
|
|
|
- return json(['code' => 0, 'msg' => '保存视频失败', 'data' => ['savePath' => $savePath, 'dirWritable' => is_writable($saveDir)]]);
|
|
|
+ if (file_put_contents($savePath, $videoContent) === false) {
|
|
|
+ return json(['code' => 0, 'msg' => '保存视频失败', 'data' => ['savePath' => $savePath]]);
|
|
|
}
|
|
|
|
|
|
- // 生成视频的访问 URL
|
|
|
- $videoAccessUrl = '/uploads/ceshi/' . $fileName;
|
|
|
+ $objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
|
|
|
+ $upload = $this->uploadToOSS($savePath, $objectKey);
|
|
|
+ if ($upload['success']) {
|
|
|
+ $webUrl = $upload['url'] !== '' ? $upload['url'] : Common::ossFullUrl($objectKey);
|
|
|
+ } else {
|
|
|
+ $webUrl = $objectKey;
|
|
|
+ Log::write('[Get_ImgToVideo] OSS上传失败,使用本地相对路径: ' . $objectKey, 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ Db::name('video')->where('video_id', $taskId)->update(['web_url' => $webUrl]);
|
|
|
+ } catch (Exception $e) {
|
|
|
+ return json([
|
|
|
+ 'code' => 0,
|
|
|
+ 'msg' => '视频已生成,但数据库更新失败',
|
|
|
+ 'data' => [
|
|
|
+ 'task_id' => $taskId,
|
|
|
+ 'web_url' => $webUrl,
|
|
|
+ 'error_message' => $e->getMessage(),
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
|
|
|
- // 返回结果
|
|
|
return json([
|
|
|
'code' => 1,
|
|
|
+ 'msg' => '视频获取成功',
|
|
|
'data' => [
|
|
|
'task_id' => $taskId,
|
|
|
+ 'video_id' => $taskId,
|
|
|
'status' => $taskStatus,
|
|
|
- 'video_url' => $videoAccessUrl,
|
|
|
- 'save_path' => $savePath
|
|
|
- ]
|
|
|
+ 'web_url' => $webUrl,
|
|
|
+ 'oss_object_key' => $upload['object_key'] ?? $objectKey,
|
|
|
+ 'oss_uploaded' => $upload['success'] ?? false,
|
|
|
+ 'local_path' => $objectKey,
|
|
|
+ ],
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 下载远程文件(视频等)
|
|
|
+ * @return string|false
|
|
|
+ */
|
|
|
+ private function downloadRemoteFile(string $url)
|
|
|
+ {
|
|
|
+ $ch = curl_init();
|
|
|
+ curl_setopt($ch, CURLOPT_URL, $url);
|
|
|
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
|
|
+ curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
|
|
+ $body = curl_exec($ch);
|
|
|
+ if (curl_errno($ch)) {
|
|
|
+ Log::write('[downloadRemoteFile] ' . curl_error($ch) . ' | url=' . $url, 'error');
|
|
|
+ curl_close($ch);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
+ curl_close($ch);
|
|
|
+ if ($httpCode < 200 || $httpCode >= 300 || $body === false) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return $body;
|
|
|
+ }
|
|
|
+
|
|
|
//获取视频列表
|
|
|
public function Getvideolist(){
|
|
|
if (!$this->request->isGet()) {
|
|
|
@@ -539,13 +579,13 @@ class WorkOrder extends Api{
|
|
|
*/
|
|
|
//文生视频
|
|
|
public function video(){
|
|
|
- $apiUrl = '';
|
|
|
- $apiKey = '';
|
|
|
+ $apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
|
|
|
+ $apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
|
|
|
|
|
|
$params = $this->request->param();
|
|
|
$postData = [
|
|
|
'prompt' => $params['prompt'],
|
|
|
- 'model' => $params['model'],
|
|
|
+ 'model' => 'doubao-seedance-1-5-pro-251215',
|
|
|
'seconds' => $params['seconds'],
|
|
|
'size' => $params['size'],
|
|
|
];
|
|
|
@@ -636,9 +676,7 @@ class WorkOrder extends Api{
|
|
|
'size' => $postData['size'],
|
|
|
'sys_rq' => date("Y-m-d H:i:s")
|
|
|
];
|
|
|
- echo "<pre>";
|
|
|
- print_r($videoData);
|
|
|
- echo "<pre>";die;
|
|
|
+
|
|
|
// 尝试插入数据
|
|
|
try {
|
|
|
$res = Db::name('video')->insert($videoData);
|
|
|
@@ -838,9 +876,6 @@ class WorkOrder extends Api{
|
|
|
}
|
|
|
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
/**
|
|
|
* 查询模版
|
|
|
*/
|
|
|
@@ -905,56 +940,6 @@ class WorkOrder extends Api{
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 新增模版
|
|
|
- * (暂时无用)
|
|
|
- */
|
|
|
-// public function Add_Product_Template(){
|
|
|
-// try {
|
|
|
-// $params = $this->request->param();
|
|
|
-//
|
|
|
-// $chinese_description = $params['chinese_description'] ?? '';
|
|
|
-// $template_image_url = $params['template_image_url'] ?? '';
|
|
|
-// $template_name = $params['template_name'] ?? '';
|
|
|
-// $size = $params['size'] ?? '';
|
|
|
-// $style = $params['style'] ?? '';
|
|
|
-// $type = $params['type'] ?? '';
|
|
|
-// $user_id = $params['user_id'] ?? '';
|
|
|
-// $seconds = $params['seconds'] ?? '';
|
|
|
-// $video_id = $params['video_id'] ?? '';
|
|
|
-//
|
|
|
-// if (empty($template_name)) {
|
|
|
-// return json(['code' => 1, 'msg' => '模板名称不能为空']);
|
|
|
-// }
|
|
|
-//
|
|
|
-// $data = [
|
|
|
-// 'toexamine' => "未审核",
|
|
|
-// 'chinese_description' => $chinese_description,
|
|
|
-// 'template_image_url' => $template_image_url,
|
|
|
-// 'template_name' => $template_name,
|
|
|
-// 'type' => $type,
|
|
|
-// 'style' => $style,
|
|
|
-// 'seconds' => $seconds,
|
|
|
-// 'size' => $size,
|
|
|
-// 'video_id' => $video_id,
|
|
|
-// 'user_id' => $user_id,
|
|
|
-// 'sys_rq' => date('Y-m-d'),
|
|
|
-// 'create_time' => date('Y-m-d H:i:s')
|
|
|
-// ];
|
|
|
-// $result = Db::name('product_template')->insert($data);
|
|
|
-// if ($result) {
|
|
|
-// return json(['code' => 0, 'msg' => '模板保存成功']);
|
|
|
-// } else {
|
|
|
-// return json(['code' => 1, 'msg' => '模板保存失败: 数据库操作未影响任何行']);
|
|
|
-// }
|
|
|
-// } catch (\Exception $e) {
|
|
|
-// Log::record('模板保存异常: ' . $e->getMessage(), 'error');
|
|
|
-// Log::record('异常堆栈: ' . $e->getTraceAsString(), 'error');
|
|
|
-// return json(['code' => 1, 'msg' => '模板保存失败: ' . $e->getMessage()]);
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
-
|
|
|
|
|
|
/**
|
|
|
* 获取 AI 模型配置
|
|
|
@@ -1068,17 +1053,6 @@ class WorkOrder extends Api{
|
|
|
$where['sys_id'] = ['like', '%' . $params['sys_id'] . '%'];
|
|
|
}
|
|
|
|
|
|
-// $isSuperAdmin = false;
|
|
|
-// if (!empty($params['sys_id']) && $params['sys_id'] == '超级管理员') {
|
|
|
-// $isSuperAdmin = true;
|
|
|
-// }
|
|
|
-//
|
|
|
-// if (!$isSuperAdmin && !empty($params['sys_id'])) {
|
|
|
-// 普通
|
|
|
-// }else{
|
|
|
-//
|
|
|
-// }
|
|
|
-
|
|
|
$prompt = Db::name('product_image_generate')->field('prompt')->where($where)->group('prompt')->order('id desc')->select();
|
|
|
$data = Db::name('product_image_generate')->where($where)->order('id desc')->page($page, $pageSize)->select();
|
|
|
|
|
|
@@ -1305,49 +1279,12 @@ class WorkOrder extends Api{
|
|
|
|
|
|
$count = $redis->lLen($key_txttoimg) + $redis->lLen($key_txttotxt) + $redis->lLen($key_imgtotxt) + $redis->lLen($key_imgtoimg);
|
|
|
|
|
|
-// if ($count === 0) {
|
|
|
-// return json([
|
|
|
-// 'code' => 1,
|
|
|
-// 'msg' => '暂无队列需要停止'
|
|
|
-// ]);
|
|
|
-// }
|
|
|
return json([
|
|
|
'code' => 0,
|
|
|
'msg' => '成功停止队列任务'
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 开启队列任务
|
|
|
- * 暂时用不到、服务器已开启自动开启队列模式
|
|
|
- */
|
|
|
-// public function kaiStats()
|
|
|
-// {
|
|
|
-// // 判断是否已有监听进程在运行
|
|
|
-// $check = shell_exec("ps aux | grep 'queue:listen' | grep -v grep");
|
|
|
-// if ($check) {
|
|
|
-// return json([
|
|
|
-// 'code' => 1,
|
|
|
-// 'msg' => '监听进程已存在,请勿重复启动'
|
|
|
-// ]);
|
|
|
-// }
|
|
|
-// // 启动监听
|
|
|
-// $command = 'nohup php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /var/log/job_queue.log 2>&1 &';
|
|
|
-// exec($command, $output, $status);
|
|
|
-// if ($status === 0) {
|
|
|
-// return json([
|
|
|
-// 'code' => 0,
|
|
|
-// 'msg' => '队列监听已启动'
|
|
|
-// ]);
|
|
|
-// } else {
|
|
|
-// return json([
|
|
|
-// 'code' => 1,
|
|
|
-// 'msg' => '队列启动失败',
|
|
|
-// 'output' => $output
|
|
|
-// ]);
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
/**
|
|
|
*获取服务器URL地址和端口 IP地址:端口
|
|
|
* 用于获取图片路径拼接时
|
|
|
@@ -1367,4 +1304,496 @@ class WorkOrder extends Api{
|
|
|
];
|
|
|
return json($res);
|
|
|
}
|
|
|
-}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建图生视频 content:单帧无 role,双帧带 first_frame / last_frame
|
|
|
+ */
|
|
|
+ private function buildImgToVideoContent(string $prompt, string $firstImageUrl, string $lastImageUrl): array
|
|
|
+ {
|
|
|
+ $content = [
|
|
|
+ ['type' => 'text', 'text' => $prompt],
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($lastImageUrl !== '') {
|
|
|
+ $content[] = [
|
|
|
+ 'type' => 'image_url',
|
|
|
+ 'image_url' => ['url' => $firstImageUrl],
|
|
|
+ 'role' => 'first_frame',
|
|
|
+ ];
|
|
|
+ $content[] = [
|
|
|
+ 'type' => 'image_url',
|
|
|
+ 'image_url' => ['url' => $lastImageUrl],
|
|
|
+ 'role' => 'last_frame',
|
|
|
+ ];
|
|
|
+ return $content;
|
|
|
+ }
|
|
|
+
|
|
|
+ $content[] = [
|
|
|
+ 'type' => 'image_url',
|
|
|
+ 'image_url' => ['url' => $firstImageUrl],
|
|
|
+ ];
|
|
|
+ return $content;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 图生视频创建前,帧图暂存 OSS 子目录(AI 返回 id 后会迁移到正式目录) */
|
|
|
+ private const FRAME_OSS_STAGING_ID = '_staging';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 任务 id 用于路径的安全片段
|
|
|
+ */
|
|
|
+ private function sanitizeTaskIdSegment(string $taskId): string
|
|
|
+ {
|
|
|
+ $segment = preg_replace('/[\\\\\/:*?"<>|]/u', '_', trim($taskId));
|
|
|
+ return $segment !== '' ? $segment : '_unknown';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 图生视频任务媒体本地目录:public/uploads/videos/{日期}/{taskId}/
|
|
|
+ */
|
|
|
+ private function buildTaskMediaLocalDir(string $taskId): string
|
|
|
+ {
|
|
|
+ return str_replace('\\', '/', ROOT_PATH . 'public/uploads/videos/' . date('Ymd') . '/' . $this->sanitizeTaskIdSegment($taskId) . '/');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 图生视频任务媒体 OSS 对象键:uploads/videos/{日期}/{taskId}/{文件名}(帧图与生成视频同目录)
|
|
|
+ */
|
|
|
+ private function buildTaskMediaObjectKey(string $taskId, string $fileName): string
|
|
|
+ {
|
|
|
+ return 'uploads/videos/' . date('Ymd') . '/' . $this->sanitizeTaskIdSegment($taskId) . '/' . ltrim($fileName, '/');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析帧图片(含本地路径与暂存 OSS 信息,供创建任务后按 AI id 归档)
|
|
|
+ * @return array{type:string,api_url:string,local_path:string,staging_object_key:string}
|
|
|
+ */
|
|
|
+ private function resolveFrameImagePayload(array $params, string $role, string &$error = ''): array
|
|
|
+ {
|
|
|
+ $empty = ['type' => 'empty', 'api_url' => '', 'local_path' => '', 'staging_object_key' => ''];
|
|
|
+ $prefix = $role === 'last' ? 'last' : 'first';
|
|
|
+ $error = '';
|
|
|
+ $taskId = trim((string)($params['task_id'] ?? $params['video_id'] ?? ''));
|
|
|
+ $ossTaskId = $taskId !== '' ? $taskId : self::FRAME_OSS_STAGING_ID;
|
|
|
+
|
|
|
+ $uploadedFile = $this->request->file("{$prefix}_image");
|
|
|
+ if (!empty($uploadedFile)) {
|
|
|
+ $fileList = is_array($uploadedFile) ? $uploadedFile : [$uploadedFile];
|
|
|
+ foreach ($fileList as $file) {
|
|
|
+ if (!$file) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $fileError = '';
|
|
|
+ $result = $this->uploadFrameFileToOss($file, $prefix, $fileError, $ossTaskId);
|
|
|
+ if (($result['url'] ?? '') !== '') {
|
|
|
+ return [
|
|
|
+ 'type' => 'local',
|
|
|
+ 'api_url' => $result['url'],
|
|
|
+ 'local_path' => $result['local_path'] ?? '',
|
|
|
+ 'staging_object_key' => $result['object_key'] ?? '',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ if ($fileError !== '') {
|
|
|
+ $error = $fileError;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $base64Keys = ["{$prefix}_image", "{$prefix}_image_base64"];
|
|
|
+ foreach ($base64Keys as $key) {
|
|
|
+ if (empty($params[$key]) || !is_string($params[$key])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $raw = trim($params[$key]);
|
|
|
+ if ($raw === '' || strlen($raw) < 50) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $result = $this->uploadBase64ImageToOss($raw, $prefix, $ossTaskId);
|
|
|
+ if (($result['url'] ?? '') !== '') {
|
|
|
+ return [
|
|
|
+ 'type' => 'local',
|
|
|
+ 'api_url' => $result['url'],
|
|
|
+ 'local_path' => $result['local_path'] ?? '',
|
|
|
+ 'staging_object_key' => $result['object_key'] ?? '',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $urlKey = "{$prefix}_image_url";
|
|
|
+ if (!empty($params[$urlKey])) {
|
|
|
+ $url = trim((string)$params[$urlKey]);
|
|
|
+ if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) {
|
|
|
+ return ['type' => 'external', 'api_url' => $url, 'local_path' => '', 'staging_object_key' => ''];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * AI 返回任务 id 后,将本地帧图上传到 uploads/videos/{日期}/{taskId}/(与生成视频同目录)
|
|
|
+ */
|
|
|
+ private function finalizeFrameImageToOss(array $payload, string $taskId): string
|
|
|
+ {
|
|
|
+ if (($payload['type'] ?? '') === 'external') {
|
|
|
+ return (string)($payload['api_url'] ?? '');
|
|
|
+ }
|
|
|
+ if (($payload['type'] ?? '') !== 'local') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $localPath = (string)($payload['local_path'] ?? '');
|
|
|
+ if ($localPath === '' || !is_file($localPath)) {
|
|
|
+ return (string)($payload['api_url'] ?? '');
|
|
|
+ }
|
|
|
+
|
|
|
+ $objectKey = $this->buildTaskMediaObjectKey($taskId, basename($localPath));
|
|
|
+ $stagingKey = (string)($payload['staging_object_key'] ?? '');
|
|
|
+ if ($stagingKey !== '' && $stagingKey === $objectKey) {
|
|
|
+ return (string)($payload['api_url'] ?? '') ?: Common::ossFullUrl($objectKey);
|
|
|
+ }
|
|
|
+ $upload = $this->uploadToOSS($localPath, $objectKey);
|
|
|
+ if (!$upload['success']) {
|
|
|
+ Log::write('[finalizeFrameImageToOss] OSS上传失败: ' . $objectKey, 'error');
|
|
|
+ return (string)($payload['api_url'] ?? '');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($stagingKey !== '' && $stagingKey !== $objectKey) {
|
|
|
+ Common::deleteOssObject($stagingKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $upload['url'] !== '' ? $upload['url'] : Common::ossFullUrl($objectKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析帧图片:form-data 文件 > base64 > http(s) URL
|
|
|
+ */
|
|
|
+ private function resolveFrameImageUrl(array $params, string $role, string &$error = ''): string
|
|
|
+ {
|
|
|
+ $payload = $this->resolveFrameImagePayload($params, $role, $error);
|
|
|
+ return (string)($payload['api_url'] ?? '');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * form-data 上传的图片落盘并同步 OSS
|
|
|
+ * @return array{url:string,local_path:string,object_key:string}
|
|
|
+ */
|
|
|
+ private function uploadFrameFileToOss($file, string $roleLabel, string &$error = '', string $taskId = ''): array
|
|
|
+ {
|
|
|
+ $empty = ['url' => '', 'local_path' => '', 'object_key' => ''];
|
|
|
+ if ($taskId === '') {
|
|
|
+ $taskId = self::FRAME_OSS_STAGING_ID;
|
|
|
+ }
|
|
|
+ $error = '';
|
|
|
+ $ext = $this->resolveUploadedImageExt($file);
|
|
|
+ if ($ext === '') {
|
|
|
+ $error = '不支持的图片格式';
|
|
|
+ Log::write('[uploadFrameFileToOss] ' . $error, 'error');
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ $uploadInfo = $file->getInfo();
|
|
|
+ $tmpPath = isset($uploadInfo['tmp_name']) ? (string)$uploadInfo['tmp_name'] : '';
|
|
|
+ if ($tmpPath === '' || !is_file($tmpPath)) {
|
|
|
+ $tmpPath = (string)($file->getRealPath() ?: '');
|
|
|
+ }
|
|
|
+ if ($tmpPath === '' || !is_file($tmpPath)) {
|
|
|
+ $uploadErr = $uploadInfo['error'] ?? UPLOAD_ERR_NO_FILE;
|
|
|
+ $error = '未接收到上传文件(错误码' . $uploadErr . ')';
|
|
|
+ Log::write('[uploadFrameFileToOss] ' . $error, 'error');
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ $saveDir = $this->buildTaskMediaLocalDir($taskId);
|
|
|
+ if (!is_dir($saveDir)) {
|
|
|
+ mkdir($saveDir, 0755, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ $saveFileName = $roleLabel . '_' . str_replace('.', '', uniqid('', true)) . '.' . $ext;
|
|
|
+ $localFullPath = $saveDir . $saveFileName;
|
|
|
+ $saved = false;
|
|
|
+
|
|
|
+ if (method_exists($file, 'isValid') && $file->isValid()) {
|
|
|
+ $info = $file->move($saveDir, $saveFileName);
|
|
|
+ if ($info) {
|
|
|
+ $localFullPath = $saveDir . $info->getFilename();
|
|
|
+ $saved = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!$saved && !@copy($tmpPath, $localFullPath)) {
|
|
|
+ $moveErr = method_exists($file, 'getError') ? (string)$file->getError() : 'copy失败';
|
|
|
+ $error = '图片保存失败: ' . $moveErr;
|
|
|
+ Log::write('[uploadFrameFileToOss] ' . $error, 'error');
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ $objectKey = $this->buildTaskMediaObjectKey($taskId, basename($localFullPath));
|
|
|
+ $upload = $this->uploadToOSS($localFullPath, $objectKey);
|
|
|
+ if (!$upload['success']) {
|
|
|
+ $error = 'OSS上传失败';
|
|
|
+ Log::write('[uploadFrameFileToOss] ' . $error . ' | ' . $objectKey, 'error');
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ $url = $upload['url'] !== '' ? $upload['url'] : Common::ossFullUrl($objectKey);
|
|
|
+ return [
|
|
|
+ 'url' => $url,
|
|
|
+ 'local_path' => $localFullPath,
|
|
|
+ 'object_key' => $upload['object_key'] ?? $objectKey,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析上传图片扩展名
|
|
|
+ */
|
|
|
+ private function resolveUploadedImageExt($file): string
|
|
|
+ {
|
|
|
+ $name = $file->getInfo('name') ?? '';
|
|
|
+ $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
|
+ if ($ext === 'jpeg') {
|
|
|
+ $ext = 'jpg';
|
|
|
+ }
|
|
|
+ $allowed = ['jpg', 'png', 'gif', 'webp', 'bmp'];
|
|
|
+ if ($ext && in_array($ext, $allowed, true)) {
|
|
|
+ return $ext;
|
|
|
+ }
|
|
|
+ $mime = method_exists($file, 'getMime') ? strtolower((string)$file->getMime()) : '';
|
|
|
+ $map = [
|
|
|
+ 'image/jpeg' => 'jpg',
|
|
|
+ 'image/png' => 'png',
|
|
|
+ 'image/gif' => 'gif',
|
|
|
+ 'image/webp' => 'webp',
|
|
|
+ 'image/bmp' => 'bmp',
|
|
|
+ ];
|
|
|
+ return $map[$mime] ?? '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * base64 图片落盘并上传 OSS
|
|
|
+ * @return array{url:string,local_path:string,object_key:string}
|
|
|
+ */
|
|
|
+ private function uploadBase64ImageToOss(string $base64Input, string $roleLabel, string $taskId = ''): array
|
|
|
+ {
|
|
|
+ $empty = ['url' => '', 'local_path' => '', 'object_key' => ''];
|
|
|
+ if ($taskId === '') {
|
|
|
+ $taskId = self::FRAME_OSS_STAGING_ID;
|
|
|
+ }
|
|
|
+ $parsed = $this->parseBase64Image($base64Input);
|
|
|
+ if ($parsed === null) {
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ [$ext, $imageData] = $parsed;
|
|
|
+ $saveDir = $this->buildTaskMediaLocalDir($taskId);
|
|
|
+ if (!is_dir($saveDir)) {
|
|
|
+ mkdir($saveDir, 0755, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ $fileName = $roleLabel . '_' . str_replace('.', '', uniqid('', true)) . '.' . $ext;
|
|
|
+ $localFullPath = $saveDir . $fileName;
|
|
|
+ if (file_put_contents($localFullPath, $imageData) === false) {
|
|
|
+ Log::write('[uploadBase64ImageToOss] 本地保存失败: ' . $localFullPath, 'error');
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ $objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
|
|
|
+ $upload = $this->uploadToOSS($localFullPath, $objectKey);
|
|
|
+ if (!$upload['success']) {
|
|
|
+ Log::write('[uploadBase64ImageToOss] OSS上传失败: ' . $objectKey, 'error');
|
|
|
+ return $empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ $url = $upload['url'] !== '' ? $upload['url'] : Common::ossFullUrl($objectKey);
|
|
|
+ return [
|
|
|
+ 'url' => $url,
|
|
|
+ 'local_path' => $localFullPath,
|
|
|
+ 'object_key' => $upload['object_key'] ?? $objectKey,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 base64 图片(支持 data URI 与纯 base64)
|
|
|
+ * @return array{0:string,1:string}|null [扩展名, 二进制内容]
|
|
|
+ */
|
|
|
+ private function parseBase64Image(string $base64Input): ?array
|
|
|
+ {
|
|
|
+ $base64Input = trim($base64Input);
|
|
|
+ if ($base64Input === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (preg_match('/^data:image\/(png|jpe?g|webp);base64,(.+)$/is', $base64Input, $m)) {
|
|
|
+ $ext = strtolower($m[1]) === 'jpeg' ? 'jpg' : strtolower($m[1]);
|
|
|
+ $raw = preg_replace('/\s+/', '', $m[2]);
|
|
|
+ $imageData = base64_decode($raw, true);
|
|
|
+ } else {
|
|
|
+ $raw = preg_replace('/\s+/', '', $base64Input);
|
|
|
+ $imageData = base64_decode($raw, true);
|
|
|
+ $ext = $imageData !== false ? $this->detectImageExtension($imageData) : '';
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($imageData === false || strlen($imageData) < 100 || $ext === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [$ext, $imageData];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function detectImageExtension(string $imageData): string
|
|
|
+ {
|
|
|
+ if (strncmp($imageData, "\x89PNG\r\n\x1a\n", 8) === 0) {
|
|
|
+ return 'png';
|
|
|
+ }
|
|
|
+ if (strncmp($imageData, "\xFF\xD8\xFF", 3) === 0) {
|
|
|
+ return 'jpg';
|
|
|
+ }
|
|
|
+ if (strlen($imageData) >= 12 && substr($imageData, 0, 4) === 'RIFF' && substr($imageData, 8, 4) === 'WEBP') {
|
|
|
+ return 'webp';
|
|
|
+ }
|
|
|
+ return 'jpg';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将本地文件上传到阿里云 OSS
|
|
|
+ *
|
|
|
+ * @param string $file 本地完整路径,或 public 下相对路径(如 uploads/videos/20260604/xxx.mp4)
|
|
|
+ * @param string $objectKey OSS 对象键;传 uploads/ 开头或带扩展名则视为完整键,否则作为子目录标识(如 task_id)
|
|
|
+ * @return array{success:bool,object_key:string,url:string}
|
|
|
+ */
|
|
|
+ private function uploadToOSS(string $file, string $objectKey = ''): array
|
|
|
+ {
|
|
|
+ $localFullPath = $this->resolvePublicLocalPath($file);
|
|
|
+ if ($localFullPath === '') {
|
|
|
+ Log::write('[uploadToOSS] 本地文件不存在: ' . $file, 'error');
|
|
|
+ return ['success' => false, 'object_key' => '', 'url' => ''];
|
|
|
+ }
|
|
|
+
|
|
|
+ $fileName = basename($localFullPath);
|
|
|
+ if ($objectKey !== '' && (strpos($objectKey, 'uploads/') === 0 || preg_match('/\.[a-z0-9]{1,8}$/i', $objectKey))) {
|
|
|
+ $ossObjectKey = Common::normalizeOssObjectKey($objectKey);
|
|
|
+ } else {
|
|
|
+ $segments = ['uploads', 'videos', date('Ymd')];
|
|
|
+ if ($objectKey !== '') {
|
|
|
+ $segments[] = preg_replace('/[\\\\\/:*?"<>|]/u', '_', $objectKey);
|
|
|
+ }
|
|
|
+ $segments[] = $fileName;
|
|
|
+ $ossObjectKey = implode('/', $segments);
|
|
|
+ }
|
|
|
+
|
|
|
+ $success = $this->uploadLocalFileToAliyunOss($localFullPath, $ossObjectKey);
|
|
|
+ return [
|
|
|
+ 'success' => $success,
|
|
|
+ 'object_key' => $ossObjectKey,
|
|
|
+ 'url' => $success ? Common::ossFullUrl($ossObjectKey) : '',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传本地文件到阿里云 OSS(仅 WorkOrder 内实现,不修改 Common)
|
|
|
+ */
|
|
|
+ private function uploadLocalFileToAliyunOss(string $localFullPath, string $objectKey): bool
|
|
|
+ {
|
|
|
+ if (!Common::isOssEnabled() || !is_file($localFullPath)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $objectKey = Common::normalizeOssObjectKey($objectKey);
|
|
|
+ if ($objectKey === '') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (class_exists(\OSS\OssClient::class, true)) {
|
|
|
+ try {
|
|
|
+ $config = Common::getOssConfig();
|
|
|
+ $client = new \OSS\OssClient(
|
|
|
+ $config['accessKeyId'],
|
|
|
+ $config['accessKeySecret'],
|
|
|
+ $config['endpoint']
|
|
|
+ );
|
|
|
+ $client->uploadFile($config['bucket'], $objectKey, $localFullPath);
|
|
|
+ return true;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::write('[uploadLocalFileToAliyunOss SDK] ' . $e->getMessage() . ' | ' . $objectKey, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->putLocalFileToAliyunOssByCurl($localFullPath, $objectKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 无 OSS SDK 时通过 REST PUT 上传
|
|
|
+ */
|
|
|
+ private function putLocalFileToAliyunOssByCurl(string $localFullPath, string $objectKey): bool
|
|
|
+ {
|
|
|
+ $config = Common::getOssConfig();
|
|
|
+ $content = file_get_contents($localFullPath);
|
|
|
+ if ($content === false) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ $mime = function_exists('mime_content_type') ? (mime_content_type($localFullPath) ?: '') : '';
|
|
|
+ if ($mime === '') {
|
|
|
+ $ext = strtolower(pathinfo($localFullPath, PATHINFO_EXTENSION));
|
|
|
+ $mimeMap = [
|
|
|
+ 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
|
|
|
+ 'gif' => 'image/gif', 'webp' => 'image/webp', 'mp4' => 'video/mp4',
|
|
|
+ ];
|
|
|
+ $mime = $mimeMap[$ext] ?? 'application/octet-stream';
|
|
|
+ }
|
|
|
+
|
|
|
+ $date = gmdate('D, d M Y H:i:s \G\M\T');
|
|
|
+ $bucket = $config['bucket'];
|
|
|
+ $endpoint = ltrim((string)$config['endpoint'], 'https://');
|
|
|
+ $endpoint = ltrim($endpoint, 'http://');
|
|
|
+ $canonicalizedResource = '/' . $bucket . '/' . $objectKey;
|
|
|
+ $stringToSign = "PUT\n\n{$mime}\n{$date}\n{$canonicalizedResource}";
|
|
|
+ $signature = base64_encode(hash_hmac('sha1', $stringToSign, $config['accessKeySecret'], true));
|
|
|
+ $urlPath = implode('/', array_map('rawurlencode', explode('/', $objectKey)));
|
|
|
+ $url = 'https://' . $bucket . '.' . $endpoint . '/' . $urlPath;
|
|
|
+
|
|
|
+ $ch = curl_init($url);
|
|
|
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
|
|
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
|
|
|
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
|
+ 'Date: ' . $date,
|
|
|
+ 'Content-Type: ' . $mime,
|
|
|
+ 'Authorization: OSS ' . $config['accessKeyId'] . ':' . $signature,
|
|
|
+ ]);
|
|
|
+ curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
|
|
+ $response = curl_exec($ch);
|
|
|
+ $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
+ $curlError = curl_error($ch);
|
|
|
+ curl_close($ch);
|
|
|
+
|
|
|
+ if ($httpCode >= 200 && $httpCode < 300) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::write(
|
|
|
+ '[putLocalFileToAliyunOssByCurl] http=' . $httpCode
|
|
|
+ . ' err=' . $curlError
|
|
|
+ . ' resp=' . substr((string)$response, 0, 500)
|
|
|
+ . ' | objectKey=' . $objectKey,
|
|
|
+ 'error'
|
|
|
+ );
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析为 public 目录下的本地绝对路径
|
|
|
+ */
|
|
|
+ private function resolvePublicLocalPath(string $file): string
|
|
|
+ {
|
|
|
+ $file = str_replace('\\', '/', trim($file));
|
|
|
+ if ($file === '') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ if (is_file($file)) {
|
|
|
+ return $file;
|
|
|
+ }
|
|
|
+ $publicPath = str_replace('\\', '/', ROOT_PATH . 'public/' . ltrim($file, '/'));
|
|
|
+ return is_file($publicPath) ? $publicPath : '';
|
|
|
+ }
|
|
|
+}
|