package m3u8ticket import ( "reflect" "91porn-server/common" "91porn-server/models/v/vidmod" "github.com/gin-gonic/gin" ) // maxSignDepth 反射兜底遍历响应体的最大深度,防止异常对象图导致过深递归(视频响应本身是浅树)。 const maxSignDepth = 8 // ticketSigner 实现 vidmod.M3u8Signer:持有本次请求的签票上下文(用户、客户端 IP/UA), // 由各响应体的 SignM3u8 回调,对交出的每个 m3u8 地址字段就地签票。 type ticketSigner struct { userID uint64 ip string ua string } // SignM3u8URL 对单个 m3u8 地址字段就地签票;空串或签发失败(绝对地址、非 m3u8 等)时保持原值不变。 func (s *ticketSigner) SignM3u8URL(field *string, playable, preview bool) { if field == nil || *field == "" { return } if signed := BuildSignedURL(s.userID, *field, s.ip, s.ua, playable, preview); signed != "" { *field = signed } } func requestIP(c *gin.Context) string { if c == nil { return "" } return common.GetIP(c) } func requestUA(c *gin.Context) string { if c == nil { return "" } u, err := common.GetUA(c) if err != nil { return "" } return u.UserAgent } // Sign 是 SignResponse 的便捷封装:自动从上下文取登录用户ID,控制器只需在返回前加一行 // m3u8ticket.Sign(ctx, data) 即可对响应体里的视频 m3u8 地址按需签票。 func Sign(c *gin.Context, data interface{}) { if c == nil { return } SignResponse(c, common.TryGetUID(c), data) } // SignResponse 是各视频接口的统一入口:对响应体 data 里的视频 m3u8 地址就地签票(不再限 H5)。 // // data 只要实现 vidmod.M3u8Signable(所有携带视频地址的响应体都已实现)即可零反射签票;另外兼容控制器直接 // 下发的裸切片(recommser.GetAd 等)与 gin.H(RecommendList 等)。少数尚未实现接口的历史/边缘类型走反射兜底, // 保证不漏签。data 为 nil / userID 为 0 / 未配置密钥(BuildSignedURL 返回空)时零副作用,兼容灰度未配置密钥。 func SignResponse(c *gin.Context, userID uint64, data interface{}) { if data == nil || userID == 0 { return } // data 可能是服务出错时返回的类型化 nil 指针((*Resp)(nil)):接口本身非 nil,但对值接收者方法会解引用 // nil 而 panic,先挡掉(等价于旧反射对 nil 指针的 IsNil 保护)。 if rv := reflect.ValueOf(data); rv.Kind() == reflect.Ptr && rv.IsNil() { return } signData(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}, data) } // signData 把 data 分派给对应签票实现:优先零反射的接口/裸切片/map,其余历史类型走反射兜底。 func signData(s vidmod.M3u8Signer, data interface{}) { switch d := data.(type) { case nil: return case vidmod.M3u8Signable: d.SignM3u8(s) case []*vidmod.VideoInfo: vidmod.SignM3u8Infos(s, d) case []*vidmod.VideoInfoResp: vidmod.SignM3u8Resps(s, d) case gin.H: for _, v := range d { signData(s, v) } default: signByReflect(s, reflect.ValueOf(data), 0) } } // signByReflect 是反射兜底:仅用于尚未实现 vidmod.M3u8Signable 的历史/边缘响应类型,语义与旧实现一致—— // 遍历对象图,命中 *VideoInfo/*VideoInfoResp 即签票。正常高频接口都走上面的接口分支,不会到这里。 // 待剩余类型补齐 SignM3u8 后,可整体删除本函数及 reflect 依赖。 func signByReflect(s vidmod.M3u8Signer, rv reflect.Value, depth int) { if depth > maxSignDepth || !rv.IsValid() { return } switch rv.Kind() { case reflect.Ptr: if rv.IsNil() { return } switch v := rv.Interface().(type) { case *vidmod.VideoInfo: v.SignM3u8(s) return case *vidmod.VideoInfoResp: v.SignM3u8(s) return } signByReflect(s, rv.Elem(), depth+1) case reflect.Interface: if rv.IsNil() { return } signByReflect(s, rv.Elem(), depth+1) case reflect.Struct: if rv.CanAddr() { switch v := rv.Addr().Interface().(type) { case *vidmod.VideoInfo: v.SignM3u8(s) return case *vidmod.VideoInfoResp: v.SignM3u8(s) return } } t := rv.Type() for i := 0; i < rv.NumField(); i++ { if t.Field(i).PkgPath != "" { // 跳过未导出字段,reflect 无法安全访问 continue } signByReflect(s, rv.Field(i), depth+1) } case reflect.Slice, reflect.Array: for i := 0; i < rv.Len(); i++ { signByReflect(s, rv.Index(i), depth+1) } case reflect.Map: for _, k := range rv.MapKeys() { signByReflect(s, rv.MapIndex(k), depth+1) } } } // SignURL 供只有单个 m3u8 地址字段的响应(官网视频、媒体内容等)复用。 // 不再限制 H5:已配置密钥时改写,未配置(BuildSignedURL 返回空)则原样返回,兼容灰度未配置密钥的情况。 func SignURL(c *gin.Context, userID uint64, field *string, playable, preview bool) { if field == nil || *field == "" || userID == 0 { return } (&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}).SignM3u8URL(field, playable, preview) }