Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

245 lines
7.4 KiB
Go

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,
})
})
}