package productser import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" "time" "91porn-server/app/appg" "91porn-server/app/service/mediacontentser" "91porn-server/common/db" "91porn-server/common/log" redisutil "91porn-server/common/redis" "91porn-server/common/stderr" "91porn-server/middleware/ua" "91porn-server/models/v/media_buy_record_mod" "91porn-server/models/v/mediacontentmod" "91porn-server/models/v/mediamod" "91porn-server/models/v/txnmod" "91porn-server/models/v/usermod" "91porn-server/models/v/walletmod" "go.mongodb.org/mongo-driver/bson/primitive" ) const ( dramaBuyRequestTTL = 24 * time.Hour dramaBuyLockTTL = 2 * time.Minute ) var errDramaAlreadyBought = errors.New("drama episode already bought") var releaseDramaBuyLockScript = redisutil.NewScript(` if redis.call('GET', KEYS[1]) ~= ARGV[1] then return 0 end return redis.call('DEL', KEYS[1]) `) type BuyDramaEpisodeResponse struct { OrderID primitive.ObjectID `json:"orderId"` MediaID primitive.ObjectID `json:"mediaId"` ContentID primitive.ObjectID `json:"contentId"` AccessType string `json:"accessType"` CanPlay bool `json:"canPlay"` PaidCoin int64 `json:"paidCoin"` CoinBalance int64 `json:"coinBalance"` } func BuyDramaEpisode( uid uint64, mediaID, contentID primitive.ObjectID, checkoutContextID, requestID string, uaInfo ua.UA, ip string, ) (BuyDramaEpisodeResponse, stderr.Code) { requestID = strings.TrimSpace(requestID) checkoutContextID = strings.TrimSpace(checkoutContextID) if requestID == "" || len(requestID) > 128 || contentID.IsZero() || mediaID.IsZero() || !strings.HasPrefix(checkoutContextID, "drama-checkout-") { return BuyDramaEpisodeResponse{}, stderr.ErrParamError } media, err := mediamod.GetInfo(mediaID) if err != nil || media.ID.IsZero() || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete { return BuyDramaEpisodeResponse{}, stderr.InvalidProduct } content, err := mediacontentmod.GetInfo(contentID, true) if err != nil || content.ID.IsZero() || content.MediaID != mediaID || content.MediaType != mediamod.MediaTypeDrama || content.IsDelete { return BuyDramaEpisodeResponse{}, stderr.InvalidProduct } if bought, buyErr := media_buy_record_mod.IsBuy(uid, mediaID, contentID); buyErr != nil { return BuyDramaEpisodeResponse{}, stderr.PayBusy } else if bought { return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase } hasCard, cardErr := mediacontentser.ActiveDramaCardStatus(uid) if cardErr != nil { return BuyDramaEpisodeResponse{}, stderr.PayBusy } isFree, canPlay, accessType := mediacontentser.ResolveDramaAccess(content, media, false, hasCard) if isFree || (canPlay && accessType == "card") { return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase } if content.Price <= 0 { return BuyDramaEpisodeResponse{}, stderr.InvalidProduct } wallet, err := walletmod.GetWallet(uid) if err != nil || wallet == nil || wallet.ID.IsZero() { return BuyDramaEpisodeResponse{}, stderr.PayBusy } plan := debitPlan(wallet, content.Price) if plan == nil { return BuyDramaEpisodeResponse{}, stderr.InsufficientBalance } user, err := usermod.FindUserByUID(uid) if err != nil || user == nil { return BuyDramaEpisodeResponse{}, stderr.PayBusy } if appg.Redis == nil { return BuyDramaEpisodeResponse{}, stderr.PayBusy } requestKey := dramaBuyKey("request", uid, requestID) requestAcquired, err := appg.Redis.SetNX(requestKey, "1", dramaBuyRequestTTL) if err != nil { return BuyDramaEpisodeResponse{}, stderr.PayBusy } if !requestAcquired { if bought, _ := media_buy_record_mod.IsBuy(uid, mediaID, contentID); bought { return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase } return BuyDramaEpisodeResponse{}, stderr.PayBusy } lockKey := dramaBuyKey("episode", uid, contentID.Hex()) lockAcquired, err := appg.Redis.SetNX(lockKey, requestID, dramaBuyLockTTL) if err != nil || !lockAcquired { _, _ = appg.Redis.Del(requestKey) return BuyDramaEpisodeResponse{}, stderr.PayBusy } defer releaseDramaBuyLock(lockKey, requestID) orderID := primitive.NewObjectID() createdAt := time.Now() balanceBefore := wallet.Amount + wallet.Income balanceAfter := balanceBefore err = appg.VideoDB.Trans(func(t *db.MongoTool) error { alreadyBought, checkErr := media_buy_record_mod.IsBuyWithTool(t, uid, mediaID, contentID) if checkErr != nil { return checkErr } if alreadyBought { return errDramaAlreadyBought } updatedWallet, debitErr := walletmod.Debit(t, plan, uid) if debitErr != nil { return debitErr } balanceAfter = updatedWallet.Amount + updatedWallet.Income record := &media_buy_record_mod.MediaBuyRecord{ ID: orderID, MediaId: mediaID, MediaType: mediamod.MediaTypeDrama, Uid: uid, Type: 0, ContentId: contentID, Coins: content.Price, PayMoney: content.Price, CreatedAt: createdAt, UpdateTime: createdAt, } if createErr := media_buy_record_mod.Create(t, record); createErr != nil { return createErr } transaction := txnmod.TransactionLog{ TransNo: orderID, UID: uid, Amount: -content.Price, ActualAmount: float64(-content.Price), TranType: txnmod.BuyAcg.Key(), TranTypeInt: int64(txnmod.BuyAcg), Desc: fmt.Sprintf("短剧-单集购买-%s-第%d集", media.Title, content.EpisodeNumber), DiscDoc: user.DiscDoc, SysType: user.SysType, RealAmount: walletmod.GetRealAmount(updatedWallet), } if logErr := txnmod.InsertTransactionLog(t, &transaction); logErr != nil { return logErr } if countErr := mediamod.IncPurchasesCountWithTool(t, mediaID, 1); countErr != nil { return countErr } if countErr := mediacontentmod.IncCountPurchasesWithTool(t, contentID, 1); countErr != nil { return countErr } return nil }) if err != nil { _, _ = appg.Redis.Del(requestKey) if errors.Is(err, errDramaAlreadyBought) { return BuyDramaEpisodeResponse{}, stderr.RepeatPurchase } log.Error("buy drama episode transaction failed", log.E(err), log.Any("uid", uid), log.Any("contentID", contentID)) return BuyDramaEpisodeResponse{}, stderr.BuyFailed } return BuyDramaEpisodeResponse{ OrderID: orderID, MediaID: mediaID, ContentID: contentID, AccessType: "bought", CanPlay: true, PaidCoin: content.Price, CoinBalance: balanceAfter, }, stderr.Success } func dramaBuyKey(scope string, uid uint64, value string) string { sum := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%s", scope, uid, value))) return "drama:buy:" + scope + ":" + hex.EncodeToString(sum[:]) } func releaseDramaBuyLock(key, token string) { if appg.Redis == nil || key == "" || token == "" { return } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if _, err := appg.Redis.RunScriptContext(ctx, releaseDramaBuyLockScript, []string{key}, token); err != nil { log.Error("release drama buy lock failed", log.E(err), log.Any("key", key)) } }