feat:修改头像bug

This commit is contained in:
zerosaturation 2026-06-02 21:10:35 +08:00
parent 0f1cbb39c1
commit edadb0e082
8 changed files with 568 additions and 14 deletions

BIN
backend/cleanup-orphan-avatars Executable file

Binary file not shown.

View File

@ -0,0 +1,241 @@
// Package main 提供 OSS 头像孤儿文件清理工具。
//
// 用法:
//
// go run ./gateway/cmd/cleanup-orphan-avatars \
// --older-than-days 7 # 只看 7 天前的对象
// --limit 100 # 一次最多处理 100 个
// --delete # 真正删除;缺省为 dry-run只打印不打
//
// DB / OSS 配置走环境变量(与 userService 一致):
// - DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME
// - OSS_REGION, OSS_BUCKET_NAME, OSS_STS_ROLE_ARN,
// OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_AVATAR_DIR
//
// 默认扫描前缀 = $OSS_AVATAR_DIR"avatar/"),用 --prefix 覆盖。
package main
import (
"flag"
"fmt"
"log"
"os"
"strconv"
"time"
"github.com/topfans/backend/gateway/config"
"github.com/topfans/backend/gateway/pkg/ossutil"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
var (
prefix = flag.String("prefix", "", "OSS 前缀,默认 = cfg.OSS.AvatarDir")
olderThanDays = flag.Int("older-than-days", 0, "仅标记 mtime 早于 N 天的对象0 = 不过滤)")
limit = flag.Int("limit", 0, "最多处理多少个孤儿0 = 不限)")
deleteMode = flag.Bool("delete", false, "真正删除 OSS 对象;缺省为 dry-run")
batchSize = flag.Int("batch-size", 100, "每处理 N 个对象打印一次进度")
dbHost = flag.String("db-host", getEnv("DB_HOST", "localhost"), "DB host")
dbPort = flag.Int("db-port", getEnvInt("DB_PORT", 5432), "DB port")
dbUser = flag.String("db-user", getEnv("DB_USER", "postgres"), "DB user")
dbPassword = flag.String("db-password", getEnv("DB_PASSWORD", ""), "DB password")
dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "DB name")
)
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func getEnvInt(key string, fallback int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return fallback
}
func main() {
flag.Parse()
mode := "DRY-RUN"
if *deleteMode {
mode = "DELETE"
}
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Printf("=== cleanup-orphan-avatars [%s] ===", mode)
cfg := config.Load()
scanPrefix := *prefix
if scanPrefix == "" {
scanPrefix = cfg.OSS.AvatarDir
}
if scanPrefix == "" {
log.Fatalf("--prefix 为空且 cfg.OSS.AvatarDir 未设置,请显式传 --prefix")
}
log.Printf("scan prefix: %s", scanPrefix)
if *olderThanDays > 0 {
log.Printf("age filter: only mtime < %d days ago", *olderThanDays)
}
if *limit > 0 {
log.Printf("limit: max %d orphans to process", *limit)
}
if err := database.Init(database.Config{
Host: *dbHost,
Port: *dbPort,
User: *dbUser,
Password: *dbPassword,
DBName: *dbName,
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}); err != nil {
log.Fatalf("connect DB failed: %v", err)
}
defer func() { _ = database.Close() }()
db := database.GetDB()
referenced, skipped := collectReferencedKeys(db, &cfg.OSS)
log.Printf("referenced keys: %d (含 fan_profiles),解析失败/外链跳过: %d", referenced.cardinality(), skipped)
objs, err := ossutil.List(&cfg.OSS, scanPrefix, 0)
if err != nil {
log.Fatalf("OSS List failed: %v", err)
}
log.Printf("OSS objects under %q: %d", scanPrefix, len(objs))
cutoff := time.Time{}
if *olderThanDays > 0 {
cutoff = time.Now().Add(-time.Duration(*olderThanDays) * 24 * time.Hour)
}
var (
orphans []ossutil.ObjectInfo
skippedNew int
skippedRef int
)
for _, obj := range objs {
if referenced.contains(obj.Key) {
skippedRef++
continue
}
if !cutoff.IsZero() && obj.LastModified.After(cutoff) {
skippedNew++
continue
}
orphans = append(orphans, obj)
if *limit > 0 && len(orphans) >= *limit {
break
}
}
log.Printf("candidates: %d (referenced skip: %d, age skip: %d)", len(orphans), skippedRef, skippedNew)
if len(orphans) == 0 {
log.Printf("nothing to do. bye.")
return
}
var totalSize int64
for _, o := range orphans {
totalSize += o.Size
}
log.Printf("would free: %d files, %s", len(orphans), humanBytes(totalSize))
processed := 0
for _, o := range orphans {
processed++
if *deleteMode {
if err := ossutil.Delete(&cfg.OSS, o.Key); err != nil {
log.Printf("[FAIL] delete %s: %v", o.Key, err)
continue
}
log.Printf("[DEL ] %s size=%s mtime=%s", o.Key, humanBytes(o.Size), o.LastModified.Format(time.RFC3339))
} else {
log.Printf("[SKIP] %s size=%s mtime=%s", o.Key, humanBytes(o.Size), o.LastModified.Format(time.RFC3339))
}
if processed%*batchSize == 0 {
log.Printf("--- progress: %d / %d ---", processed, len(orphans))
}
}
log.Printf("done. processed=%d", processed)
}
// referencedKeySet 记录"正在被数据库引用的 OSS key"。
// 用 map[string]struct{} 而不是 sync.Map —— 工具是单 goroutine。
type referencedKeySet struct {
m map[string]struct{}
}
func (s *referencedKeySet) add(k string) {
if s.m == nil {
s.m = make(map[string]struct{})
}
s.m[k] = struct{}{}
}
func (s *referencedKeySet) contains(k string) bool {
_, ok := s.m[k]
return ok
}
func (s *referencedKeySet) cardinality() int {
return len(s.m)
}
// collectReferencedKeys 从 users / fan_profiles 收集所有头像 URL
// 通过 cfg 校验 host 后转成 OSS key 入集合。
// 返回:(set, 解析失败/外链 数量)。
func collectReferencedKeys(db *gorm.DB, cfg *config.OSSConfig) (referencedKeySet, int) {
set := referencedKeySet{}
skipped := 0
var userURLs []string
if err := db.Model(&models.User{}).
Where("avatar_url IS NOT NULL AND avatar_url <> '' AND deleted_at IS NULL").
Pluck("avatar_url", &userURLs).Error; err != nil {
log.Fatalf("query users.avatar_url failed: %v", err)
}
for _, u := range userURLs {
k, err := ossutil.ExtractKeyFromPublicURL(cfg, u)
if err != nil {
skipped++
continue
}
set.add(k)
}
var profileURLs []string
if err := db.Model(&models.FanProfile{}).
Where("avatar_url IS NOT NULL AND avatar_url <> ''").
Pluck("avatar_url", &profileURLs).Error; err != nil {
log.Fatalf("query fan_profiles.avatar_url failed: %v", err)
}
for _, u := range profileURLs {
k, err := ossutil.ExtractKeyFromPublicURL(cfg, u)
if err != nil {
skipped++
continue
}
set.add(k)
}
return set, skipped
}
func humanBytes(n int64) string {
const k = 1024
if n < k {
return fmt.Sprintf("%dB", n)
}
if n < k*k {
return fmt.Sprintf("%.1fKB", float64(n)/k)
}
if n < k*k*k {
return fmt.Sprintf("%.1fMB", float64(n)/(k*k))
}
return fmt.Sprintf("%.1fGB", float64(n)/(k*k*k))
}

