197 lines
6.8 KiB
Go
197 lines
6.8 KiB
Go
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))
|
|
}
|
|
}
|