feat: add direct s3 upload flow
This commit is contained in:
@@ -38,12 +38,29 @@ type Config struct {
|
||||
Log Log `yaml:"Log"`
|
||||
Currency Currency `yaml:"Currency"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
S3 S3Config `yaml:"S3"`
|
||||
Administrator struct {
|
||||
Email string `yaml:"Email" default:"admin@ppanel.dev"`
|
||||
Password string `yaml:"Password" default:"password"`
|
||||
} `yaml:"Administrator"`
|
||||
}
|
||||
|
||||
type S3Config struct {
|
||||
Enable bool `yaml:"Enable" default:"false"`
|
||||
Region string `yaml:"Region" default:""`
|
||||
Bucket string `yaml:"Bucket" default:""`
|
||||
Endpoint string `yaml:"Endpoint" default:""`
|
||||
AccessKey string `yaml:"AccessKey" default:""`
|
||||
SecretKey string `yaml:"SecretKey" default:""`
|
||||
SessionToken string `yaml:"SessionToken" default:""`
|
||||
Prefix string `yaml:"Prefix" default:"app-upload"`
|
||||
PublicBaseURL string `yaml:"PublicBaseURL" default:""`
|
||||
UsePathStyle bool `yaml:"UsePathStyle" default:"false"`
|
||||
PresignExpireSeconds int64 `yaml:"PresignExpireSeconds" default:"300"`
|
||||
MaxUploadSize int64 `yaml:"MaxUploadSize" default:"104857600"`
|
||||
AllowedContentTypes string `yaml:"AllowedContentTypes" default:"application/zip,application/x-zip-compressed,application/gzip,application/x-gzip,application/octet-stream,text/plain,application/json"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `yaml:"Host" default:"localhost:6379"`
|
||||
Pass string `yaml:"Pass" default:""`
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/file"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Complete file upload
|
||||
func FileUploadCompleteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.FileUploadCompleteRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := file.NewFileUploadCompleteLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.FileUploadComplete(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/file"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Init file upload
|
||||
func FileUploadInitHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.FileUploadInitRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := file.NewFileUploadInitLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.FileUploadInit(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
common "github.com/perfect-panel/server/internal/handler/common"
|
||||
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
|
||||
publicDocument "github.com/perfect-panel/server/internal/handler/public/document"
|
||||
publicFile "github.com/perfect-panel/server/internal/handler/public/file"
|
||||
publicIapApple "github.com/perfect-panel/server/internal/handler/public/iap/apple"
|
||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||
@@ -865,6 +866,17 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
publicDocumentGroupRouter.GET("/list", publicDocument.QueryDocumentListHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicFileGroupRouter := router.Group("/v1/public/file")
|
||||
publicFileGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
{
|
||||
// Init file upload
|
||||
publicFileGroupRouter.POST("/upload/init", publicFile.FileUploadInitHandler(serverCtx))
|
||||
|
||||
// Complete file upload
|
||||
publicFileGroupRouter.POST("/upload/complete", publicFile.FileUploadCompleteHandler(serverCtx))
|
||||
}
|
||||
|
||||
publicOrderGroupRouter := router.Group("/v1/public/order")
|
||||
publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
fileUploadInitStatus = "init"
|
||||
fileUploadCompleteStatus = "completed"
|
||||
fileUploadRedisPrefix = "file:upload:"
|
||||
)
|
||||
|
||||
var safeFileNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
|
||||
|
||||
type uploadMeta struct {
|
||||
FileID string `json:"file_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
BizType string `json:"biz_type"`
|
||||
FileName string `json:"file_name"`
|
||||
ContentType string `json:"content_type"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Size int64 `json:"size"`
|
||||
Sha256 string `json:"sha256"`
|
||||
Status string `json:"status"`
|
||||
Etag string `json:"etag,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
CompletedAt int64 `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
func currentUserFromContext(ctx context.Context) (*user.User, error) {
|
||||
u, ok := ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok || u == nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func allowedContentTypeSet(csv string) map[string]struct{} {
|
||||
set := make(map[string]struct{})
|
||||
for _, item := range strings.Split(csv, ",") {
|
||||
item = strings.TrimSpace(strings.ToLower(item))
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
set[item] = struct{}{}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func validateInitRequest(svcCtx *svc.ServiceContext, bizType, fileName, contentType string, size int64) error {
|
||||
if svcCtx.S3Store == nil || !svcCtx.Config.S3.Enable {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "S3 upload is not enabled")
|
||||
}
|
||||
if strings.TrimSpace(bizType) == "" {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "biz_type is required")
|
||||
}
|
||||
if strings.TrimSpace(fileName) == "" {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file_name is required")
|
||||
}
|
||||
if size <= 0 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "size must be greater than 0")
|
||||
}
|
||||
if svcCtx.Config.S3.MaxUploadSize > 0 && size > svcCtx.Config.S3.MaxUploadSize {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file size exceeds max limit")
|
||||
}
|
||||
allowed := allowedContentTypeSet(svcCtx.Config.S3.AllowedContentTypes)
|
||||
if len(allowed) > 0 {
|
||||
if _, ok := allowed[strings.ToLower(strings.TrimSpace(contentType))]; !ok {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "content_type is not allowed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildFileID(userID int64, bizType, fileName string) string {
|
||||
h := sha1.New()
|
||||
_, _ = h.Write([]byte(fmt.Sprintf("%d:%s:%s:%d", userID, bizType, fileName, time.Now().UnixNano())))
|
||||
return hex.EncodeToString(h.Sum(nil))[:24]
|
||||
}
|
||||
|
||||
func safeFileName(name string) string {
|
||||
base := filepath.Base(strings.TrimSpace(name))
|
||||
if base == "." || base == "/" || base == "" {
|
||||
return "file.bin"
|
||||
}
|
||||
safe := safeFileNameRegexp.ReplaceAllString(base, "_")
|
||||
if safe == "" {
|
||||
return "file.bin"
|
||||
}
|
||||
return safe
|
||||
}
|
||||
|
||||
func buildObjectKey(prefix string, userID int64, bizType, fileID, fileName string, now time.Time) string {
|
||||
prefix = strings.Trim(prefix, "/")
|
||||
if prefix == "" {
|
||||
prefix = "app-upload"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/%d/%04d/%02d/%s_%s",
|
||||
prefix,
|
||||
strings.Trim(safeFileNameRegexp.ReplaceAllString(strings.ToLower(strings.TrimSpace(bizType)), "-"), "-"),
|
||||
userID,
|
||||
now.Year(),
|
||||
int(now.Month()),
|
||||
fileID,
|
||||
safeFileName(fileName),
|
||||
)
|
||||
}
|
||||
|
||||
func uploadRedisKey(fileID string) string {
|
||||
return fileUploadRedisPrefix + fileID
|
||||
}
|
||||
|
||||
func saveUploadMeta(ctx context.Context, svcCtx *svc.ServiceContext, meta *uploadMeta, ttl time.Duration) error {
|
||||
data, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal upload meta failed")
|
||||
}
|
||||
if err := svcCtx.Redis.Set(ctx, uploadRedisKey(meta.FileID), data, ttl).Err(); err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "save upload meta failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadUploadMeta(ctx context.Context, svcCtx *svc.ServiceContext, fileID string) (*uploadMeta, error) {
|
||||
val, err := svcCtx.Redis.Get(ctx, uploadRedisKey(fileID)).Result()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "upload meta not found")
|
||||
}
|
||||
var meta uploadMeta
|
||||
if err := json.Unmarshal([]byte(val), &meta); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse upload meta failed")
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.9.2
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type FileUploadCompleteLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Complete file upload
|
||||
func NewFileUploadCompleteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FileUploadCompleteLogic {
|
||||
return &FileUploadCompleteLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *FileUploadCompleteLogic) FileUploadComplete(req *types.FileUploadCompleteRequest) (resp *types.FileUploadCompleteResponse, err error) {
|
||||
u, err := currentUserFromContext(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta, err := loadUploadMeta(l.ctx, l.svcCtx, req.FileId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.UserID != u.Id {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
head, err := l.svcCtx.S3Store.HeadObject(l.ctx, meta.ObjectKey)
|
||||
if err != nil {
|
||||
l.Errorw("head object failed", logger.Field("error", err.Error()), logger.Field("file_id", meta.FileID))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "uploaded file not found")
|
||||
}
|
||||
if meta.Size > 0 && head.ContentLength != meta.Size {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "uploaded file size mismatch")
|
||||
}
|
||||
|
||||
meta.Status = fileUploadCompleteStatus
|
||||
meta.Etag = head.ETag
|
||||
meta.ContentType = head.ContentType
|
||||
meta.CompletedAt = time.Now().Unix()
|
||||
if err := saveUploadMeta(l.ctx, l.svcCtx, meta, 7*24*time.Hour); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.FileUploadCompleteResponse{
|
||||
FileId: meta.FileID,
|
||||
ObjectKey: meta.ObjectKey,
|
||||
Size: head.ContentLength,
|
||||
ContentType: head.ContentType,
|
||||
Etag: head.ETag,
|
||||
Status: meta.Status,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.9.2
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
)
|
||||
|
||||
type FileUploadInitLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// Init file upload
|
||||
func NewFileUploadInitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FileUploadInitLogic {
|
||||
return &FileUploadInitLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *FileUploadInitLogic) FileUploadInit(req *types.FileUploadInitRequest) (resp *types.FileUploadInitResponse, err error) {
|
||||
u, err := currentUserFromContext(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateInitRequest(l.svcCtx, req.BizType, req.FileName, req.ContentType, req.Size); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fileID := buildFileID(u.Id, req.BizType, req.FileName)
|
||||
objectKey := buildObjectKey(l.svcCtx.Config.S3.Prefix, u.Id, req.BizType, fileID, req.FileName, now)
|
||||
expireSeconds := l.svcCtx.Config.S3.PresignExpireSeconds
|
||||
if expireSeconds <= 0 {
|
||||
expireSeconds = 300
|
||||
}
|
||||
|
||||
presigned, err := l.svcCtx.S3Store.PresignPutObject(l.ctx, objectKey, req.ContentType, time.Duration(expireSeconds)*time.Second)
|
||||
if err != nil {
|
||||
l.Errorw("presign put object failed", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta := &uploadMeta{
|
||||
FileID: fileID,
|
||||
UserID: u.Id,
|
||||
BizType: req.BizType,
|
||||
FileName: req.FileName,
|
||||
ContentType: req.ContentType,
|
||||
ObjectKey: objectKey,
|
||||
Size: req.Size,
|
||||
Sha256: req.Sha256,
|
||||
Status: fileUploadInitStatus,
|
||||
CreatedAt: now.Unix(),
|
||||
}
|
||||
if err := saveUploadMeta(l.ctx, l.svcCtx, meta, 24*time.Hour); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.FileUploadInitResponse{
|
||||
FileId: fileID,
|
||||
ObjectKey: objectKey,
|
||||
UploadURL: presigned.URL,
|
||||
Method: presigned.Method,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": req.ContentType,
|
||||
},
|
||||
ExpiredAt: presigned.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/perfect-panel/server/internal/model/node"
|
||||
"github.com/perfect-panel/server/internal/model/redemption"
|
||||
"github.com/perfect-panel/server/pkg/device"
|
||||
"github.com/perfect-panel/server/pkg/storage"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/ads"
|
||||
@@ -44,6 +45,7 @@ type ServiceContext struct {
|
||||
ExchangeRate float64
|
||||
GeoIP *IPLocation
|
||||
SignatureValidator *signature.Validator
|
||||
S3Store *storage.S3Store
|
||||
|
||||
//NodeCache *cache.NodeCacheClient
|
||||
AuthModel auth.Model
|
||||
@@ -143,6 +145,13 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
}
|
||||
srv.IAPAppleTransactionModel = iapapple.NewModel(db, rds)
|
||||
srv.DeviceManager = NewDeviceManager(srv)
|
||||
if c.S3.Enable {
|
||||
s3Store, err := storage.NewS3Store(context.Background(), c.S3)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
srv.S3Store = s3Store
|
||||
}
|
||||
return srv
|
||||
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package types
|
||||
|
||||
// compat_types.go — 手动补充的类型,已同步到 .api 定义
|
||||
// 下次用 goctl 重新生成 types.go 后,以下类型会自动包含在 types.go 中,届时可以删除本文件中对应的定义:
|
||||
// - ContactRequest (已加入 common.api)
|
||||
// - GetDownloadLinkRequest / GetDownloadLinkResponse (已加入 common.api)
|
||||
// - EmailLoginRequest (已更新 auth.api, 加入 form tag)
|
||||
// - ReportLogMessageRequest / ReportLogMessageResponse (已加入 common.api, 路由 POST /v1/common/log/report)
|
||||
// - LegacyCheckVerificationCodeRequest / LegacyCheckVerificationCodeResponse (已加入 common.api, 路由 POST /v1/common/check_code)
|
||||
|
||||
type ContactRequest struct {
|
||||
Name string `json:"name" validate:"required,max=100"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
OtherContact string `json:"other_contact" validate:"max=200"`
|
||||
Notes string `json:"notes" validate:"max=2000"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkRequest struct {
|
||||
InviteCode string `form:"invite_code,optional"`
|
||||
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkResponse struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type ReportLogMessageRequest struct {
|
||||
Platform string `json:"platform" validate:"required,max=32"`
|
||||
AppVersion string `json:"app_version" validate:"required,max=64"`
|
||||
OsName string `json:"os_name" validate:"max=64"`
|
||||
OsVersion string `json:"os_version" validate:"max=64"`
|
||||
DeviceId string `json:"device_id" validate:"required,max=255"`
|
||||
UserId int64 `json:"user_id"`
|
||||
SessionId string `json:"session_id" validate:"max=255"`
|
||||
Level uint8 `json:"level"`
|
||||
ErrorCode string `json:"error_code" validate:"max=128"`
|
||||
Message string `json:"message" validate:"required,max=65535"`
|
||||
Stack string `json:"stack" validate:"max=1048576"`
|
||||
Context interface{} `json:"context"`
|
||||
OccurredAt int64 `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type ReportLogMessageResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeRequest struct {
|
||||
Method string `json:"method" form:"method" validate:"omitempty,oneof=email mobile"`
|
||||
Account string `json:"account" form:"account"`
|
||||
Email string `json:"email" form:"email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Type uint8 `json:"type" form:"type" validate:"required"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
||||
type EmailLoginRequest struct {
|
||||
Identifier string `json:"identifier" form:"identifier"`
|
||||
Email string `json:"email" form:"email" validate:"required,email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Invite string `json:"invite" form:"invite"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional" form:"cf_token"`
|
||||
}
|
||||
+125
-35
@@ -1,8 +1,12 @@
|
||||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl 1.7.2
|
||||
// goctl 1.9.2
|
||||
|
||||
package types
|
||||
|
||||
type ActivateOrderRequest struct {
|
||||
OrderNo string `json:"order_no" validate:"required"`
|
||||
}
|
||||
|
||||
type Ads struct {
|
||||
Id int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@@ -295,6 +299,13 @@ type ConnectionRecords struct {
|
||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||
}
|
||||
|
||||
type ContactRequest struct {
|
||||
Name string `json:"name" validate:"required,max=100"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
OtherContact string `json:"other_contact" validate:"max=200"`
|
||||
Notes string `json:"notes" validate:"max=2000"`
|
||||
}
|
||||
|
||||
type Coupon struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -549,6 +560,13 @@ type CurrencyConfig struct {
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
}
|
||||
|
||||
type DailyTrafficStats struct {
|
||||
Date string `json:"date"`
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type DeleteAccountRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
@@ -561,13 +579,6 @@ type DeleteAccountResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
}
|
||||
|
||||
type DailyTrafficStats struct {
|
||||
Date string `json:"date"`
|
||||
Upload int64 `json:"upload"`
|
||||
Download int64 `json:"download"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type DeleteAdsRequest struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
@@ -689,8 +700,15 @@ type EmailAuthticateConfig struct {
|
||||
DomainSuffixList string `json:"domain_suffix_list"`
|
||||
}
|
||||
|
||||
type ExportGroupResultRequest struct {
|
||||
HistoryId *int64 `form:"history_id,omitempty"`
|
||||
type EmailLoginRequest struct {
|
||||
Identifier string `json:"identifier" form:"identifier"`
|
||||
Email string `json:"email" form:"email" validate:"required,email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Invite string `json:"invite,optional" form:"invite"`
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `header:"User-Agent"`
|
||||
LoginType string `header:"Login-Type"`
|
||||
CfToken string `json:"cf_token,optional" form:"cf_token"`
|
||||
}
|
||||
|
||||
type ErrorLogMessage struct {
|
||||
@@ -708,6 +726,10 @@ type ErrorLogMessage struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type ExportGroupResultRequest struct {
|
||||
HistoryId *int64 `form:"history_id,omitempty"`
|
||||
}
|
||||
|
||||
type FamilyDetail struct {
|
||||
Summary FamilySummary `json:"summary"`
|
||||
Members []FamilyMemberItem `json:"members"`
|
||||
@@ -740,6 +762,36 @@ type FamilySummary struct {
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FileUploadCompleteRequest struct {
|
||||
FileId string `json:"file_id" validate:"required"`
|
||||
}
|
||||
|
||||
type FileUploadCompleteResponse struct {
|
||||
FileId string `json:"file_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
Size int64 `json:"size"`
|
||||
ContentType string `json:"content_type"`
|
||||
Etag string `json:"etag"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type FileUploadInitRequest struct {
|
||||
BizType string `json:"biz_type" validate:"required"`
|
||||
FileName string `json:"file_name" validate:"required"`
|
||||
ContentType string `json:"content_type" validate:"required"`
|
||||
Size int64 `json:"size" validate:"required"`
|
||||
Sha256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
type FileUploadInitResponse struct {
|
||||
FileId string `json:"file_id"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
Method string `json:"method"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
}
|
||||
|
||||
type FilterBalanceLogRequest struct {
|
||||
FilterLogParams
|
||||
UserId int64 `form:"user_id,optional"`
|
||||
@@ -1048,6 +1100,15 @@ type GetDocumentListResponse struct {
|
||||
List []Document `json:"list"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkRequest struct {
|
||||
InviteCode string `form:"invite_code,optional"`
|
||||
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
|
||||
}
|
||||
|
||||
type GetDownloadLinkResponse struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type GetErrorLogMessageDetailResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
@@ -1118,18 +1179,6 @@ type GetGlobalConfigResponse struct {
|
||||
WebAd bool `json:"web_ad"`
|
||||
}
|
||||
|
||||
type GetInviteSalesRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
StartTime int64 `form:"start_time"`
|
||||
EndTime int64 `form:"end_time"`
|
||||
}
|
||||
|
||||
type GetInviteSalesResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
List []InvitedUserSale `json:"list"`
|
||||
}
|
||||
|
||||
type GetGroupConfigRequest struct {
|
||||
Keys []string `form:"keys,omitempty"`
|
||||
}
|
||||
@@ -1161,6 +1210,18 @@ type GetGroupHistoryResponse struct {
|
||||
List []GroupHistory `json:"list"`
|
||||
}
|
||||
|
||||
type GetInviteSalesRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
StartTime int64 `form:"start_time"`
|
||||
EndTime int64 `form:"end_time"`
|
||||
}
|
||||
|
||||
type GetInviteSalesResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
List []InvitedUserSale `json:"list"`
|
||||
}
|
||||
|
||||
type GetLoginLogRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@@ -1419,7 +1480,7 @@ type GetUserListRequest struct {
|
||||
FamilyStatus string `form:"family_status,omitempty"`
|
||||
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
SortOrder string `form:"sort_order,omitempty"` // asc or desc, default desc
|
||||
SortOrder string `form:"sort_order,omitempty"`
|
||||
}
|
||||
|
||||
type GetUserListResponse struct {
|
||||
@@ -1601,6 +1662,19 @@ type KickOfflineRequest struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeRequest struct {
|
||||
Method string `json:"method" form:"method" validate:"omitempty,oneof=email mobile"`
|
||||
Account string `json:"account" form:"account"`
|
||||
Email string `json:"email" form:"email"`
|
||||
Code string `json:"code" form:"code" validate:"required"`
|
||||
Type uint8 `json:"type" form:"type" validate:"required"`
|
||||
}
|
||||
|
||||
type LegacyCheckVerificationCodeResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
||||
type LogResponse struct {
|
||||
List interface{} `json:"list"`
|
||||
}
|
||||
@@ -1765,8 +1839,8 @@ type Order struct {
|
||||
TradeNo string `json:"trade_no"`
|
||||
Status uint8 `json:"status"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
CreatedAt int64 `json:"created_at"` // Unix milliseconds
|
||||
UpdatedAt int64 `json:"updated_at"` // Unix milliseconds
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type OrderDetail struct {
|
||||
@@ -1789,8 +1863,8 @@ type OrderDetail struct {
|
||||
Status uint8 `json:"status"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
Subscribe Subscribe `json:"subscribe"`
|
||||
CreatedAt int64 `json:"created_at"` // Unix milliseconds
|
||||
UpdatedAt int64 `json:"updated_at"` // Unix milliseconds
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type OrdersStatistics struct {
|
||||
@@ -2246,6 +2320,10 @@ type RecoverOrderResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type RecoverySendCodeRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
type RedeemCodeRequest struct {
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
@@ -2319,6 +2397,26 @@ type RenewalOrderResponse struct {
|
||||
AppAccountToken string `json:"app_account_token"`
|
||||
}
|
||||
|
||||
type ReportLogMessageRequest struct {
|
||||
Platform string `json:"platform" validate:"required,max=32"`
|
||||
AppVersion string `json:"app_version" validate:"required,max=64"`
|
||||
OsName string `json:"os_name" validate:"max=64"`
|
||||
OsVersion string `json:"os_version" validate:"max=64"`
|
||||
DeviceId string `json:"device_id" validate:"required,max=255"`
|
||||
UserId int64 `json:"user_id"`
|
||||
SessionId string `json:"session_id" validate:"max=255"`
|
||||
Level uint8 `json:"level"`
|
||||
ErrorCode string `json:"error_code" validate:"max=128"`
|
||||
Message string `json:"message" validate:"required,max=65535"`
|
||||
Stack string `json:"stack" validate:"max=1048576"`
|
||||
Context interface{} `json:"context"`
|
||||
OccurredAt int64 `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type ReportLogMessageResponse struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type ResetAllSubscribeTokenResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
@@ -2370,10 +2468,6 @@ type ResetTrafficOrderResponse struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
}
|
||||
|
||||
type RecoverySendCodeRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
type ResetUserSubscribeTokenRequest struct {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
}
|
||||
@@ -3000,10 +3094,6 @@ type UpdateOrderStatusRequest struct {
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
}
|
||||
|
||||
type ActivateOrderRequest struct {
|
||||
OrderNo string `json:"order_no" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdatePaymentMethodRequest struct {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
|
||||
Reference in New Issue
Block a user