This commit is contained in:
Your Name
2026-05-08 14:13:46 +08:00
27 changed files with 1660 additions and 74 deletions
@@ -15,6 +15,7 @@
namespace app\adminapi\controller;
use app\common\service\DirectUploadService;
use app\common\service\UploadService;
use Exception;
use think\response\Json;
@@ -77,4 +78,41 @@ class UploadController extends BaseAdminController
}
}
/**
* @notes 浏览器直传 OSS - 签发 STS 临时凭证
* @return Json
*/
public function ossCredentials()
{
$type = trim((string)$this->request->post('type', 'video'));
try {
$result = DirectUploadService::issueCredentials($type);
return $this->success('ok', $result);
} catch (Exception $e) {
return $this->fail($e->getMessage());
}
}
/**
* @notes 浏览器直传 OSS - 上传完成回执(写 file 表 + HEAD 校验)
* @return Json
*/
public function ossConfirm()
{
try {
$result = DirectUploadService::confirm([
'type' => trim((string)$this->request->post('type', 'video')),
'key' => trim((string)$this->request->post('key', '')),
'name' => trim((string)$this->request->post('name', '')),
'size' => (int)$this->request->post('size', 0),
'content_type' => trim((string)$this->request->post('content_type', '')),
'cid' => (int)$this->request->post('cid', 0),
'admin_id' => $this->adminId,
]);
return $this->success('上传成功', $result);
} catch (Exception $e) {
return $this->fail($e->getMessage());
}
}
}
@@ -501,6 +501,24 @@ class DiagnosisController extends BaseAdminController
return $this->fail(DiagnosisLogic::getError());
}
/**
* @notes 手动上传视频前,先创建一条模拟通话记录
* @return \think\response\Json
*/
public function createManualCallRecord()
{
$params = (new DiagnosisValidate())->post()->goCheck('trackingNotes');
$result = DiagnosisLogic::createManualCallRecord([
'diagnosis_id' => (int)$params['diagnosis_id'],
'admin_id' => (int)$this->adminId,
]);
if (!$result) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('创建成功', $result);
}
/**
* @notes 医助旁观诊单当前视频通话(TRTC 进房参数,仅拉流)
* @return \think\response\Json
@@ -0,0 +1,150 @@
<?php
namespace app\common\service;
use app\common\enum\FileEnum;
use app\common\model\file\File;
use app\common\service\storage\engine\Qcloud as QcloudEngine;
use Exception;
/**
* 浏览器直传 OSS 服务
* - 目前仅支持腾讯云 COSqcloud)
* - 其他 driver 返回 fallback=true,由前端降级到老链路
*
* Class DirectUploadService
* @package app\common\service
*/
class DirectUploadService
{
/** 视频允许的扩展名(沿用 config/project.file_video */
public const TYPE_VIDEO = 'video';
/** 默认凭证有效期 30 分钟 */
public const DEFAULT_DURATION = 1800;
/** 允许扩展类型 → 大小上限(字节) */
private const MAX_SIZE = [
self::TYPE_VIDEO => 2 * 1024 * 1024 * 1024, // 2GB
];
/**
* @notes 签发临时凭证
* @param string $type
* @return array
* @throws Exception
*/
public static function issueCredentials(string $type): array
{
if (!isset(self::MAX_SIZE[$type])) {
throw new Exception('不支持的上传类型: ' . $type);
}
$default = ConfigService::get('storage', 'default', 'local');
if ($default !== 'qcloud') {
// 其他 driver 不支持直传,前端降级
return ['provider' => $default, 'fallback' => true];
}
$storageConfig = ConfigService::get('storage', 'qcloud');
if (empty($storageConfig['bucket']) || empty($storageConfig['region'])
|| empty($storageConfig['access_key']) || empty($storageConfig['secret_key'])) {
throw new Exception('腾讯云 COS 配置不完整');
}
$keyPrefix = self::buildKeyPrefix($type);
$engine = new QcloudEngine($storageConfig);
$sts = $engine->getStsCredentials($keyPrefix, self::MAX_SIZE[$type], self::DEFAULT_DURATION);
return [
'provider' => 'qcloud',
'fallback' => false,
'bucket' => $sts['bucket'],
'region' => $sts['region'],
'host' => $sts['host'],
'cdn_domain' => rtrim((string)($storageConfig['domain'] ?? ''), '/'),
'key_prefix' => $keyPrefix,
'max_size' => self::MAX_SIZE[$type],
'duration' => self::DEFAULT_DURATION,
'expired_time' => $sts['expiredTime'],
'start_time' => $sts['startTime'],
'credentials' => $sts['credentials'],
];
}
/**
* @notes 直传完成后的回执:HEAD 校验 + 写 file 表
* @param array $params {key, type, name, size, content_type, cid, admin_id}
* @return array {id, cid, type, name, uri, url}
* @throws Exception
*/
public static function confirm(array $params): array
{
$type = (string)($params['type'] ?? '');
if (!isset(self::MAX_SIZE[$type])) {
throw new Exception('不支持的上传类型');
}
$default = ConfigService::get('storage', 'default', 'local');
if ($default !== 'qcloud') {
throw new Exception('当前存储驱动不支持直传回执');
}
$key = ltrim((string)($params['key'] ?? ''), '/');
$allowedPrefix = self::buildKeyPrefix($type);
if ($key === '' || strpos($key, $allowedPrefix) !== 0) {
throw new Exception('对象 Key 非法');
}
$storageConfig = ConfigService::get('storage', 'qcloud');
$engine = new QcloudEngine($storageConfig);
$head = $engine->headObject($key);
if ($head === false) {
throw new Exception('对象未找到,请确认上传是否完成');
}
if ($head['size'] <= 0 || $head['size'] > self::MAX_SIZE[$type]) {
throw new Exception('文件大小超出限制');
}
$name = trim((string)($params['name'] ?? ''));
if ($name === '') {
$name = basename($key);
}
if (strlen($name) > 128) {
$name = substr($name, 0, 123) . substr($name, -5);
}
$file = File::create([
'cid' => (int)($params['cid'] ?? 0),
'type' => self::resolveFileType($type),
'name' => $name,
'uri' => $key,
'source' => FileEnum::SOURCE_ADMIN,
'source_id' => (int)($params['admin_id'] ?? 0),
'create_time' => time(),
]);
$url = FileService::getFileUrl($key);
return [
'id' => $file['id'],
'cid' => $file['cid'],
'type' => $file['type'],
'name' => $file['name'],
'uri' => $url,
'url' => $url,
];
}
private static function buildKeyPrefix(string $type): string
{
return 'uploads/' . $type . '/' . date('Ymd') . '/';
}
private static function resolveFileType(string $type): int
{
return match ($type) {
self::TYPE_VIDEO => FileEnum::VIDEO_TYPE,
default => FileEnum::FILE_TYPE,
};
}
}
@@ -4,6 +4,7 @@ namespace app\common\service\storage\engine;
use Exception;
use Qcloud\Cos\Client;
use QCloud\COSSTS\Sts;
/**
* 腾讯云存储引擎 (COS)
@@ -113,4 +114,125 @@ class Qcloud extends Server
return $this->fileName;
}
/**
* @notes 获取 STS 临时凭证(用于浏览器直传)
* @param string $keyPrefix 资源前缀,如 uploads/video/20260508/
* @param int $maxSizeBytes 单文件大小上限(字节)
* @param int $durationSeconds 凭证有效期(秒)
* @return array {credentials, expiredTime, requestId}
* @throws Exception
*/
public function getStsCredentials(string $keyPrefix, int $maxSizeBytes, int $durationSeconds = 1800): array
{
$bucket = $this->config['bucket'];
// bucket 形如 likeadmin-1300000000appId 即末段
$appId = '';
if (strpos($bucket, '-') !== false) {
$parts = explode('-', $bucket);
$appId = end($parts);
}
if (!$appId || !ctype_digit($appId)) {
throw new Exception('腾讯云 COS bucket 名称不合法,无法解析 appId(期望形如 name-1300000000');
}
$shortBucket = substr($bucket, 0, strrpos($bucket, '-'));
$region = $this->config['region'];
$prefix = ltrim($keyPrefix, '/');
if ($prefix === '' || substr($prefix, -1) !== '/') {
$prefix = $prefix . '/';
}
$duration = max(900, min($durationSeconds, 7200));
// 自行构造 policy:对象级写动作收紧 + bucket 级 ListMultipartUploadscos-js-sdk-v5 续传探测必需)
$objectArn = sprintf('qcs::cos:%s:uid/%s:%s/%s*', $region, $appId, $bucket, $prefix);
$bucketArn = sprintf('qcs::cos:%s:uid/%s:%s/*', $region, $appId, $bucket);
$policy = [
'version' => '2.0',
'statement' => [
[
'effect' => 'allow',
'action' => [
'name/cos:PostObject',
'name/cos:PutObject',
'name/cos:InitiateMultipartUpload',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload',
'name/cos:AbortMultipartUpload',
'name/cos:ListParts',
],
'resource' => [$objectArn],
'condition' => [
'numeric_less_than_equal' => [
'cos:content-length' => $maxSizeBytes,
],
],
],
[
// ListMultipartUploads 是 bucket 级操作,无法用对象级 ARN 命中
'effect' => 'allow',
'action' => ['name/cos:ListMultipartUploads'],
'resource' => [$bucketArn],
],
],
];
$config = [
'url' => 'https://sts.tencentcloudapi.com/',
'domain' => 'sts.tencentcloudapi.com',
'proxy' => '',
'secretId' => $this->config['access_key'],
'secretKey' => $this->config['secret_key'],
'bucket' => $bucket,
'region' => $region,
'durationSeconds' => $duration,
'policy' => $policy,
];
$sts = new Sts();
$result = $sts->getTempKeys($config);
if (!is_array($result) || empty($result['credentials'])) {
throw new Exception('获取 STS 临时凭证失败');
}
return [
'credentials' => [
'tmpSecretId' => $result['credentials']['tmpSecretId'] ?? '',
'tmpSecretKey' => $result['credentials']['tmpSecretKey'] ?? '',
'sessionToken' => $result['credentials']['sessionToken'] ?? '',
],
'expiredTime' => (int)($result['expiredTime'] ?? (time() + $duration)),
'startTime' => (int)($result['startTime'] ?? time()),
'bucket' => $bucket,
'shortBucket' => $shortBucket,
'region' => $region,
'appId' => $appId,
'host' => sprintf('https://%s.cos.%s.myqcloud.com', $bucket, $region),
];
}
/**
* @notes HEAD 校验对象是否真实存在并返回 size / contentType
* @param string $key
* @return array|false
*/
public function headObject(string $key)
{
try {
$result = $this->cosClient->headObject([
'Bucket' => $this->config['bucket'],
'Key' => $key,
]);
return [
'size' => (int)($result['ContentLength'] ?? 0),
'content_type' => (string)($result['ContentType'] ?? ''),
'etag' => trim((string)($result['ETag'] ?? ''), '"'),
];
} catch (Exception $e) {
$this->error = $e->getMessage();
return false;
}
}
}
+1
View File
@@ -29,6 +29,7 @@
"phpoffice/phpspreadsheet": "^1.22",
"qiniu/php-sdk": "7.4",
"qcloud/cos-sdk-v5": "^2.5",
"qcloud_sts/qcloud-sts-sdk": "^3.0",
"aliyuncs/oss-sdk-php": "^2.4",
"alibabacloud/client": "^1.5",
"rmccue/requests": "^2.0",
+56 -4
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c8b18a5821cfd82f2e70cf67a7f40740",
"content-hash": "a5f0d3c9e4372fa66156ac28411cd86e",
"packages": [
{
"name": "adbario/php-dot-notation",
@@ -2541,6 +2541,58 @@
},
"time": "2023-08-16T02:26:48+00:00"
},
{
"name": "qcloud_sts/qcloud-sts-sdk",
"version": "3.0.12",
"source": {
"type": "git",
"url": "https://github.com/tencentyun/qcloud-cos-sts-php-sdk.git",
"reference": "16ebb03f1079ef71c272180fc015cc809ce8320b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tencentyun/qcloud-cos-sts-php-sdk/zipball/16ebb03f1079ef71c272180fc015cc809ce8320b",
"reference": "16ebb03f1079ef71c272180fc015cc809ce8320b",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"QCloud\\COSSTS\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "qcloudterminal",
"email": "qcloudterminal@gmail.com"
},
{
"name": "tuunalai",
"email": "550566181@qq.com"
}
],
"description": "PHP SDK for QCloud STS",
"homepage": "https://github.com/tencentyun/qcloud-cos-sts-sdk",
"keywords": [
"cos",
"php",
"qcloud",
"sts"
],
"support": {
"issues": "https://github.com/tencentyun/qcloud-cos-sts-php-sdk/issues",
"source": "https://github.com/tencentyun/qcloud-cos-sts-php-sdk/tree/3.0.12"
},
"time": "2023-12-20T06:42:36+00:00"
},
{
"name": "qiniu/php-sdk",
"version": "v7.4.0",
@@ -5132,12 +5184,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.0"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
+23 -4
View File
@@ -32,6 +32,11 @@ class InstalledVersions
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
@@ -309,6 +314,12 @@ class InstalledVersions
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
@@ -322,19 +333,27 @@ class InstalledVersions
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
@@ -350,7 +369,7 @@ class InstalledVersions
}
}
if (self::$installed !== array()) {
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
+3 -2
View File
@@ -9,14 +9,14 @@ return array(
'think\\view\\driver\\' => array($vendorDir . '/topthink/think-view/src'),
'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'),
'think\\app\\' => array($vendorDir . '/topthink/think-multi-app/src'),
'think\\' => array($vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/think-template/src'),
'think\\' => array($vendorDir . '/topthink/think-template/src', $vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/think-helper/src'),
'clagiordano\\weblibs\\configmanager\\' => array($vendorDir . '/clagiordano/weblibs-configmanager/src'),
'app\\' => array($baseDir . '/app'),
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'WpOrg\\Requests\\' => array($vendorDir . '/rmccue/requests/src'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'TheNorthMemory\\Xml\\' => array($vendorDir . '/thenorthmemory/xml/src'),
'TencentCloud\\' => array($vendorDir . '/tencentcloud/common/src/TencentCloud', $vendorDir . '/tencentcloud/sms/src/TencentCloud', $vendorDir . '/tencentcloud/trtc/src/TencentCloud'),
'TencentCloud\\' => array($vendorDir . '/tencentcloud/trtc/src/TencentCloud', $vendorDir . '/tencentcloud/sms/src/TencentCloud', $vendorDir . '/tencentcloud/common/src/TencentCloud'),
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
@@ -37,6 +37,7 @@ return array(
'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'),
'Qiniu\\' => array($vendorDir . '/qiniu/php-sdk/src/Qiniu'),
'Qcloud\\Cos\\' => array($vendorDir . '/qcloud/cos-sdk-v5/src'),
'QCloud\\COSSTS\\' => array($vendorDir . '/qcloud_sts/qcloud-sts-sdk/src'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
+10 -5
View File
@@ -87,6 +87,7 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
array (
'Qiniu\\' => 6,
'Qcloud\\Cos\\' => 11,
'QCloud\\COSSTS\\' => 14,
),
'P' =>
array (
@@ -162,10 +163,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
),
'think\\' =>
array (
0 => __DIR__ . '/..' . '/topthink/framework/src/think',
1 => __DIR__ . '/..' . '/topthink/think-helper/src',
0 => __DIR__ . '/..' . '/topthink/think-template/src',
1 => __DIR__ . '/..' . '/topthink/framework/src/think',
2 => __DIR__ . '/..' . '/topthink/think-orm/src',
3 => __DIR__ . '/..' . '/topthink/think-template/src',
3 => __DIR__ . '/..' . '/topthink/think-helper/src',
),
'clagiordano\\weblibs\\configmanager\\' =>
array (
@@ -193,9 +194,9 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
),
'TencentCloud\\' =>
array (
0 => __DIR__ . '/..' . '/tencentcloud/common/src/TencentCloud',
0 => __DIR__ . '/..' . '/tencentcloud/trtc/src/TencentCloud',
1 => __DIR__ . '/..' . '/tencentcloud/sms/src/TencentCloud',
2 => __DIR__ . '/..' . '/tencentcloud/trtc/src/TencentCloud',
2 => __DIR__ . '/..' . '/tencentcloud/common/src/TencentCloud',
),
'Symfony\\Polyfill\\Php81\\' =>
array (
@@ -277,6 +278,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
array (
0 => __DIR__ . '/..' . '/qcloud/cos-sdk-v5/src',
),
'QCloud\\COSSTS\\' =>
array (
0 => __DIR__ . '/..' . '/qcloud_sts/qcloud-sts-sdk/src',
),
'Psr\\SimpleCache\\' =>
array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
+55
View File
@@ -2637,6 +2637,61 @@
},
"install-path": "../qcloud/cos-sdk-v5"
},
{
"name": "qcloud_sts/qcloud-sts-sdk",
"version": "3.0.12",
"version_normalized": "3.0.12.0",
"source": {
"type": "git",
"url": "https://github.com/tencentyun/qcloud-cos-sts-php-sdk.git",
"reference": "16ebb03f1079ef71c272180fc015cc809ce8320b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tencentyun/qcloud-cos-sts-php-sdk/zipball/16ebb03f1079ef71c272180fc015cc809ce8320b",
"reference": "16ebb03f1079ef71c272180fc015cc809ce8320b",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=5.3.0"
},
"time": "2023-12-20T06:42:36+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"QCloud\\COSSTS\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "qcloudterminal",
"email": "qcloudterminal@gmail.com"
},
{
"name": "tuunalai",
"email": "550566181@qq.com"
}
],
"description": "PHP SDK for QCloud STS",
"homepage": "https://github.com/tencentyun/qcloud-cos-sts-sdk",
"keywords": [
"cos",
"php",
"qcloud",
"sts"
],
"support": {
"issues": "https://github.com/tencentyun/qcloud-cos-sts-php-sdk/issues",
"source": "https://github.com/tencentyun/qcloud-cos-sts-php-sdk/tree/3.0.12"
},
"install-path": "../qcloud_sts/qcloud-sts-sdk"
},
{
"name": "qiniu/php-sdk",
"version": "v7.4.0",
+15 -6
View File
@@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'topthink/think',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => NULL,
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '4dc9ec1878f6cdd83cfdc9044b93828d46eda715',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -376,6 +376,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'qcloud_sts/qcloud-sts-sdk' => array(
'pretty_version' => '3.0.12',
'version' => '3.0.12.0',
'reference' => '16ebb03f1079ef71c272180fc015cc809ce8320b',
'type' => 'library',
'install_path' => __DIR__ . '/../qcloud_sts/qcloud-sts-sdk',
'aliases' => array(),
'dev_requirement' => false,
),
'qiniu/php-sdk' => array(
'pretty_version' => 'v7.4.0',
'version' => '7.4.0.0',
@@ -632,9 +641,9 @@
'dev_requirement' => false,
),
'topthink/think' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => NULL,
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '4dc9ec1878f6cdd83cfdc9044b93828d46eda715',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
+3 -4
View File
@@ -4,10 +4,9 @@
$issues = array();
// Temporarily disabled for testing
// if (!(PHP_VERSION_ID >= 80100)) {
// $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
// }
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
@@ -0,0 +1,7 @@
composer.phar
/vendor/
.idea/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 腾讯云
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+110
View File
@@ -0,0 +1,110 @@
## 获取 SDK
- composer 安装
```
创建composer.json的文件,内容如下:
{
"require":{
"qcloud_sts/qcloud-sts-sdk": "3.0.*"
}
}
```
## 查看示例
请查看 [demo 示例](https://github.com/tencentyun/qcloud-cos-sts-sdk/tree/master/php/demo),里面描述了如何调用SDK。
## 接口说明
### getTempKeys
获取临时密钥接口
### 参数说明
|字段|类型|描述|
| ---- | ---- | ---- |
|secretId|string| 云 API 密钥 Id|
|secretKey|string| 云 API 密钥 key|
|durationSeconds|long| 要申请的临时密钥最长有效时间,单位秒,默认 1800,最大可设置 7200 |
|bucket|string| 存储桶名称:bucketName-appid, 如 test-125000000|
|region|string| 存储桶所属地域,如 ap-guangzhou|
|allowPrefix|string|资源的前缀,如授予操作所有资源,则为`*`;如授予操作某个路径a下的所有资源,则为 `a/*`,如授予只能操作特定的文件a/test.jpg, 则为`a/test.jpg`|
|allowActions|array| 授予 COS API 权限集合, 如简单上传操作:name/cos:PutObject|
|allowCiSource|boolean| 是否使用万象资源 true: 是,不传或false则无法使用万象资源|
|policy|array| 策略:由 allowActions、bucket、region、allowPrefix字段组成的描述授权的具体信息|
### 返回值说明
|字段|类型|描述|
| ---- | ---- | ---- |
|credentials | string | 临时密钥信息 |
|tmpSecretId | string | 临时密钥 Id,可用于计算签名 |
|tmpSecretKey | string | 临时密钥 Key,可用于计算签名 |
|sessionToken | string | 请求时需要用的 token 字符串,最终请求 COS API 时,需要放在 Header 的 x-cos-security-token 字段 |
|startTime | string | 密钥的起始时间,是 UNIX 时间戳 |
|expiredTime | string | 密钥的失效时间,是 UNIX 时间戳 |
### 返回数据示例
```
{
"expiredTime": 1589258683,
"expiration": "2020-05-12T04:44:43Z",
"credentials": {
"sessionToken": "Biypn6exa48PpMe7wFerEnNMpBKKPQo180c57e0a5275ebae506d7851a85f36a4P0TV5UFR3FYJjsoZA1tk6uRKoDRzc6-60BmwLqdS75OhjHEa7GlVYpL_ofKQJTpPKziKX7FnI10D_6qtLdjzf2NdsyUtQEd5kPpDCOQJZn9-BpleqWQe8oyH_2u7xi2f0FtjCYaoGIZ_lUqlILXQwr0B0t3hLfL4xNE-EmVjUlUXa16HxVCn4_hJetqo9LmI0AgLOjCbYx9aVrsV10eDsRta-TQSIXmJNP3aJ6oz8d8GBTgTE1I2qSFDnv9pjtQKW8HZWI_glPIfmHXCCwAESxEFL_owGz839Va0qYhF6LkfVmsuoU1zNcvJR1w3cIE6izV3SKHaOtWaew3IOervuOPoN3S2oYGNwv2EavtDAWyUBIeI7X6nMVzlpnyJ-3bkIhOq9QVIQAs8wh5A0u9mvKWugT5t6qgyEgvEZSj9k6p-JjwxMgLC6s5uK1i_nnf4fN7ZQ6I-JAfHnH4jEDiVtJgXqfuWPX_vnzskyR2Co6E",
"tmpSecretId": "AKIDTRPc-oe6c_avPSRwFVsPDyy3IoAr3szMajlOGuoEXY1232YLy6j4f-xZ5zL-NBMG",
"tmpSecretKey": "2v29SZztGYk6SGwHYm\/chJXdD3zPRFasmPoJiCmlR\/I="
},
"requestId": "69ef6295-b981-464d-9816-9c2ef92189d1",
"startTime": 1589256883
}
```
### getRoleCredential
申请扮演角色
### 参数说明
|字段|类型|描述|必选|
| ---- | ---- | ---- | ----|
|roleArn|string|角色的资源描述,可在 [访问管理](https://console.cloud.tencent.com/cam/role) 点击角色名获取。| 是 |
|secretId|string| 云 API 密钥 Id| 是 |
|secretKey|string| 云 API 密钥 key| 是 |
|endpoint|string| 接入点,内网填写"internal.tencentcloudapi.com",外网填写"tencentcloudapi.com"| 是 |
|durationSeconds|long| 要申请的临时密钥最长有效时间,单位秒,默认 1800,最大可设置 7200 | 否 |
|bucket|string| 存储桶名称:bucketName-appid, 如 test-125000000| 是 |
|region|string| 存储桶所属地域,如 ap-guangzhou| 是 |
|allowPrefix|string|资源的前缀,如授予操作所有资源,则为`*`;如授予操作某个路径a下的所有资源,则为 `a/*`,如授予只能操作特定的文件a/test.jpg, 则为`a/test.jpg`| 是 |
|allowActions|array| 授予 COS API 权限集合, 如简单上传操作:name/cos:PutObject| 是 |
|policy|array| 策略:由 allowActions、bucket、region、allowPrefix字段组成的描述授权的具体信息| 否 |
|externalId|string| 角色外部ID| 否 |
### 返回值说明
|字段|类型|描述|
| ---- | ---- | ---- |
|credentials | string | 临时密钥信息 |
|tmpSecretId | string | 临时密钥 Id,可用于计算签名 |
|tmpSecretKey | string | 临时密钥 Key,可用于计算签名 |
|sessionToken | string | 请求时需要用的 token 字符串,最终请求 COS API 时,需要放在 Header 的 x-cos-security-token 字段 |
|startTime | string | 密钥的起始时间,是 UNIX 时间戳 |
|expiredTime | string | 密钥的失效时间,是 UNIX 时间戳 |
### 返回数据示例
```
{
"Response": {
"Credentials": {
"Token": "da1e9d2ee9dda83506832d5ecb903b790132dfe340001",
"TmpSecretId": "AKID65zyIP0mpXtaI******WIQVMn1umNH58",
"TmpSecretKey": "q95K84wrzuEGoc*******52boxvp71yoh"
},
"ExpiredTime": 1543914376,
"Expiration": "2018-12-04T09:06:16Z",
"RequestId": "4daec797-9cd2-4f09-9e7a-7d4c43b2a74c"
}
}
```
+28
View File
@@ -0,0 +1,28 @@
{
"name": "qcloud_sts/qcloud-sts-sdk",
"description": "PHP SDK for QCloud STS",
"keywords": [
"qcloud", "sts", "cos", "php"
],
"homepage": "https://github.com/tencentyun/qcloud-cos-sts-sdk",
"license": "MIT",
"authors": [
{
"name": "qcloudterminal",
"email": "qcloudterminal@gmail.com"
},
{
"name": "tuunalai",
"email": "550566181@qq.com"
}
],
"autoload": {
"psr-4": {
"QCloud\\COSSTS\\": "src"
}
},
"require": {
"php": ">=5.3.0",
"ext-curl": "*"
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace QCloud\COSSTS;
class Scope{
var $action;
var $bucket;
var $region;
var $resourcePrefix;
var $effect = 'allow';
function __construct($action, $bucket, $region, $resourcePrefix){
$this->action = $action;
$this->bucket = $bucket;
$this->region = $region;
$this->resourcePrefix = $resourcePrefix;
}
function set_effect($isAllow){
if($isAllow){
$this->effect = 'allow';
}else{
$this->effect = 'deny';
}
}
function get_action(){
if($this->action == null){
throw new \Exception("action == null");
}
return $this->action;
}
function get_resource(){
if($this->bucket == null){
throw new \Exception("bucket == null");
}
if($this->region == null){
throw new \Exception("region == null");
}
if($this->resourcePrefix == null){
throw new \Exception("resourcePrefix == null");
}
$index = strripos($this->bucket, '-');
if($index < 0){
throw new Exception("bucket is invalid: " . $this->bucket);
}
$appid = substr($this->bucket, $index + 1);
if(!(strpos($this->resourcePrefix, '/') === 0)){
$this->resourcePrefix = '/' . $this->resourcePrefix;
}
return 'qcs::cos:' . $this->region . ':uid/' . $appid . ':' . $this->bucket . $this->resourcePrefix;
}
function get_effect(){
return $this->effect;
}
}
?>
+466
View File
@@ -0,0 +1,466 @@
<?php
namespace QCloud\COSSTS;
class Sts{
// 临时密钥计算样例
function _hex2bin($data) {
$len = strlen($data);
return pack("H" . $len, $data);
}
// obj 转 query string
function json2str($obj, $notEncode = false) {
ksort($obj);
$arr = array();
if(!is_array($obj)){
throw new \Exception('$obj must be an array, the actual value is:' . json_encode($obj));
}
foreach ($obj as $key => $val) {
array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val)));
}
return join('&', $arr);
}
// 计算临时密钥用的签名
function getSignature($opt, $key, $method, $config) {
$host = "sts.tencentcloudapi.com";
if (array_key_exists('domain', $config)) {
$host = $config['domain'];
}
if (array_key_exists('endpoint', $config)) {
$host = "sts." . $config['endpoint'];
}
$formatString = $method . $host . '/?' . $this->json2str($opt, 1);
$sign = hash_hmac('sha1', $formatString, $key);
$sign = base64_encode($this->_hex2bin($sign));
return $sign;
}
// v2接口的key首字母小写,v3改成大写,此处做了向下兼容
function backwardCompat($result) {
if(!is_array($result)){
throw new \Exception('$result must be an array, the actual value is:' . json_encode($result));
}
$compat = array();
foreach ($result as $key => $value) {
if(is_array($value)) {
$compat[lcfirst($key)] = $this->backwardCompat($value);
} elseif ($key == 'Token') {
$compat['sessionToken'] = $value;
} else {
$compat[lcfirst($key)] = $value;
}
}
return $compat;
}
// 获取临时密钥
function getTempKeys($config) {
$result = null;
try{
if(array_key_exists('policy', $config)){
$policy = $config['policy'];
}else{
if(array_key_exists('bucket', $config)){
$ShortBucketName = substr($config['bucket'],0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
}else{
throw new \Exception("bucket== null");
}
if(array_key_exists('allowPrefix', $config)){
$resource = array();
foreach($config['allowPrefix'] as &$val) {
if (!(strpos($val, '/') === 0)) {
$allow = '/' . $val;
}
$resource[] = 'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . '/' . $val;
}
// 处理万象资源
if(array_key_exists('allowCiSource', $config) && $config['allowCiSource'] === true) {
$resource[] = 'qcs::ci:' . $config['region'] . ':uid/' . $AppId . ':' . 'bucket/' . $config['bucket'] . '/*';
}
}else{
throw new \Exception("allowPrefix == null");
}
if(!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
if (!array_key_exists('condition', $config)) {
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> $resource
)
)
);
} else {
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> $resource,
'condition'=>$config['condition']
)
)
);
}
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'GetFederationToken';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
if(array_key_exists('durationSeconds', $config)){
if(!(is_integer($config['durationSeconds']))){
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId'=> $config['secretId'],
'Timestamp'=> $Timestamp,
'Nonce'=> $Nonce,
'Action'=> $Action,
'DurationSeconds'=> $config['durationSeconds'],
'Version'=>'2018-08-13',
'Name'=> 'cos',
'Region'=> $config['region'],
'Policy'=> urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.tencentcloudapi.com/';
if(array_key_exists('url', $config)) {
$url = $config['url'];
}
if(!array_key_exists('url', $config) && array_key_exists('domain', $config)) {
$url = 'https://sts.' . $config['domain'];
}
if(array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if(array_key_exists('proxy', $config)){
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if(curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if(isset($result['Error'])){
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
}catch(\Exception $e){
if($result == null){
$result = "error: " . $e->getMessage();
}else{
$result = json_encode($result);
}
throw new \Exception($result);
}
}
// 获取临时密钥-兼容万象资源 不在维护,请使用getTempKeys函数,$config的ci_source字段
function getTempKeys4Ci($config) {
$result = null;
try{
if(array_key_exists('policy', $config)){
$policy = $config['policy'];
}else{
if(array_key_exists('bucket', $config)){
$ShortBucketName = substr($config['bucket'],0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
}else{
throw new Exception("bucket== null");
}
$resource = array();
$resource[] = 'qcs::ci:' . $config['region'] . ':uid/' . $AppId . ':' . 'bucket/' . $config['bucket'] . '/*';
if(array_key_exists('allowPrefix', $config)){
foreach($config['allowPrefix'] as &$val) {
if (!(strpos($val, '/') === 0)) {
$allow = '/' . $val;
}
$resource[] = 'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . '/' . $val;
}
}else{
throw new \Exception("allowPrefix == null");
}
if(!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
if (!array_key_exists('condition', $config)) {
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> $resource
)
)
);
} else {
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> $resource,
'condition'=>$config['condition']
)
)
);
}
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'GetFederationToken';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
if(array_key_exists('durationSeconds', $config)){
if(!(is_integer($config['durationSeconds']))){
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId'=> $config['secretId'],
'Timestamp'=> $Timestamp,
'Nonce'=> $Nonce,
'Action'=> $Action,
'DurationSeconds'=> $config['durationSeconds'],
'Version'=>'2018-08-13',
'Name'=> 'cos',
'Region'=> $config['region'],
'Policy'=> urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.tencentcloudapi.com/';
if(array_key_exists('url', $config)) {
$url = $config['url'];
}
if(!array_key_exists('url', $config) && array_key_exists('domain', $config)) {
$url = 'https://sts.' . $config['domain'];
}
if(array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if(array_key_exists('proxy', $config)){
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if(curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if(isset($result['Error'])){
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
}catch(\Exception $e){
if($result == null){
$result = "error: " . $e->getMessage();
}else{
$result = json_encode($result);
}
throw new \Exception($result);
}
}
//申请角色授权
function getRoleCredential($config) {
$result = null;
try{
if(array_key_exists('policy', $config)){
$policy = $config['policy'];
}else{
if(array_key_exists('bucket', $config)){
$ShortBucketName = substr($config['bucket'],0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
}else{
throw new \Exception("bucket== null");
}
if(array_key_exists('allowPrefix', $config)){
$resource = array();
foreach($config['allowPrefix'] as &$val) {
if (!(strpos($val, '/') === 0)) {
$allow = '/' . $val;
}
$resource[] = 'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . '/' . $val;
}
}else{
throw new \Exception("allowPrefix == null");
}
if(!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
if (!array_key_exists('condition', $config)) {
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> $resource
)
)
);
} else {
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> $resource,
'condition'=>$config['condition']
)
)
);
}
}
if (array_key_exists('roleArn', $config)) {
$RoleArn = $config['roleArn'];
} else {
throw new \Exception("roleArn == null");
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'AssumeRole';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
$ExternalId = "";
if (array_key_exists('externalId', $config)) {
$ExternalId = $config['externalId'];
}
if(array_key_exists('durationSeconds', $config)){
if(!(is_integer($config['durationSeconds']))){
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId'=> $config['secretId'],
'Timestamp'=> $Timestamp,
'RoleArn'=> $RoleArn,
'Action'=> $Action,
'Nonce'=> $Nonce,
'DurationSeconds'=> $config['durationSeconds'],
'Version'=>'2018-08-13',
'RoleSessionName'=> 'cos',
'Region'=> $config['region'],
'ExternalId' => $ExternalId,
'Policy'=> urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.internal.tencentcloudapi.com/';
if(array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if(array_key_exists('proxy', $config)){
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if(curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if(isset($result['Error'])){
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
}catch(\Exception $e){
if($result == null){
$result = "error: " . $e->getMessage();
}else{
$result = json_encode($result);
}
throw new \Exception($result);
}
}
// get policy
function getPolicy($scopes){
if (!is_array($scopes)){
return null;
}
$statements = array();
for($i=0, $counts=count($scopes); $i < $counts; $i++){
$actions=array();
$resources = array();
array_push($actions, $scopes[$i]->get_action());
array_push($resources, $scopes[$i]->get_resource());
$statement = array(
'action' => $actions,
'effect' => $scopes[$i]->get_effect(),
'resource' => $resources
);
array_push($statements, $statement);
}
$policy = array(
'version' => '2.0',
'statement' => $statements
);
return $policy;
}
}
?>
+1 -1
View File
@@ -1,5 +1,5 @@
<?php
// This file is automatically generated at:2026-03-27 09:27:20
// This file is automatically generated at:2026-05-08 09:52:28
declare (strict_types = 1);
return array (
0 => 'think\\app\\Service',