package dramaser import ( "context" "crypto/sha256" "errors" "fmt" "strings" "time" "91porn-server/app/appg" "91porn-server/common/db" "91porn-server/models/v/media_buy_record_mod" "91porn-server/models/v/mediacontentmod" "91porn-server/models/v/mediamod" "91porn-server/models/v/usermod" "91porn-server/models/v/walletmod" "go.mongodb.org/mongo-driver/bson/primitive" ) var ( ErrDramaEntitlementRequired = errors.New("active drama entitlement is required") ErrDownloadResourceInvalid = errors.New("short drama episode is unavailable") ) const downloadTicketTTL = 6 * time.Hour type DownloadAuthorizeRequest struct { MediaID primitive.ObjectID `json:"mediaId" binding:"required"` ContentID primitive.ObjectID `json:"contentId" binding:"required"` } type DownloadAuthorizeResponse struct { MediaID primitive.ObjectID `json:"mediaId"` ContentID primitive.ObjectID `json:"contentId"` EpisodeNumber int `json:"episodeNumber"` Name string `json:"name"` Cover string `json:"cover"` DownloadURL string `json:"downloadUrl"` H265DownloadURL string `json:"h265DownloadUrl"` MediaSize int64 `json:"mediaSize"` ExpiresAt time.Time `json:"expiresAt"` RemainingDownloadCount int64 `json:"remainingDownloadCount"` Charged bool `json:"charged"` } func AuthorizeDownload(ctx context.Context, uid uint64, requestID string, req DownloadAuthorizeRequest, now time.Time) (DownloadAuthorizeResponse, error) { requestID = strings.TrimSpace(requestID) if len(requestID) < 1 || len(requestID) > 128 { return DownloadAuthorizeResponse{}, errors.New("X-Request-ID长度必须为1到128") } media, err := mediamod.GetInfo(req.MediaID) if err != nil || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { return DownloadAuthorizeResponse{}, ErrDownloadResourceInvalid } content, err := mediacontentmod.GetInfo(req.ContentID) if err != nil || content.MediaID != req.MediaID || content.MediaType != mediamod.MediaTypeDrama || content.Status != 1 || !content.IsActive || content.IsDelete { return DownloadAuthorizeResponse{}, ErrDownloadResourceInvalid } requestKey := downloadRequestKey(uid, requestID) fingerprint := req.MediaID.Hex() + ":" + req.ContentID.Hex() var wallet *walletmod.Wallet var charged bool for attempt := 0; attempt < 3; attempt++ { err = appg.VideoDB.TransCtx(ctx, func(t *db.MongoTool) error { user, userErr := usermod.FindUserByUIDTrans(t, uid) if userErr != nil { return userErr } bought := false if user != nil && !user.HasLocked && !user.DramaExpire.After(now) { bought, userErr = media_buy_record_mod.IsBuyWithTool(t, uid, req.MediaID, req.ContentID) if userErr != nil { return userErr } } if !canAuthorizeDramaDownload(user, bought, now) { return ErrDramaEntitlementRequired } wallet, charged, userErr = walletmod.AuthorizeDramaDownload(t, uid, requestKey, fingerprint, now) return userErr }) if !errors.Is(err, walletmod.ErrDownloadAuthorizationRace) { break } } if err != nil { return DownloadAuthorizeResponse{}, err } name := strings.TrimSpace(content.Name) if name == "" { name = fmt.Sprintf("第%d集", content.EpisodeNumber) } return DownloadAuthorizeResponse{ MediaID: req.MediaID, ContentID: req.ContentID, EpisodeNumber: content.EpisodeNumber, Name: name, Cover: content.Cover, DownloadURL: content.VideoUrl, H265DownloadURL: appg.H265URLForApp(content.H265Url), MediaSize: content.MediaSize, ExpiresAt: now.Add(downloadTicketTTL), RemainingDownloadCount: wallet.DownloadCount, Charged: charged, }, nil } func downloadRequestKey(uid uint64, requestID string) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s", uid, requestID)))) } func canAuthorizeDramaDownload(user *usermod.User, bought bool, now time.Time) bool { return user != nil && !user.HasLocked && (user.DramaExpire.After(now) || bought) }