// Package imgurl 把数据库里存储的图片存储值拼装为对外可访问的完整 URL。 // // 存储约定(见迁移 007):库里只存「七牛 key」(如 runway/abc.jpg)或「本地相对路径」 // (如 /uploads/runway/x.jpg),绝不存完整 URL。这样做两个好处: // 1. 换 CDN 域名只改配置,无需迁移全表; // 2. 做「普通 / 高清」分级时无需从 URL 反向解析 key。 // // 分级与安全(付费墙): // - 高清(hd=true,仅 VIP):私有空间下载签名 URL(base_url/key-StyleVip,如 key-vip),限时有效; // 免费/未登录用户拿不到 VIP 样式 key,即使从别处得到 key,因无签名也会被七牛拒绝(403)。 // - 普通(hd=false,所有人):base_url/key-StyleNormal(如 key-free)的私有空间签名 URL,长过期(供 SSG 静态页与 CDN 缓存)。 // - 本地兜底 /uploads/*:原样返回(前端 toAbs 拼 host)。 // - 外链 http(s):原样返回(历史遗留 / vogue 等,无法套七牛样式,签名无意义)。 // // 七牛私有空间下载签名:对 "base_url/key?e=" 做 HMAC-SHA1(SK),urlsafe base64 后拼 // "AK:sign" 作为 token。签名覆盖完整 path(含 -StyleNormal / -StyleVip 后缀),七牛校验通过才返回资源。 // 前置:bucket 须在七牛控制台设为「私有」,并把 base_url 改为自定义 HTTPS 域名。 package imgurl import ( "crypto/hmac" "crypto/sha1" "encoding/base64" "fmt" "strings" "time" ) // Composer 图片 URL 拼装器。 type Composer struct { // BaseURL 七牛 bucket 绑定域名,如 https://cdn.toom-studio.com(末尾无斜杠)。 BaseURL string // StyleNormal 普通图七牛命名样式名(对应七牛控制台创建的样式),如 "free"。 // 访问即 base_url/key-StyleNormal。务必与控制台样式名一致(如 free 或 free.webp)。 StyleNormal string // StyleVip VIP 高清七牛命名样式名(对应七牛控制台创建的样式),如 "vip"。 // 访问即 base_url/key-StyleVip。VIP 用户走此样式;务必与控制台样式名一致。 StyleVip string // LocalBase 本地兜底图片前缀;通常空——本地 /uploads 路径由前端 toAbs 拼 host。 LocalBase string // AK/SK 七牛 AccessKey/SecretKey,用于私有空间下载签名。未配置时回落为公开 URL。 AK string SK string // SignTTLNormal 普通/压缩图(含命名样式 -free)签名过期秒;长过期供 SSG 静态页与 CDN 缓存。 SignTTLNormal int64 // SignTTLHD VIP 高清原图签名过期秒;短过期使泄漏窗口小。 SignTTLHD int64 } // New 构造 Composer。styleNormal 缺省回落 "free";styleVip 缺省回落 "vip";两个 TTL 缺省回落 31536000 / 3600。 func New(baseURL, styleNormal, styleVip, localBase, ak, sk string, ttlNormal, ttlHD int64) *Composer { if styleNormal == "" { styleNormal = "free" } if styleVip == "" { styleVip = "vip" } if ttlNormal <= 0 { ttlNormal = 31536000 } if ttlHD <= 0 { ttlHD = 3600 } return &Composer{ BaseURL: strings.TrimRight(baseURL, "/"), StyleNormal: styleNormal, StyleVip: styleVip, LocalBase: strings.TrimRight(localBase, "/"), AK: ak, SK: sk, SignTTLNormal: ttlNormal, SignTTLHD: ttlHD, } } // sign 生成七牛私有空间下载签名 URL。 // // rawKey 是待访问的对象路径(可能含 -StyleNormal 后缀)。签名字符串为 // "base_url/rawKey?e=",对其做 HMAC-SHA1(SK) 后 urlsafe base64 得到 token。 // // 无 AK/SK 或 BaseURL 时回落为公开 URL(兼容未配置七牛的开发态:公开空间会忽略 token 直接返回)。 func (c *Composer) sign(rawKey string, ttl int64) string { if c.BaseURL == "" { return rawKey } if c.AK == "" || c.SK == "" { return c.BaseURL + "/" + rawKey } deadline := time.Now().Unix() + ttl toSign := fmt.Sprintf("%s/%s?e=%d", c.BaseURL, rawKey, deadline) mac := hmac.New(sha1.New, []byte(c.SK)) mac.Write([]byte(toSign)) enc := base64.URLEncoding.EncodeToString(mac.Sum(nil)) return fmt.Sprintf("%s&token=%s:%s", toSign, c.AK, enc) } // Compose 把存储值拼成可访问 URL。 // // - stored 为空:返回 "" // - 本地兜底 /uploads/*:原样返回(前端 toAbs 拼 host) // - 已是完整 http(s) URL(历史遗留 / 外链图,如 vogue):hd 原样返回; // normal 尝试从 base_url 还原 key 再套样式签名,还原失败原样返回(外链无法套样式) // - 七牛 key:hd → 原图签名 URL(短过期);normal → 普通图(命名样式)签名 URL(长过期) // // 这样免费/未登录用户永远拿不到「不带签名的原图 URL」;VIP 拿到的是限时签名原图 URL。 func (c *Composer) Compose(stored string, hd bool) string { stored = strings.TrimSpace(stored) if stored == "" { return "" } // 本地兜底路径:由前端拼绝对地址,原样返回。 if strings.HasPrefix(stored, "/uploads/") { return stored } // 已是完整 URL(历史遗留 / 外链图)。 if strings.HasPrefix(stored, "http://") || strings.HasPrefix(stored, "https://") { if hd { return stored } // 同域七牛 URL:剥掉 base_url 还原 key,再套普通样式签名。 if c.BaseURL != "" { if key, ok := strings.CutPrefix(stored, c.BaseURL+"/"); ok { return c.sign(key+"-"+c.StyleNormal, c.SignTTLNormal) } } return stored // 外链图无法套样式,原样返回 } // 七牛 key:VIP 高清(-StyleVip 命名样式)签名(短过期);免费图(-StyleNormal 命名样式)签名(长过期)。 if hd { return c.sign(stored+"-"+c.StyleVip, c.SignTTLHD) } return c.sign(stored+"-"+c.StyleNormal, c.SignTTLNormal) }