Merge branch 'long-0507'

This commit is contained in:
2026-05-08 10:55:21 +08:00
26 changed files with 1644 additions and 72 deletions
+45
View File
@@ -19,6 +19,7 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.9", "axios": "^1.7.9",
"cos-js-sdk-v5": "^1.10.1",
"css-color-function": "^1.3.3", "css-color-function": "^1.3.3",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.4", "element-plus": "^2.9.4",
@@ -6064,6 +6065,16 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/cos-js-sdk-v5": {
"version": "1.10.1",
"resolved": "https://registry.npmmirror.com/cos-js-sdk-v5/-/cos-js-sdk-v5-1.10.1.tgz",
"integrity": "sha512-a4SRfCY5g6Z35C7OWe9te/S1zk77rVQzfpvZ33gmTdJQzKxbNbEG7Aw/v453XwVMsQB352FIf7KRMm5Ya/wlZQ==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"fast-xml-parser": "4.5.0"
}
},
"node_modules/crelt": { "node_modules/crelt": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -7580,6 +7591,28 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-xml-parser": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
"integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
},
{
"type": "paypal",
"url": "https://paypal.me/naturalintelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -12299,6 +12332,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/sucrase": { "node_modules/sucrase": {
"version": "3.35.1", "version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+1
View File
@@ -21,6 +21,7 @@
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.9", "axios": "^1.7.9",
"cos-js-sdk-v5": "^1.10.1",
"css-color-function": "^1.3.3", "css-color-function": "^1.3.3",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.4", "element-plus": "^2.9.4",
+47
View File
@@ -90,3 +90,50 @@ export function fileMove(params: Record<string, any>) {
export function fileRename(params: { id: number; name: string }) { export function fileRename(params: { id: number; name: string }) {
return request.post({ url: '/file/rename', params }) return request.post({ url: '/file/rename', params })
} }
/** 浏览器直传 OSS - 凭证类型 */
export interface OssCredentialsResponse {
provider: string
fallback: boolean
bucket?: string
region?: string
host?: string
cdn_domain?: string
key_prefix?: string
max_size?: number
duration?: number
expired_time?: number
start_time?: number
credentials?: {
tmpSecretId: string
tmpSecretKey: string
sessionToken: string
}
}
/** 申请 STS 临时凭证 */
export function getOssCredentials(params: { type: 'video' }) {
return request.post({
url: '/upload/ossCredentials',
params
}) as Promise<OssCredentialsResponse>
}
/** 直传完成回执:写 file 表 + HEAD 校验 */
export function confirmOssUpload(params: {
type: 'video'
key: string
name: string
size: number
content_type: string
cid?: number
}) {
return request.post({ url: '/upload/ossConfirm', params }) as Promise<{
id: number
cid: number
type: number
name: string
uri: string
url: string
}>
}
+13 -2
View File
@@ -229,14 +229,25 @@ export function startCloudRecording(params: { diagnosis_id: number }) {
return request.post({ url: '/tcm.diagnosis/startCloudRecording', params }) return request.post({ url: '/tcm.diagnosis/startCloudRecording', params })
} }
/** 本地录制完成后,将已上传视频的访问地址写入通话记录(忽略取消令牌,避免与其它 POST 互斥取消 */ /** 本地录制完成后,将已上传视频的访问地址写入通话记录(可显式传 call_record_id */
export function attachLocalCallRecording(params: { diagnosis_id: number; file_url: string }) { export function attachLocalCallRecording(params: {
diagnosis_id: number
file_url: string
call_record_id?: number
}) {
return request.post( return request.post(
{ url: '/tcm.diagnosis/attachLocalCallRecording', params }, { url: '/tcm.diagnosis/attachLocalCallRecording', params },
{ ignoreCancelToken: true, isOpenRetry: false } { ignoreCancelToken: true, isOpenRetry: false }
) )
} }
export function createManualCallRecord(params: { diagnosis_id: number }) {
return request.post(
{ url: '/tcm.diagnosis/createManualCallRecord', params },
{ ignoreCancelToken: true, isOpenRetry: false }
)
}
/** 医助旁观当前进行中的视频通话(进房参数,Web TRTC 只拉流) */ /** 医助旁观当前进行中的视频通话(进房参数,Web TRTC 只拉流) */
export function getAssistantWatchCallParams(params: { diagnosis_id: number }) { export function getAssistantWatchCallParams(params: { diagnosis_id: number }) {
return request.get({ url: '/tcm.diagnosis/watchCall', params }) return request.get({ url: '/tcm.diagnosis/watchCall', params })
+75 -1
View File
@@ -9,6 +9,7 @@
:show-file-list="false" :show-file-list="false"
:headers="headers" :headers="headers"
:data="data" :data="data"
:http-request="useDirect ? httpRequest : undefined"
:on-progress="handleProgress" :on-progress="handleProgress"
:on-success="handleSuccess" :on-success="handleSuccess"
:on-exceed="handleExceed" :on-exceed="handleExceed"
@@ -41,7 +42,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { ElUpload } from 'element-plus' import type { ElUpload, UploadRequestOptions } from 'element-plus'
import { computed, defineComponent, ref, shallowRef } from 'vue' import { computed, defineComponent, ref, shallowRef } from 'vue'
import config from '@/config' import config from '@/config'
@@ -49,6 +50,10 @@ import { RequestCodeEnum } from '@/enums/requestEnums'
import useAppStore from '@/stores/modules/app' import useAppStore from '@/stores/modules/app'
import useUserStore from '@/stores/modules/user' import useUserStore from '@/stores/modules/user'
import feedback from '@/utils/feedback' import feedback from '@/utils/feedback'
import {
DirectUploadFallbackError,
uploadVideoDirectToCos
} from '@/utils/oss-direct-upload'
export default defineComponent({ export default defineComponent({
components: {}, components: {},
@@ -77,6 +82,11 @@ export default defineComponent({
showProgress: { showProgress: {
type: Boolean, type: Boolean,
default: false default: false
},
// 视频直传到 OSS(绕开服务器中转,仅 type=video 生效)
direct: {
type: Boolean,
default: false
} }
}, },
emits: ['change', 'error', 'success', 'allSuccess'], emits: ['change', 'error', 'success', 'allSuccess'],
@@ -92,6 +102,9 @@ export default defineComponent({
const visible = ref(false) const visible = ref(false)
const fileList = ref<any[]>([]) const fileList = ref<any[]>([])
// 仅 video + direct 时才接管 http-request
const useDirect = computed(() => props.direct && props.type === 'video')
const handleProgress = () => { const handleProgress = () => {
visible.value = true visible.value = true
} }
@@ -142,12 +155,73 @@ export default defineComponent({
return '*' return '*'
} }
}) })
// 走 COS 直传:成功时模拟老接口的响应 envelope,失败/降级时回到默认 XHR
const httpRequest = async (options: UploadRequestOptions) => {
visible.value = true
try {
const data = await uploadVideoDirectToCos({
file: options.file,
type: 'video',
cid: Number((options.data as any)?.cid ?? 0),
onProgress(info) {
// 触发 ElUpload 内部进度(保持与默认上传一致的体验)
;(options as any).onProgress?.({
percent: info.percent,
loaded: info.loaded,
total: info.total
})
}
})
;(options as any).onSuccess?.({ code: RequestCodeEnum.SUCCESS, msg: 'ok', data })
} catch (err: any) {
if (err instanceof DirectUploadFallbackError) {
feedback.msgWarning('当前存储不支持直传,已切换为普通上传')
await defaultXhrUpload(options)
return
}
;(options as any).onError?.(err instanceof Error ? err : new Error(String(err?.message ?? err)))
}
}
// 当 STS 不可用时,回退到 ElUpload 原生 XHR 行为(POST /upload/{type}
const defaultXhrUpload = (options: UploadRequestOptions) =>
new Promise<void>((resolve) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', action.value)
Object.entries(headers.value).forEach(([k, v]) => xhr.setRequestHeader(k, String(v ?? '')))
xhr.upload.onprogress = (e) => {
if (!e.lengthComputable) return
const percent = (e.loaded / e.total) * 100
;(options as any).onProgress?.({ percent, loaded: e.loaded, total: e.total })
}
xhr.onload = () => {
try {
const json = JSON.parse(xhr.responseText)
;(options as any).onSuccess?.(json)
} catch (e) {
;(options as any).onError?.(new Error('上传响应解析失败'))
}
resolve()
}
xhr.onerror = () => {
;(options as any).onError?.(new Error('网络错误'))
resolve()
}
const formData = new FormData()
formData.append('file', options.file)
Object.entries(options.data || {}).forEach(([k, v]) => formData.append(k, String(v)))
xhr.send(formData)
})
return { return {
uploadRefs, uploadRefs,
action, action,
headers, headers,
visible, visible,
fileList, fileList,
useDirect,
httpRequest,
getAccept, getAccept,
handleProgress, handleProgress,
handleSuccess, handleSuccess,
+142
View File
@@ -0,0 +1,142 @@
import COS from 'cos-js-sdk-v5'
import {
confirmOssUpload,
getOssCredentials,
type OssCredentialsResponse
} from '@/api/file'
export type DirectUploadType = 'video'
export interface DirectUploadProgress {
/** 0-100 */
percent: number
loaded: number
total: number
speed: number
}
export interface DirectUploadResult {
id: number
cid: number
type: number
name: string
uri: string
url: string
}
export interface DirectUploadOptions {
file: File
type: DirectUploadType
cid?: number
onProgress?: (progress: DirectUploadProgress) => void
/** 返回 false 时由调用方自行降级到老链路 */
onFallback?: (provider: string) => boolean | void
}
const SLICE_SIZE = 5 * 1024 * 1024 // 5MB
const ASYNC_LIMIT = 3
function buildKey(prefix: string, file: File): string {
const ext = (file.name.split('.').pop() || 'mp4').toLowerCase()
const ts = Date.now()
const rand = Math.random().toString(36).slice(2, 10)
return `${prefix}${ts}-${rand}.${ext}`
}
/**
* 直传到腾讯云 COS(含 STS 凭证申请、分片上传、回执)
* 不支持降级 / fallback=true 时抛错,由调用方决定走老链路。
*/
export async function uploadVideoDirectToCos(options: DirectUploadOptions): Promise<DirectUploadResult> {
const credentials: OssCredentialsResponse = await getOssCredentials({ type: options.type })
if (credentials.fallback) {
const handled = options.onFallback?.(credentials.provider) ?? false
if (!handled) {
throw new DirectUploadFallbackError(credentials.provider)
}
// 调用方自行处理,本函数早退
throw new DirectUploadFallbackError(credentials.provider)
}
if (!credentials.credentials || !credentials.bucket || !credentials.region || !credentials.key_prefix) {
throw new Error('STS 凭证返回缺失字段')
}
if (credentials.max_size && options.file.size > credentials.max_size) {
const mb = Math.round(credentials.max_size / 1024 / 1024)
throw new Error(`视频体积超出上限(${mb}MB`)
}
const cred = credentials.credentials
const expiredTime = credentials.expired_time ?? 0
const startTime = credentials.start_time ?? Math.floor(Date.now() / 1000)
const cos = new COS({
getAuthorization(_options, callback) {
callback({
TmpSecretId: cred.tmpSecretId,
TmpSecretKey: cred.tmpSecretKey,
SecurityToken: cred.sessionToken,
StartTime: startTime,
ExpiredTime: expiredTime
})
}
})
const key = buildKey(credentials.key_prefix, options.file)
await new Promise<void>((resolve, reject) => {
cos.uploadFile(
{
Bucket: credentials.bucket as string,
Region: credentials.region as string,
Key: key,
Body: options.file,
SliceSize: SLICE_SIZE,
AsyncLimit: ASYNC_LIMIT,
onProgress(info) {
if (!options.onProgress) return
const total = info.total || options.file.size
options.onProgress({
percent: Math.min(99, Math.round((info.percent || 0) * 100)),
loaded: info.loaded || 0,
total,
speed: info.speed || 0
})
}
},
(err) => {
if (err) {
reject(err)
return
}
resolve()
}
)
})
const confirmed = await confirmOssUpload({
type: options.type,
key,
name: options.file.name,
size: options.file.size,
content_type: options.file.type || '',
cid: options.cid ?? 0
})
options.onProgress?.({ percent: 100, loaded: options.file.size, total: options.file.size, speed: 0 })
return confirmed
}
/** 当后端 fallback=true 时抛出,调用方据此降级到老链路 */
export class DirectUploadFallbackError extends Error {
readonly provider: string
constructor(provider: string) {
super(`COS 直传不可用,当前驱动: ${provider}`)
this.name = 'DirectUploadFallbackError'
this.provider = provider
}
}
@@ -1,23 +1,28 @@
<template> <template>
<div class="call-record-panel"> <div class="call-record-panel">
<el-alert <div class="call-record-toolbar">
type="info" <upload
show-icon ref="toolbarUploadRef"
:closable="false" type="video"
class="mb-4" direct
title="视频通话与录制回放" :multiple="false"
> :limit="1"
<!-- <p class="call-record-tip"> :show-progress="true"
下列为与本诊单关联的通话记录医生接通并同步房间号后服务端会尝试自动发起云端混流录制需配置 CAM 密钥与云点播等录制完成后由腾讯云回调写入回放地址同时管理端浏览器会进行本地录制挂断后自动上传并合并到本条记录 @success="handleToolbarUploadSuccess"
</p> >
<p class="call-record-tip muted"> <el-button type="primary">上传视频</el-button>
回调 URL 示例<code>{{ callbackHint }}</code> </upload>
若配置了 <code>trtc.recording_callback_token</code>请在 URL 后附加 </div>
<code>?token=你的密钥</code>
</p> -->
</el-alert>
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无通话记录"> <el-table v-loading="loading" :data="rows" border stripe>
<template #empty>
<div class="call-record-empty">
<div class="call-record-empty__title">暂无通话记录</div>
<div class="call-record-empty__desc">
现在可以直接点击上方上传视频系统会自动生成一条默认通话记录来承载回放
</div>
</div>
</template>
<el-table-column label="开始时间" width="170" prop="start_time_text" /> <el-table-column label="开始时间" width="170" prop="start_time_text" />
<el-table-column label="结束时间" width="170" prop="end_time_text" /> <el-table-column label="结束时间" width="170" prop="end_time_text" />
<el-table-column label="通话类型" width="100"> <el-table-column label="通话类型" width="100">
@@ -25,7 +30,7 @@
{{ row.call_type === 1 ? '语音' : '视频' }} {{ row.call_type === 1 ? '语音' : '视频' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="房间号" width="140"> <el-table-column label="房间号" width="180">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.room_id" class="text-primary">{{ row.room_id }}</span> <span v-if="row.room_id" class="text-primary">{{ row.room_id }}</span>
<span v-else class="text-gray-400"></span> <span v-else class="text-gray-400"></span>
@@ -42,12 +47,26 @@
{{ row.recording_status_text || '—' }} {{ row.recording_status_text || '—' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="录制回放" min-width="280"> <el-table-column label="上传回放" width="180">
<template #default="{ row }">
<upload
type="video"
direct
:multiple="false"
:limit="1"
:show-progress="true"
@success="(response: any) => handleRowUploadSuccess(row, response)"
>
<el-button type="primary" plain size="small">上传视频</el-button>
</upload>
</template>
</el-table-column>
<el-table-column label="录制回放" min-width="320">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="(row.recording_urls_list || []).length" class="recording-list"> <div v-if="(row.recording_urls_list || []).length" class="recording-list">
<div <div
v-for="(url, idx) in row.recording_urls_list" v-for="(url, idx) in row.recording_urls_list"
:key="idx" :key="`${row.id}-${idx}`"
class="recording-item" class="recording-item"
> >
<video <video
@@ -70,20 +89,30 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, computed } from 'vue' import Upload from '@/components/upload/index.vue'
import { getCallRecords } from '@/api/tcm' import feedback from '@/utils/feedback'
import { attachLocalCallRecording, createManualCallRecord, getCallRecords } from '@/api/tcm'
import { ref, useTemplateRef, watch } from 'vue'
type CallRecordRow = {
id: number
call_type: number
room_id?: string
status: number
recording_status_text?: string
recording_urls_list?: string[]
start_time_text?: string
end_time_text?: string
duration_text?: string
}
const props = defineProps<{ const props = defineProps<{
diagnosisId: number diagnosisId: number
}>() }>()
const loading = ref(false) const loading = ref(false)
const rows = ref<any[]>([]) const rows = ref<CallRecordRow[]>([])
const toolbarUploadRef = useTemplateRef<InstanceType<typeof Upload>>('toolbarUploadRef')
const callbackHint = computed(() => {
const origin = typeof window !== 'undefined' ? window.location.origin : ''
return `${origin}/api/trtc/recording-notify`
})
const load = async () => { const load = async () => {
if (!props.diagnosisId) return if (!props.diagnosisId) return
@@ -101,7 +130,7 @@ const load = async () => {
watch( watch(
() => props.diagnosisId, () => props.diagnosisId,
() => { () => {
load() void load()
}, },
{ immediate: true } { immediate: true }
) )
@@ -120,30 +149,93 @@ function statusText(status: number) {
function isPlayableVideo(url: string) { function isPlayableVideo(url: string) {
if (!url || typeof url !== 'string') return false if (!url || typeof url !== 'string') return false
return /\.(mp4|webm|ogg)(\?|$)/i.test(url) return /\.(mp4|webm|ogg|mov|mkv)(\?|$)/i.test(url)
}
async function handleToolbarUploadSuccess(response: any) {
const fileUrl = normalizeUploadUrl(response)
if (!fileUrl) {
feedback.msgError('上传成功但未返回视频地址')
return
}
try {
const record = await createManualCallRecord({ diagnosis_id: props.diagnosisId })
await attachLocalCallRecording({
diagnosis_id: props.diagnosisId,
call_record_id: Number(record?.id || 0),
file_url: fileUrl
} as any)
feedback.msgSuccess('视频回放上传成功')
await load()
} catch (error: any) {
feedback.msgError(error?.message || '写入回放失败')
}
}
async function handleRowUploadSuccess(row: CallRecordRow, response: any) {
const fileUrl = normalizeUploadUrl(response)
if (!fileUrl) {
feedback.msgError('上传成功但未返回视频地址')
return
}
try {
await attachLocalCallRecording({
diagnosis_id: props.diagnosisId,
call_record_id: Number(row.id || 0),
file_url: fileUrl
} as any)
feedback.msgSuccess('视频回放上传成功')
await load()
} catch (error: any) {
feedback.msgError(error?.message || '写入回放失败')
}
}
function normalizeUploadUrl(response: any): string {
return String(response?.data?.uri || response?.data?.url || '').trim()
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.call-record-panel { .call-record-panel {
.call-record-tip { .call-record-toolbar {
margin: 0 0 8px; display: flex;
line-height: 1.5; align-items: center;
font-size: 13px; gap: 12px;
&.muted { margin-bottom: 12px;
color: var(--el-text-color-secondary);
}
code {
font-size: 12px;
word-break: break-all;
}
} }
.call-record-empty {
padding: 28px 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
.call-record-empty__title {
font-size: 14px;
color: var(--el-text-color-primary);
}
.call-record-empty__desc {
margin-top: 8px;
font-size: 12px;
line-height: 1.6;
}
.recording-list { .recording-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 8px;
} }
.recording-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.recording-video { .recording-video {
max-width: 100%; max-width: 100%;
max-height: 180px; max-height: 180px;
@@ -15,6 +15,7 @@
namespace app\adminapi\controller; namespace app\adminapi\controller;
use app\common\service\DirectUploadService;
use app\common\service\UploadService; use app\common\service\UploadService;
use Exception; use Exception;
use think\response\Json; 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()); 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 进房参数,仅拉流) * @notes 医助旁观诊单当前视频通话(TRTC 进房参数,仅拉流)
* @return \think\response\Json * @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 Exception;
use Qcloud\Cos\Client; use Qcloud\Cos\Client;
use QCloud\COSSTS\Sts;
/** /**
* 腾讯云存储引擎 (COS) * 腾讯云存储引擎 (COS)
@@ -113,4 +114,125 @@ class Qcloud extends Server
return $this->fileName; 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", "phpoffice/phpspreadsheet": "^1.22",
"qiniu/php-sdk": "7.4", "qiniu/php-sdk": "7.4",
"qcloud/cos-sdk-v5": "^2.5", "qcloud/cos-sdk-v5": "^2.5",
"qcloud_sts/qcloud-sts-sdk": "^3.0",
"aliyuncs/oss-sdk-php": "^2.4", "aliyuncs/oss-sdk-php": "^2.4",
"alibabacloud/client": "^1.5", "alibabacloud/client": "^1.5",
"rmccue/requests": "^2.0", "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c8b18a5821cfd82f2e70cf67a7f40740", "content-hash": "a5f0d3c9e4372fa66156ac28411cd86e",
"packages": [ "packages": [
{ {
"name": "adbario/php-dot-notation", "name": "adbario/php-dot-notation",
@@ -2541,6 +2541,58 @@
}, },
"time": "2023-08-16T02:26:48+00:00" "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", "name": "qiniu/php-sdk",
"version": "v7.4.0", "version": "v7.4.0",
@@ -5132,12 +5184,12 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=8.0" "php": ">=8.0"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }
+23 -4
View File
@@ -32,6 +32,11 @@ class InstalledVersions
*/ */
private static $installed; private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/** /**
* @var bool|null * @var bool|null
*/ */
@@ -309,6 +314,12 @@ class InstalledVersions
{ {
self::$installed = $data; self::$installed = $data;
self::$installedByVendor = array(); 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(); $installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = strtr(__DIR__, '\\', '/');
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } 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 */ /** @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'; $required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required; self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { $installed[] = $required;
self::$installed = $installed[count($installed) - 1]; 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; $installed[] = self::$installed;
} }
+3 -2
View File
@@ -9,14 +9,14 @@ return array(
'think\\view\\driver\\' => array($vendorDir . '/topthink/think-view/src'), 'think\\view\\driver\\' => array($vendorDir . '/topthink/think-view/src'),
'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'), 'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'),
'think\\app\\' => array($vendorDir . '/topthink/think-multi-app/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'), 'clagiordano\\weblibs\\configmanager\\' => array($vendorDir . '/clagiordano/weblibs-configmanager/src'),
'app\\' => array($baseDir . '/app'), 'app\\' => array($baseDir . '/app'),
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'), 'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'WpOrg\\Requests\\' => array($vendorDir . '/rmccue/requests/src'), 'WpOrg\\Requests\\' => array($vendorDir . '/rmccue/requests/src'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'TheNorthMemory\\Xml\\' => array($vendorDir . '/thenorthmemory/xml/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\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'), 'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
@@ -37,6 +37,7 @@ return array(
'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'), 'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'),
'Qiniu\\' => array($vendorDir . '/qiniu/php-sdk/src/Qiniu'), 'Qiniu\\' => array($vendorDir . '/qiniu/php-sdk/src/Qiniu'),
'Qcloud\\Cos\\' => array($vendorDir . '/qcloud/cos-sdk-v5/src'), '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\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/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 ( array (
'Qiniu\\' => 6, 'Qiniu\\' => 6,
'Qcloud\\Cos\\' => 11, 'Qcloud\\Cos\\' => 11,
'QCloud\\COSSTS\\' => 14,
), ),
'P' => 'P' =>
array ( array (
@@ -162,10 +163,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
), ),
'think\\' => 'think\\' =>
array ( array (
0 => __DIR__ . '/..' . '/topthink/framework/src/think', 0 => __DIR__ . '/..' . '/topthink/think-template/src',
1 => __DIR__ . '/..' . '/topthink/think-helper/src', 1 => __DIR__ . '/..' . '/topthink/framework/src/think',
2 => __DIR__ . '/..' . '/topthink/think-orm/src', 2 => __DIR__ . '/..' . '/topthink/think-orm/src',
3 => __DIR__ . '/..' . '/topthink/think-template/src', 3 => __DIR__ . '/..' . '/topthink/think-helper/src',
), ),
'clagiordano\\weblibs\\configmanager\\' => 'clagiordano\\weblibs\\configmanager\\' =>
array ( array (
@@ -193,9 +194,9 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
), ),
'TencentCloud\\' => 'TencentCloud\\' =>
array ( array (
0 => __DIR__ . '/..' . '/tencentcloud/common/src/TencentCloud', 0 => __DIR__ . '/..' . '/tencentcloud/trtc/src/TencentCloud',
1 => __DIR__ . '/..' . '/tencentcloud/sms/src/TencentCloud', 1 => __DIR__ . '/..' . '/tencentcloud/sms/src/TencentCloud',
2 => __DIR__ . '/..' . '/tencentcloud/trtc/src/TencentCloud', 2 => __DIR__ . '/..' . '/tencentcloud/common/src/TencentCloud',
), ),
'Symfony\\Polyfill\\Php81\\' => 'Symfony\\Polyfill\\Php81\\' =>
array ( array (
@@ -277,6 +278,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
array ( array (
0 => __DIR__ . '/..' . '/qcloud/cos-sdk-v5/src', 0 => __DIR__ . '/..' . '/qcloud/cos-sdk-v5/src',
), ),
'QCloud\\COSSTS\\' =>
array (
0 => __DIR__ . '/..' . '/qcloud_sts/qcloud-sts-sdk/src',
),
'Psr\\SimpleCache\\' => 'Psr\\SimpleCache\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src', 0 => __DIR__ . '/..' . '/psr/simple-cache/src',
+55
View File
@@ -2637,6 +2637,61 @@
}, },
"install-path": "../qcloud/cos-sdk-v5" "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", "name": "qiniu/php-sdk",
"version": "v7.4.0", "version": "v7.4.0",
+15 -6
View File
@@ -1,9 +1,9 @@
<?php return array( <?php return array(
'root' => array( 'root' => array(
'name' => 'topthink/think', 'name' => 'topthink/think',
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-master',
'version' => '1.0.0.0', 'version' => 'dev-master',
'reference' => NULL, 'reference' => '4dc9ec1878f6cdd83cfdc9044b93828d46eda715',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -376,6 +376,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, '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( 'qiniu/php-sdk' => array(
'pretty_version' => 'v7.4.0', 'pretty_version' => 'v7.4.0',
'version' => '7.4.0.0', 'version' => '7.4.0.0',
@@ -632,9 +641,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'topthink/think' => array( 'topthink/think' => array(
'pretty_version' => '1.0.0+no-version-set', 'pretty_version' => 'dev-master',
'version' => '1.0.0.0', 'version' => 'dev-master',
'reference' => NULL, 'reference' => '4dc9ec1878f6cdd83cfdc9044b93828d46eda715',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
+3 -4
View File
@@ -4,10 +4,9 @@
$issues = array(); $issues = array();
// Temporarily disabled for testing if (!(PHP_VERSION_ID >= 80100)) {
// if (!(PHP_VERSION_ID >= 80100)) { $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
// $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; }
// }
if ($issues) { if ($issues) {
if (!headers_sent()) { 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 <?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); declare (strict_types = 1);
return array ( return array (
0 => 'think\\app\\Service', 0 => 'think\\app\\Service',