feat: 添加媒体资产上传初始化和完成相关API接口及数据结构

This commit is contained in:
2025-12-22 17:02:53 +08:00
parent 6ab65817d8
commit 76f639b3f3
12 changed files with 959 additions and 19 deletions

View File

@@ -0,0 +1,57 @@
package dto
import "time"
// AdminMediaAssetUploadInitForm defines payload for tenant-admin to initialize a media asset upload.
type AdminMediaAssetUploadInitForm struct {
// Type is the media asset type (video/audio/image).
// Used to decide processing pipeline and validation rules; required.
Type string `json:"type,omitempty"`
// ContentType is the MIME type reported by the client (e.g. video/mp4); optional.
// Server should not fully trust it, but can use it as a hint for validation/logging.
ContentType string `json:"content_type,omitempty"`
// FileSize is the expected file size in bytes; optional.
// Used for quota/limit checks and audit; client may omit when unknown.
FileSize int64 `json:"file_size,omitempty"`
// SHA256 is the hex-encoded sha256 of the file; optional.
// Used for deduplication/audit; server may validate it later during upload-complete.
SHA256 string `json:"sha256,omitempty"`
}
// AdminMediaAssetUploadInitResponse returns server-generated upload parameters and the created asset id.
type AdminMediaAssetUploadInitResponse struct {
// AssetID is the created media asset id.
AssetID int64 `json:"asset_id"`
// Provider is the storage provider identifier (e.g. s3/minio/oss/local); for debugging/audit.
Provider string `json:"provider,omitempty"`
// Bucket is the target bucket/container; for debugging/audit (may be empty in stub mode).
Bucket string `json:"bucket,omitempty"`
// ObjectKey is the server-generated object key/path; client must NOT choose it.
ObjectKey string `json:"object_key,omitempty"`
// UploadURL is the URL the client should upload to (signed URL or service endpoint).
UploadURL string `json:"upload_url,omitempty"`
// Headers are additional headers required for upload (e.g. signed headers); optional.
Headers map[string]string `json:"headers,omitempty"`
// FormFields are form fields required for multipart form upload (S3 POST policy); optional.
FormFields map[string]string `json:"form_fields,omitempty"`
// ExpiresAt indicates when UploadURL/FormFields expire; optional.
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// AdminMediaAssetUploadCompleteForm defines payload for tenant-admin to mark a media upload as completed.
// This endpoint is expected to be called after the client finishes uploading the object to storage.
type AdminMediaAssetUploadCompleteForm struct {
// ETag is the storage returned ETag (or similar checksum); optional.
// Used for audit/debugging and later integrity verification.
ETag string `json:"etag,omitempty"`
// ContentType is the MIME type observed during upload; optional.
// Server may record it for audit and later processing decisions.
ContentType string `json:"content_type,omitempty"`
// FileSize is the uploaded object size in bytes; optional.
// Server records it for quota/audit and later validation.
FileSize int64 `json:"file_size,omitempty"`
// SHA256 is the hex-encoded sha256 of the uploaded object; optional.
// Server records it for integrity checks/deduplication.
SHA256 string `json:"sha256,omitempty"`
}

View File

@@ -0,0 +1,90 @@
package tenant
import (
"time"
"quyun/v2/app/errorx"
"quyun/v2/app/http/tenant/dto"
"quyun/v2/app/services"
"quyun/v2/database/models"
"github.com/gofiber/fiber/v3"
log "github.com/sirupsen/logrus"
)
// mediaAssetAdmin provides tenant-admin media asset endpoints.
//
// @provider
type mediaAssetAdmin struct{}
// uploadInit
//
// @Summary 初始化媒体资源上传(租户管理)
// @Tags Tenant
// @Accept json
// @Produce json
// @Param tenantCode path string true "Tenant Code"
// @Param form body dto.AdminMediaAssetUploadInitForm true "Form"
// @Success 200 {object} dto.AdminMediaAssetUploadInitResponse
//
// @Router /t/:tenantCode/v1/admin/media_assets/upload_init [post]
// @Bind tenant local key(tenant)
// @Bind tenantUser local key(tenant_user)
// @Bind form body
func (*mediaAssetAdmin) uploadInit(
ctx fiber.Ctx,
tenant *models.Tenant,
tenantUser *models.TenantUser,
form *dto.AdminMediaAssetUploadInitForm,
) (*dto.AdminMediaAssetUploadInitResponse, error) {
if err := requireTenantAdmin(tenantUser); err != nil {
return nil, err
}
if form == nil {
return nil, errorx.ErrInvalidParameter
}
log.WithFields(log.Fields{
"tenant_id": tenant.ID,
"user_id": tenantUser.UserID,
"type": form.Type,
}).Info("tenant.admin.media_assets.upload_init")
return services.MediaAsset.AdminUploadInit(ctx.Context(), tenant.ID, tenantUser.UserID, form, time.Now())
}
// uploadComplete
//
// @Summary 确认上传完成并进入处理(租户管理)
// @Tags Tenant
// @Accept json
// @Produce json
// @Param tenantCode path string true "Tenant Code"
// @Param assetID path int64 true "AssetID"
// @Param form body dto.AdminMediaAssetUploadCompleteForm false "Form"
// @Success 200 {object} models.MediaAsset
//
// @Router /t/:tenantCode/v1/admin/media_assets/:assetID/upload_complete [post]
// @Bind tenant local key(tenant)
// @Bind tenantUser local key(tenant_user)
// @Bind assetID path
// @Bind form body
func (*mediaAssetAdmin) uploadComplete(
ctx fiber.Ctx,
tenant *models.Tenant,
tenantUser *models.TenantUser,
assetID int64,
form *dto.AdminMediaAssetUploadCompleteForm,
) (*models.MediaAsset, error) {
if err := requireTenantAdmin(tenantUser); err != nil {
return nil, err
}
log.WithFields(log.Fields{
"tenant_id": tenant.ID,
"user_id": tenantUser.UserID,
"asset_id": assetID,
}).Info("tenant.admin.media_assets.upload_complete")
return services.MediaAsset.AdminUploadComplete(ctx.Context(), tenant.ID, tenantUser.UserID, assetID, form, time.Now())
}

View File

@@ -31,6 +31,13 @@ func Provide(opts ...opt.Option) error {
}); err != nil {
return err
}
if err := container.Container.Provide(func() (*mediaAssetAdmin, error) {
obj := &mediaAssetAdmin{}
return obj, nil
}); err != nil {
return err
}
if err := container.Container.Provide(func() (*order, error) {
obj := &order{}
@@ -56,6 +63,7 @@ func Provide(opts ...opt.Option) error {
content *content,
contentAdmin *contentAdmin,
me *me,
mediaAssetAdmin *mediaAssetAdmin,
middlewares *middlewares.Middlewares,
order *order,
orderAdmin *orderAdmin,
@@ -68,6 +76,7 @@ func Provide(opts ...opt.Option) error {
content: content,
contentAdmin: contentAdmin,
me: me,
mediaAssetAdmin: mediaAssetAdmin,
middlewares: middlewares,
order: order,
orderAdmin: orderAdmin,

View File

@@ -27,6 +27,7 @@ type Routes struct {
content *content
contentAdmin *contentAdmin
me *me
mediaAssetAdmin *mediaAssetAdmin
order *order
orderAdmin *orderAdmin
orderMe *orderMe
@@ -132,6 +133,22 @@ func (r *Routes) Register(router fiber.Router) {
Local[*models.User]("user"),
Query[dto.MyLedgerListFilter]("filter"),
))
// Register routes for controller: mediaAssetAdmin
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/admin/media_assets/:assetID/upload_complete -> mediaAssetAdmin.uploadComplete")
router.Post("/t/:tenantCode/v1/admin/media_assets/:assetID/upload_complete"[len(r.Path()):], DataFunc4(
r.mediaAssetAdmin.uploadComplete,
Local[*models.Tenant]("tenant"),
Local[*models.TenantUser]("tenant_user"),
PathParam[int64]("assetID"),
Body[dto.AdminMediaAssetUploadCompleteForm]("form"),
))
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/admin/media_assets/upload_init -> mediaAssetAdmin.uploadInit")
router.Post("/t/:tenantCode/v1/admin/media_assets/upload_init"[len(r.Path()):], DataFunc3(
r.mediaAssetAdmin.uploadInit,
Local[*models.Tenant]("tenant"),
Local[*models.TenantUser]("tenant_user"),
Body[dto.AdminMediaAssetUploadInitForm]("form"),
))
// Register routes for controller: order
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/contents/:contentID/purchase -> order.purchaseContent")
router.Post("/t/:tenantCode/v1/contents/:contentID/purchase"[len(r.Path()):], DataFunc4(