This commit is contained in:
toom1996
2026-09-16 00:56:15 +08:00
parent 5e4803b97f
commit 7dd3fda73c
10 changed files with 184 additions and 54 deletions

View File

@ -89,7 +89,9 @@ func (h *ArticleHandler) Detail(c *gin.Context) {
response.Error(c, http.StatusBadRequest, "缺少文章 id")
return
}
_, numeric, err := hashid.DecodeTyped(raw)
// 类型由路由提供(/runway-looks 即 runway)。类型已揉进密码:非 runway 编码的串
// 在此解出的是错误主键,查库自然 404,从而强制前端按类型化路由分流(无需靠前缀嗅探)。
numeric, err := hashid.DecodeWithType(raw, hashid.TypeRunway)
if err != nil {
response.Error(c, http.StatusNotFound, "文章不存在")
return
@ -97,8 +99,17 @@ func (h *ArticleHandler) Detail(c *gin.Context) {
detail, err := h.articles.Detail(c.Request.Context(), strconv.FormatUint(uint64(numeric), 10), parseLocale(c))
if err != nil {
fail(c, err)
return
// 向后兼容:迁移前收藏/历史/旧外链可能仍带 r= 前缀(旧 EncodeTyped)。
// 主线按类型化密码解码不到实体时,按旧编码再解一次回退(仅当旧类型确为 runway)。
if ltyp, lnum, lerr := hashid.DecodeTyped(raw); lerr == nil && ltyp == hashid.TypeRunway {
if d2, e2 := h.articles.Detail(c.Request.Context(), strconv.FormatUint(uint64(lnum), 10), parseLocale(c)); e2 == nil {
detail, err = d2, nil
}
}
if err != nil {
fail(c, err)
return
}
}
// 未登录:主图只返回预览上限(PreviewLimit)张并打 preview=true,前端据此展示
@ -112,9 +123,9 @@ func (h *ArticleHandler) Detail(c *gin.Context) {
preview = true
}
// 已登录:把当前用户对该图集(gallery 级)与每张图片(image 级,含细节图)的收藏态
// 随详情返回,前端无需再单独调 /me/favorites/checks。gallery uid(r=)与图片 id(i=)
// 前缀不同,混传 FilterExisting 不影响查询;匿名请求不查(保持公开接口无用户态、可缓存)。
// 已登录:把当前用户对该图集(gallery 级 TypeRunway)与每张图片(image 级 TypeRunwayImage,含细节图)的收藏态
// 随详情返回,前端无需再单独调 /me/favorites/checks。gallery uid 与图片 id 因类型进密码、串值不同,
// 混传 FilterExisting 不影响查询;匿名请求不查(保持公开接口无用户态、可缓存)。
if uid, ok := middleware.UserIDFrom(c); ok {
ids := make([]string, 0, len(detail.Images)*2+1)
ids = append(ids, detail.UID)

View File

@ -258,10 +258,10 @@ func (h *BackstageHandler) Runways(c *gin.Context) {
}
// RunwayDetail 走秀详情:展示完整图集,复用 ArticleService.Detail。
// 路由 :id 是带 "r" 前缀的 hashid,需先解码成数字主键再查库(与公开接口 article_handler 保持一致)。
// 路由 :id 是 runway 类型化编码(无前缀,类型进密码),按 TypeRunway 解码(与公开接口 article_handler 一致)。
func (h *BackstageHandler) RunwayDetail(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeRunway)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "走秀不存在或已删除: " + raw})
return
@ -311,10 +311,10 @@ func (h *BackstageHandler) StreetSnaps(c *gin.Context) {
}
// StreetSnapDetail 街拍详情:展示完整图集,复用 StreetSnapService.Detail。
// 路由 :id 是带 "s" 前缀的 hashid,需先解码成数字主键再查库(与公开接口 street_snap_handler 保持一致)。
// 路由 :id 是 snap 类型化编码(无前缀,类型进密码),按 TypeSnap 解码(与公开接口 street_snap_handler 一致)。
func (h *BackstageHandler) StreetSnapDetail(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "街拍不存在或已删除: " + raw})
return
@ -590,7 +590,7 @@ func usernameString(v any) string {
// RunwayEdit 渲染走秀编辑表单(预填现有字段 + 图片管理)。
func (h *BackstageHandler) RunwayEdit(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeRunway)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "走秀不存在或已删除: " + raw})
return
@ -619,7 +619,7 @@ func (h *BackstageHandler) RunwayEdit(c *gin.Context) {
// RunwayUpdate 应用走秀字段编辑。
func (h *BackstageHandler) RunwayUpdate(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeRunway)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "走秀不存在或已删除: " + raw})
return
@ -650,7 +650,7 @@ func (h *BackstageHandler) RunwayUpdate(c *gin.Context) {
// RunwayImageDelete 软删单张走秀图片。
func (h *BackstageHandler) RunwayImageDelete(c *gin.Context) {
raw := c.Param("id")
if _, _, err := hashid.DecodeTyped(raw); err != nil {
if _, err := hashid.DecodeWithType(raw, hashid.TypeRunway); err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "走秀不存在或已删除: " + raw})
return
}
@ -669,7 +669,7 @@ func (h *BackstageHandler) RunwayImageDelete(c *gin.Context) {
// RunwaySetCover 把某张图片设为封面。
func (h *BackstageHandler) RunwaySetCover(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeRunway)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "走秀不存在或已删除: " + raw})
return
@ -689,7 +689,7 @@ func (h *BackstageHandler) RunwaySetCover(c *gin.Context) {
// RunwayToggleDeleted 上下架(下架=is_deleted=1 对外不可见)。
func (h *BackstageHandler) RunwayToggleDeleted(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeRunway)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "走秀不存在或已删除: " + raw})
return
@ -710,7 +710,7 @@ func (h *BackstageHandler) RunwayToggleDeleted(c *gin.Context) {
// StreetSnapEdit 渲染街拍编辑表单。
func (h *BackstageHandler) StreetSnapEdit(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "街拍不存在或已删除: " + raw})
return
@ -731,7 +731,7 @@ func (h *BackstageHandler) StreetSnapEdit(c *gin.Context) {
// StreetSnapUpdate 应用街拍字段编辑。
func (h *BackstageHandler) StreetSnapUpdate(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "街拍不存在或已删除: " + raw})
return
@ -757,7 +757,7 @@ func (h *BackstageHandler) StreetSnapUpdate(c *gin.Context) {
// StreetSnapImageDelete 软删单张街拍图片。
func (h *BackstageHandler) StreetSnapImageDelete(c *gin.Context) {
raw := c.Param("id")
if _, _, err := hashid.DecodeTyped(raw); err != nil {
if _, err := hashid.DecodeWithType(raw, hashid.TypeSnap); err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "街拍不存在或已删除: " + raw})
return
}
@ -776,7 +776,7 @@ func (h *BackstageHandler) StreetSnapImageDelete(c *gin.Context) {
// StreetSnapSetCover 把某张图片设为封面。
func (h *BackstageHandler) StreetSnapSetCover(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "街拍不存在或已删除: " + raw})
return
@ -796,7 +796,7 @@ func (h *BackstageHandler) StreetSnapSetCover(c *gin.Context) {
// StreetSnapToggleDeleted 上下架。
func (h *BackstageHandler) StreetSnapToggleDeleted(c *gin.Context) {
raw := c.Param("id")
_, numeric, err := hashid.DecodeTyped(raw)
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
if err != nil {
renderHTML(c, http.StatusNotFound, tplError, gin.H{"Error": "街拍不存在或已删除: " + raw})
return

View File

@ -74,7 +74,9 @@ func (h *StreetSnapHandler) Detail(c *gin.Context) {
response.Error(c, http.StatusBadRequest, "缺少街拍 id")
return
}
_, numeric, err := hashid.DecodeTyped(raw)
// 类型由路由提供(/street-snaps 即 snap)。类型已揉进密码:非 snap 编码的串
// 在此解出的是错误主键,查库自然 404,从而强制前端按类型化路由分流(无需靠前缀嗅探)。
numeric, err := hashid.DecodeWithType(raw, hashid.TypeSnap)
if err != nil {
response.Error(c, http.StatusNotFound, "街拍不存在")
return
@ -82,8 +84,17 @@ func (h *StreetSnapHandler) Detail(c *gin.Context) {
detail, err := h.svc.Detail(c.Request.Context(), strconv.FormatUint(uint64(numeric), 10))
if err != nil {
fail(c, err)
return
// 向后兼容:迁移前收藏/历史/旧外链可能仍带 s= 前缀(旧 EncodeTyped)。
// 主线按类型化密码解码不到实体时,按旧编码再解一次回退(仅当旧类型确为 snap)。
if ltyp, lnum, lerr := hashid.DecodeTyped(raw); lerr == nil && ltyp == hashid.TypeSnap {
if d2, e2 := h.svc.Detail(c.Request.Context(), strconv.FormatUint(uint64(lnum), 10)); e2 == nil {
detail, err = d2, nil
}
}
if err != nil {
fail(c, err)
return
}
}
// 未登录:图片集仅返回预览上限(PreviewLimit)张,并打 preview=true,
@ -96,7 +107,7 @@ func (h *StreetSnapHandler) Detail(c *gin.Context) {
preview = true
}
// 已登录:图集(gallery 级 s=)与每张图片(image 级 j=)收藏态随详情返回,
// 已登录:图集(gallery 级 TypeSnap)与每张图片(image 级 TypeSnapImage)收藏态随详情返回,
// 前端无需再单独调 /me/favorites/checks。
if uid, ok := middleware.UserIDFrom(c); ok {
ids := make([]string, 0, len(detail.Images)+1)