View File

@ -817,7 +817,7 @@ func (ctrl *AssetController) GetAssetStatus(c *gin.Context) {
// @Security BearerAuth
// @Param type query string false "上传类型: avatar/asset默认asset"
// @Param order_id query string false "订单ID可选。不传则后端生成并在响应中返回用于后续铸造全流程唯一标识"
// @Success 200 {object} response.Response{data=object{order_id=string,policy=string,security_token=string,x_oss_signature_version=string,x_oss_credential=string,x_oss_date=string,signature=string,host=string,dir=string,expire_time=int}} "成功返回上传签名信息"
// @Success 200 {object} response.Response{data=object{order_id=string,policy=string,security_token=string,x_oss_signature_version=string,x_oss_credential=string,x_oss_date=string,signature=string,host=string,dir=string,key=string,expire_time=int}} "成功返回上传签名信息"
// @Failure 400 {object} response.Response "参数错误"
// @Failure 401 {object} response.Response "未授权"
// @Failure 500 {object} response.Response "OSS配置错误或生成签名失败"
@ -898,13 +898,13 @@ func (ctrl *AssetController) GetOSSUploadSignature(c *gin.Context) {
// GetPublicOSSUploadSignature 公开版 OSS 上传签名(用于注册等未登录场景的头像上传)
// @Summary 获取公开 OSS 上传签名(注册流程用)
// @Description 用于注册等未登录场景下上传头像,无需鉴权。
// @Description 限定 key 必须以 avatar/register-pending/{key}/ 开头key 由前端传入(如手机号),并要求 key 与 scene+key 一致
// @Description 后端生成唯一完整 keypolicy 锁到该 key前端只能写到指定路径key 形如 avatar/register-pending/{key}/avatar_<uuid>.png
// @Tags assets
// @Accept json
// @Produce json
// @Param scene query string true "场景,目前固定为 register"
// @Param key query string true "前端传入的命名空间(注册时传 mobile仅允许 [a-zA-Z0-9_-],最长 32 字符"
// @Success 200 {object} response.Response "成功返回上传签名信息"
// @Success 200 {object} response.Response{data=object{policy=string,security_token=string,x_oss_signature_version=string,x_oss_credential=string,x_oss_date=string,signature=string,host=string,dir=string,key=string,expire_time=int}} "成功返回上传签名信息"
// @Failure 400 {object} response.Response "参数错误"
// @Failure 500 {object} response.Response "OSS配置错误或生成签名失败"
// @Router /api/v1/public/oss/upload-signature [get]
@ -927,8 +927,10 @@ func (ctrl *AssetController) GetPublicOSSUploadSignature(c *gin.Context) {
}
// 强制使用 avatar 目录,并将 key 限定到 avatar/register-pending/{key}/ 子目录
// 后端生成唯一文件名policy 用 eq 锁到完整 key
uploadDir := fmt.Sprintf("%sregister-pending/%s/", cfg.OSS.AvatarDir, key)
token, err := ctrl.generateOSSPolicyTokenWithDir(cfg.OSS, uploadDir)
uniqueFilename := fmt.Sprintf("avatar_%s.png", uuid.New().String())
token, err := ctrl.generateOSSPolicyTokenWithKey(cfg.OSS, uploadDir+uniqueFilename)
if err != nil {
logger.Logger.Error("Generate public OSS signature failed",
zap.Error(err),
@ -979,9 +981,108 @@ func (ctrl *AssetController) generateOSSPolicyToken(
if userID != nil && starID != nil {
uploadDir = fmt.Sprintf("%s%d/%d/", baseDir, userID, starID)
}
// 头像上传:后端生成唯一 key避免
// 1) URL 字符串相同导致 webview/CDN 命中老图缓存
// 2) 并发上传时两路相互覆盖
// 3) 上传中途失败留下半截文件
// policy 用 eq 锁到该 key前端只能写到指定路径
if uploadType == "avatar" {
uniqueFilename := fmt.Sprintf("avatar_%s.png", uuid.New().String())
return ctrl.generateOSSPolicyTokenWithKey(ossConfig, uploadDir+uniqueFilename)
}
return ctrl.generateOSSPolicyTokenWithDir(ossConfig, uploadDir)
}
// dirFromKey 从完整 OSS key 提取目录(含末尾 /
func dirFromKey(key string) string {
idx := strings.LastIndex(key, "/")
if idx < 0 {
return ""
}
return key[:idx+1]
}
// generateOSSPolicyTokenWithKey 按显式完整 keyOSS 对象路径)生成 OSS 上传策略和签名
// 与 WithDir 区别policy 用 eq 锁到该 key更严返回值带 key 供前端直接使用
// 适用场景:头像上传——后端控制完整 keyURL 天然每次唯一
func (ctrl *AssetController) generateOSSPolicyTokenWithKey(
ossConfig config.OSSConfig,
uploadKey string,
) (map[string]interface{}, error) {
// 1. 创建 STS 凭证提供器
credConfig := new(credentials.Config).
SetType("ram_role_arn").
SetAccessKeyId(ossConfig.AccessKeyID).
SetAccessKeySecret(ossConfig.AccessKeySecret).
SetRoleArn(ossConfig.RoleArn).
SetRoleSessionName("topfans-upload-session").
SetPolicy("").
SetRoleSessionExpiration(ossConfig.TokenExpireTime)
provider, err := credentials.NewCredential(credConfig)
if err != nil {
return nil, fmt.Errorf("创建凭证提供器失败: %w", err)
}
// 2. 获取临时凭证
cred, err := provider.GetCredential()
if err != nil {
return nil, fmt.Errorf("获取临时凭证失败: %w", err)
}
// 3. 构建 Policy
utcTime := time.Now().UTC()
date := utcTime.Format("20060102")
expiration := utcTime.Add(time.Duration(ossConfig.TokenExpireTime) * time.Second)
// 构建 Policy 条件
conditions := []interface{}{
map[string]string{"bucket": ossConfig.BucketName},
// 限制 key 必须等于指定完整 key更严前端只能写到该路径
[]interface{}{"eq", "$key", uploadKey},
map[string]string{"x-oss-signature-version": "OSS4-HMAC-SHA256"},
map[string]string{"x-oss-credential": fmt.Sprintf("%s/%s/%s/%s/aliyun_v4_request",
*cred.AccessKeyId, date, ossConfig.Region, "oss")},
map[string]string{"x-oss-date": utcTime.Format("20060102T150405Z")},
map[string]string{"x-oss-security-token": *cred.SecurityToken},
}
policyMap := map[string]interface{}{
"expiration": expiration.Format("2006-01-02T15:04:05.000Z"),
"conditions": conditions,
}
// 4. 生成 Policy 的 Base64 编码
policyJSON, err := json.Marshal(policyMap)
if err != nil {
return nil, fmt.Errorf("序列化 policy 失败: %w", err)
}
policyBase64 := base64.StdEncoding.EncodeToString(policyJSON)
// 5. 计算签名
signingKey := buildSigningKey(*cred.AccessKeySecret, date, ossConfig.Region, "oss")
signature := calculateSignature(signingKey, policyBase64)
// 6. 构建返回数据
host := fmt.Sprintf("https://%s.oss-%s.aliyuncs.com", ossConfig.BucketName, ossConfig.Region)
return map[string]interface{}{
"policy": policyBase64,
"security_token": *cred.SecurityToken,
"x_oss_signature_version": "OSS4-HMAC-SHA256",
"x_oss_credential": fmt.Sprintf("%s/%s/%s/%s/aliyun_v4_request",
*cred.AccessKeyId, date, ossConfig.Region, "oss"),
"x_oss_date": utcTime.Format("20060102T150405Z"),
"signature": signature,
"host": host,
"dir": dirFromKey(uploadKey),
"key": uploadKey,
"expire_time": expiration.Unix(),
}, nil
}
// generateOSSPolicyTokenWithDir 按显式目录生成 OSS 上传策略和签名
// (用于公开场景等需要把 key 限定到非 userID/starID 命名空间的场景)
func (ctrl *AssetController) generateOSSPolicyTokenWithDir(

View File

@ -9,7 +9,9 @@ import (
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/gin-gonic/gin"
"github.com/topfans/backend/gateway/config"
"github.com/topfans/backend/gateway/dto"
"github.com/topfans/backend/gateway/pkg/ossutil"
"github.com/topfans/backend/gateway/pkg/response"
"github.com/topfans/backend/pkg/logger"
pb "github.com/topfans/backend/pkg/proto/user"
@ -332,13 +334,14 @@ func (ctrl *UserController) UpdatePassword(c *gin.Context) {
// UpdateAvatar 更新用户头像
// @Summary 更新用户头像
// @Description 更新当前用户的头像URL
// @Description 更新当前用户的头像URL。提交前会校验 OSS 对象真实存在,避免 user 表指向不存在的对象。
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body pb.UpdateAvatarRequest true "更新头像请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response "头像URL无效或对象不存在"
// @Router /api/v1/me/avatar [put]
func (ctrl *UserController) UpdateAvatar(c *gin.Context) {
var req pb.UpdateAvatarRequest
@ -360,6 +363,27 @@ func (ctrl *UserController) UpdateAvatar(c *gin.Context) {
return
}
// 提取 OSS key 并校验对象真实存在,避免 user 表写入一个尚未落地的 URL
cfg := config.Load()
ossKey, err := ossutil.ExtractKeyFromPublicURL(&cfg.OSS, req.AvatarUrl)
if err != nil {
response.BadRequest(c, "头像URL格式不合法: "+err.Error())
return
}
exist, err := ossutil.Head(&cfg.OSS, ossKey)
if err != nil {
logger.Logger.Error("OSS HEAD failed during UpdateAvatar",
zap.String("oss_key", ossKey),
zap.Error(err),
)
response.Error(c, http.StatusInternalServerError, "校验头像对象失败")
return
}
if !exist {
response.BadRequest(c, "头像对象在 OSS 上不存在,请先完成上传")
return
}
userID, _ := c.Get("user_id")
starID, _ := c.Get("star_id")
@ -389,7 +413,6 @@ func (ctrl *UserController) UpdateAvatar(c *gin.Context) {
"avatar_url": resp.AvatarUrl,
})
}
// AddIdentity 添加粉丝身份
// @Summary 添加粉丝身份
// @Description 为当前用户添加一个新的明星粉丝身份

View File

@ -0,0 +1,185 @@
// Package ossutil 提供 gateway 侧对 OSS 的轻量操作封装。
//
// 涉及 STS 凭证的操作建议在请求粒度调用即可(不要跨请求复用 STS
// STS token 过期会导致后续调用 403。每次调用都换一次凭证延迟成本可接受。
package ossutil
import (
"fmt"
"net/url"
"strings"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/aliyun/credentials-go/credentials"
"github.com/topfans/backend/gateway/config"
"github.com/topfans/backend/pkg/logger"
"go.uber.org/zap"
)
// ObjectInfo 描述 OSS 对象的关键元数据,供调用方做"按时间过滤孤儿"等判断。
type ObjectInfo struct {
Key string
Size int64
LastModified time.Time
}
// ExtractKeyFromPublicURL 从完整 OSS 公开 URL 提取对象 key。
// 仅接受本 bucket 域名https://<bucket>.oss-<region>.aliyuncs.com/<key>
// 防止调用方写入指向其他 bucket / 公网图片的 URL。
// 任何 query string缓存击穿用都会被忽略只取 path 里的 key。
func ExtractKeyFromPublicURL(cfg *config.OSSConfig, rawURL string) (string, error) {
if cfg == nil {
return "", fmt.Errorf("ossutil: cfg 不能为空")
}
if rawURL == "" {
return "", fmt.Errorf("URL 不能为空")
}
u, err := url.Parse(rawURL)
if err != nil {
return "", fmt.Errorf("URL 解析失败: %w", err)
}
if u.Scheme != "https" && u.Scheme != "http" {
return "", fmt.Errorf("仅支持 http(s) URL")
}
expectedHost := fmt.Sprintf("%s.oss-%s.aliyuncs.com", cfg.BucketName, cfg.Region)
if u.Host != expectedHost {
return "", fmt.Errorf("URL host 必须是本 bucket 域名 %s", expectedHost)
}
key := strings.TrimPrefix(u.Path, "/")
if key == "" {
return "", fmt.Errorf("URL 必须包含对象 key")
}
return key, nil
}
// newClient 创建一个使用 STS 临时凭证的 OSS 客户端。
// 调用方负责 client 生命周期;本包内 helper 各自建一个,不跨调用复用。
func newClient(cfg *config.OSSConfig) (*oss.Client, *oss.Bucket, func(), error) {
credConfig := new(credentials.Config).
SetType("ram_role_arn").
SetAccessKeyId(cfg.AccessKeyID).
SetAccessKeySecret(cfg.AccessKeySecret).
SetRoleArn(cfg.RoleArn).
SetRoleSessionName("topfans-ossutil").
SetPolicy("").
SetRoleSessionExpiration(cfg.TokenExpireTime)
provider, err := credentials.NewCredential(credConfig)
if err != nil {
return nil, nil, nil, fmt.Errorf("创建凭证提供器失败: %w", err)
}
cred, err := provider.GetCredential()
if err != nil {
return nil, nil, nil, fmt.Errorf("获取临时凭证失败: %w", err)
}
endpoint := fmt.Sprintf("https://oss-%s.aliyuncs.com", cfg.Region)
client, err := oss.New(endpoint, *cred.AccessKeyId, *cred.AccessKeySecret,
oss.SecurityToken(*cred.SecurityToken))
if err != nil {
return nil, nil, nil, fmt.Errorf("创建OSS客户端失败: %w", err)
}
bucket, err := client.Bucket(cfg.BucketName)
if err != nil {
return nil, nil, nil, fmt.Errorf("获取Bucket失败: %w", err)
}
cleanup := func() {
// OSS SDK 当前没有需要显式关闭的连接;留作未来扩展
}
return client, bucket, cleanup, nil
}
// Head 检查指定 key 的对象是否存在。
// 返回 (true, nil) 表示存在;(false, nil) 表示不存在但调用成功;
// 返回 (_, err) 表示调用本身失败(网络/权限等)。
func Head(cfg *config.OSSConfig, key string) (bool, error) {
if cfg == nil {
return false, fmt.Errorf("ossutil: cfg 不能为空")
}
if key == "" {
return false, fmt.Errorf("ossutil: key 不能为空")
}
_, bucket, cleanup, err := newClient(cfg)
if err != nil {
return false, err
}
defer cleanup()
exist, err := bucket.IsObjectExist(key)
if err != nil {
return false, fmt.Errorf("OSS HEAD 失败: %w", err)
}
return exist, nil
}
// Delete 删除指定 key 的对象。
func Delete(cfg *config.OSSConfig, key string) error {
if cfg == nil {
return fmt.Errorf("ossutil: cfg 不能为空")
}
if key == "" {
return fmt.Errorf("ossutil: key 不能为空")
}
_, bucket, cleanup, err := newClient(cfg)
if err != nil {
return err
}
defer cleanup()
if err := bucket.DeleteObject(key); err != nil {
logger.Logger.Warn("OSS DeleteObject 失败", zap.String("key", key), zap.Error(err))
return fmt.Errorf("OSS DELETE 失败: %w", err)
}
return nil
}
// List 列出指定前缀下的所有对象元数据(不含公共前缀本身)。
// maxKeys 传 <=0 时使用 OSS 默认分页行为,每次最多 1000 个,内部循环拉完。
func List(cfg *config.OSSConfig, prefix string, maxKeys int) ([]ObjectInfo, error) {
if cfg == nil {
return nil, fmt.Errorf("ossutil: cfg 不能为空")
}
_, bucket, cleanup, err := newClient(cfg)
if err != nil {
return nil, err
}
defer cleanup()
var objs []ObjectInfo
var continuationToken string
for {
opts := []oss.Option{}
if prefix != "" {
opts = append(opts, oss.Prefix(prefix))
}
if continuationToken != "" {
opts = append(opts, oss.ContinuationToken(continuationToken))
}
if maxKeys > 0 {
// 限制单次返回数量OSS 单次最大 1000
if maxKeys > 1000 {
maxKeys = 1000
}
opts = append(opts, oss.MaxKeys(maxKeys))
}
result, err := bucket.ListObjects(opts...)
if err != nil {
return nil, fmt.Errorf("OSS ListObjects 失败: %w", err)
}
for _, obj := range result.Objects {
objs = append(objs, ObjectInfo{
Key: obj.Key,
Size: obj.Size,
LastModified: obj.LastModified,
})
}
if !result.IsTruncated {
break
}
continuationToken = result.NextMarker
}
return objs, nil
}

View File

@ -69,7 +69,7 @@ const navItems = [
iconHeight: '120rpx'
},
{
name: '广场',
name: '我的',
icon: '/static/icon/square.png',
angle: 57, //
path: '/pages/profile/myWorks'

View File

@ -1100,6 +1100,8 @@ const closeAvatarModal = () => {
// UI
const handleAvatarUpdateSuccess = async (newAvatarUrl) => {
// key avatar_<uuid>.png
// URL <image> ?t=
// 1.
userAvatarUrl.value = newAvatarUrl;
const userStr = uni.getStorageSync('user');
@ -1180,13 +1182,14 @@ const uploadAvatarToOss = async (filePath) => {
}
console.log(signRes)
// 2. FormDataOSS
// :avatar.png
// keywebview
const ossKey = signRes.data.key
uni.uploadFile({
url: signRes.data.host,
filePath: filePath,
name: 'file',
formData: {
key: signRes.data.dir + 'avatar.png',
key: ossKey,
policy: signRes.data.policy,
success_action_status: '200',
'x-oss-credential': signRes.data.x_oss_credential,
@ -1198,8 +1201,8 @@ const uploadAvatarToOss = async (filePath) => {
success: async (uploadRes) => {
try {
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
// 3. URL
const avatarUrl = `${signRes.data.host}/${signRes.data.dir}avatar.png`;
// 3. URL key
const avatarUrl = `${signRes.data.host}/${ossKey}`;
// 4.
if (userAvatarUrl.value) {

View File

@ -178,13 +178,14 @@ const uploadAvatarToOss = async (filePath) => {
throw new Error(signRes.message || '获取签名失败');
}
// 2. OSS avatar.png policy
// 2. OSS key /
const ossKey = signRes.data.key
uni.uploadFile({
url: signRes.data.host,
filePath: filePath,
name: 'file',
formData: {
key: signRes.data.dir + 'avatar.png',
key: ossKey,
policy: signRes.data.policy,
success_action_status: '200',
'x-oss-credential': signRes.data.x_oss_credential,
@ -196,7 +197,7 @@ const uploadAvatarToOss = async (filePath) => {
success: (uploadRes) => {
uni.hideLoading();
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
userAvatarUrl.value = `${signRes.data.host}/${signRes.data.dir}avatar.png`;
userAvatarUrl.value = `${signRes.data.host}/${ossKey}`;
uni.showToast({
title: '头像已选择',
icon: 'success'