package aiser import ( "context" "errors" "fmt" "math" "time" "91porn-server/app/appg" "91porn-server/common/constant/redisconst" "91porn-server/common/db" "91porn-server/common/laosiji_app" "91porn-server/common/log" "91porn-server/common/redis" "91porn-server/models/commod" "91porn-server/models/v/fundtransferlogmod" "91porn-server/models/v/usermod" "91porn-server/models/v/walletmod" "github.com/go-redsync/redsync/v4" "github.com/shopspring/decimal" "go.mongodb.org/mongo-driver/bson/primitive" ) const aiFundLockExpiry = 30 * time.Second type GetAuthURLResp struct { URL string `json:"url"` } type transferDependencies struct { findLatest func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) findLatestOut func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) getWallet func(uint64) (*walletmod.Wallet, error) findUser func(uint64) (*usermod.User, error) getAuthURL func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) bringOut func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) debitAndLog func(uint64, int64, time.Time) error creditAndLog func(uint64, int64, float64, string, time.Time) error now func() time.Time } func defaultTransferDependencies() transferDependencies { return transferDependencies{ findLatest: fundtransferlogmod.FindLatestByUIDAndCategory, findLatestOut: fundtransferlogmod.FindLatestOutByUIDAndCategory, getWallet: walletmod.GetWallet, findUser: usermod.FindUserByUID, getAuthURL: laosiji_app.GetAiMateURL, bringOut: laosiji_app.AiMateBringOut, debitAndLog: debitAndLog, creditAndLog: creditAndLog, now: time.Now, } } func acquireLock(uid uint64) (func(), error) { if appg.Redis == nil { return nil, errors.New("redis is unavailable") } lock := redis.BuildLock( appg.Redis, redisconst.GetAiFundLockKey(uid), redsync.WithExpiry(aiFundLockExpiry), redsync.WithTries(1), ) if err := lock.Lock(); err != nil { return nil, err } return func() { if _, err := lock.Unlock(); err != nil { log.Warn("unlock AI fund lock failed", log.Any("uid", uid), log.E(err)) } }, nil } // GetAuthURL 先回收上一次第三方余额,再将主钱包金币上分并返回AI女友授权地址。 func GetAuthURL(ctx context.Context, uid uint64) (GetAuthURLResp, error) { unlock, err := acquireLock(uid) if err != nil { return GetAuthURLResp{}, errors.New("操作频繁,请稍后再试") } defer unlock() return getAuthURL(ctx, uid, defaultTransferDependencies()) } func getAuthURL(ctx context.Context, uid uint64, deps transferDependencies) (GetAuthURLResp, error) { if err := settleDown(ctx, uid, deps); err != nil { return GetAuthURLResp{}, fmt.Errorf("下分失败: %w", err) } wallet, err := deps.getWallet(uid) if err != nil { return GetAuthURLResp{}, fmt.Errorf("获取钱包失败: %w", err) } var amount int64 if wallet != nil { amount = wallet.Amount } var remainder float64 lastOut, err := deps.findLatestOut(uid, fundtransferlogmod.CategoryAiGirlfriend) if err != nil { return GetAuthURLResp{}, fmt.Errorf("获取下分余数失败: %w", err) } if lastOut != nil { remainder = lastOut.Remainder } nickname, avatar := "用户", "" if user, findErr := deps.findUser(uid); findErr == nil && user != nil { if user.Name != "" { nickname = user.Name } avatar = user.Portrait } username := laosiji_app.GetUserName(appg.Conf.Base.Env, commod.KFK_APPID, uid) authResp, err := deps.getAuthURL(ctx, laosiji_app.GetAiMateURLReq{ Username: username, Nickname: nickname, Asset: assetAmount(amount, remainder), Currency: "CNY", Theme: "dark", UserAvatar: avatar, }) if err != nil { return GetAuthURLResp{}, fmt.Errorf("获取AI女友地址失败: %w", err) } if authResp.AuthURL == "" { return GetAuthURLResp{}, errors.New("AI女友地址为空") } if amount > 0 { if err = deps.debitAndLog(uid, amount, deps.now()); err != nil { return GetAuthURLResp{}, fmt.Errorf("上分扣除金币失败: %w", err) } } return GetAuthURLResp{URL: authResp.AuthURL}, nil } // TrySettleDown 在钱包查询前尝试将AI女友剩余余额下分回主钱包。 func TrySettleDown(ctx context.Context, uid uint64) error { unlock, err := acquireLock(uid) if err != nil { return errors.New("操作频繁,请稍后再试") } defer unlock() return settleDown(ctx, uid, defaultTransferDependencies()) } func settleDown(ctx context.Context, uid uint64, deps transferDependencies) error { latest, err := deps.findLatest(uid, fundtransferlogmod.CategoryAiGirlfriend) if err != nil { return err } if latest == nil || latest.FundType != fundtransferlogmod.FundTypeIn { return nil } resp, err := deps.bringOut(ctx, laosiji_app.AiMateBringOutReq{ Username: laosiji_app.GetUserName(appg.Conf.Base.Env, commod.KFK_APPID, uid), }) if err != nil { return err } returnAmount, remainder, err := balanceToWallet(resp.Balance) if err != nil { return err } desc := "AI女友下分" if returnAmount == 0 { desc += "-金币为0" } return deps.creditAndLog(uid, returnAmount, remainder, desc, deps.now()) } func assetAmount(amount int64, remainder float64) string { amountDecimal := decimal.NewFromInt(amount).Div(decimal.NewFromInt(10)) return amountDecimal.Add(decimal.NewFromFloat(remainder)).Round(2).StringFixed(2) } func balanceToWallet(balance string) (int64, float64, error) { value, err := decimal.NewFromString(balance) if err != nil { return 0, 0, fmt.Errorf("无效的第三方余额: %w", err) } if value.IsNegative() { return 0, 0, errors.New("第三方余额不能为负数") } coins := value.Mul(decimal.NewFromInt(10)).Floor() if coins.GreaterThan(decimal.NewFromInt(math.MaxInt64)) { return 0, 0, errors.New("第三方余额超出范围") } returnAmount := coins.IntPart() remainderDecimal := value.Sub(decimal.NewFromInt(returnAmount).Div(decimal.NewFromInt(10))).Round(2) remainder, _ := remainderDecimal.Float64() return returnAmount, remainder, nil } func debitAndLog(uid uint64, amount int64, now time.Time) error { if appg.VideoDB == nil { return errors.New("video database is unavailable") } return appg.VideoDB.Trans(func(t *db.MongoTool) error { wallet, err := walletmod.DebitAmount(t, amount, uid) if err != nil { return err } return fundtransferlogmod.Insert(t, &fundtransferlogmod.FundTransferLog{ ID: primitive.NewObjectID(), UID: uid, Category: fundtransferlogmod.CategoryAiGirlfriend, FundType: fundtransferlogmod.FundTypeIn, Amount: amount, Balance: wallet.Amount, Desc: "AI女友上分", CreatedAt: now, }) }) } func creditAndLog(uid uint64, amount int64, remainder float64, desc string, now time.Time) error { if appg.VideoDB == nil { return errors.New("video database is unavailable") } return appg.VideoDB.Trans(func(t *db.MongoTool) error { wallet, err := walletmod.CreditAmount(t, amount, uid) if err != nil { return err } return fundtransferlogmod.Insert(t, &fundtransferlogmod.FundTransferLog{ ID: primitive.NewObjectID(), UID: uid, Category: fundtransferlogmod.CategoryAiGirlfriend, FundType: fundtransferlogmod.FundTypeOut, Amount: amount, Balance: wallet.Amount, Remainder: remainder, Desc: desc, CreatedAt: now, }) }) }