@@ -0,0 +1,383 @@
|
||||
package ai_mate_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
aimate "91porn-server/common/aiMate"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func GetApiUrl() string {
|
||||
var apiUrl string
|
||||
if appg.Conf.URL.AIMateH5 != "" {
|
||||
if appg.Conf.Base.Env == "prod" {
|
||||
apiUrl = fmt.Sprintf("%s/?aId=%d", appg.Conf.URL.AIMateH5, commod.KFK_APPID)
|
||||
} else {
|
||||
apiUrl = fmt.Sprintf("%s/?aId=%d&devTest=1", appg.Conf.URL.AIMateH5, commod.KFK_APPID)
|
||||
}
|
||||
}
|
||||
return apiUrl
|
||||
}
|
||||
|
||||
type LoginResp struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type loginDependencies struct {
|
||||
findUser func(uint64) (*usermod.User, error)
|
||||
updateUser func(uint64, usermod.UserSelector) (*usermod.User, error)
|
||||
getWallet func(uint64) (*walletmod.Wallet, error)
|
||||
getRemoteBalance func(uint64, string) (float64, error)
|
||||
getAccessURL func(uint64, string, string, float64) (string, error)
|
||||
newUUID func() string
|
||||
}
|
||||
|
||||
func defaultLoginDependencies() loginDependencies {
|
||||
return loginDependencies{
|
||||
findUser: usermod.FindUserByUID,
|
||||
updateUser: usermod.Update,
|
||||
getWallet: walletmod.GetWallet,
|
||||
getRemoteBalance: aimate.GetUserAiBalance,
|
||||
getAccessURL: aimate.GetUserAccessAiUrl,
|
||||
newUUID: common.UUID,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 获取当前用户的 AI 女友第三方登录地址。
|
||||
func Login(uid uint64) (LoginResp, error) {
|
||||
if appg.Redis == nil {
|
||||
return LoginResp{}, errors.New("redis is unavailable")
|
||||
}
|
||||
lock := redis.BuildLock(
|
||||
appg.Redis,
|
||||
redisconst.GetAiMateLoginLockKey(uid),
|
||||
redsync.WithExpiry(30*time.Second),
|
||||
redsync.WithTries(1),
|
||||
)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return LoginResp{}, fmt.Errorf("acquire ai mate login lock: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if _, err := lock.Unlock(); err != nil {
|
||||
log.Warn("ai_mate_ser Login unlock failed", log.Any("uid", uid), log.E(err))
|
||||
}
|
||||
}()
|
||||
return login(uid, defaultLoginDependencies())
|
||||
}
|
||||
|
||||
func login(uid uint64, deps loginDependencies) (LoginResp, error) {
|
||||
user, err := deps.findUser(uid)
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
return LoginResp{}, errors.New("ai mate login user not found")
|
||||
}
|
||||
wallet, err := deps.getWallet(uid)
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
|
||||
localBalance := float64(0)
|
||||
if wallet != nil {
|
||||
localBalance = wallet.AiMateBalance
|
||||
}
|
||||
aiMateUID := user.AiMateUid
|
||||
remoteBalance := float64(0)
|
||||
if aiMateUID == "" {
|
||||
aiMateUID = deps.newUUID()
|
||||
if aiMateUID == "" {
|
||||
return LoginResp{}, errors.New("generate ai mate uid failed")
|
||||
}
|
||||
if _, err = deps.updateUser(uid, usermod.UserSelector{AiMateUid: &aiMateUID}); err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
} else {
|
||||
remoteBalance, err = deps.getRemoteBalance(uid, aiMateUID)
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
}
|
||||
|
||||
url, err := deps.getAccessURL(uid, aiMateUID, user.Name, balanceTopUp(localBalance, remoteBalance))
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
if url == "" {
|
||||
return LoginResp{}, errors.New("ai mate login url is empty")
|
||||
}
|
||||
return LoginResp{URL: url}, nil
|
||||
}
|
||||
|
||||
func balanceTopUp(localBalance, remoteBalance float64) float64 {
|
||||
if localBalance <= remoteBalance || localBalance <= 0 {
|
||||
return 0
|
||||
}
|
||||
return localBalance - remoteBalance
|
||||
}
|
||||
|
||||
// GetCurrencyList 获取AI伴侣币列表
|
||||
func GetCurrencyList() (code stderr.Code, data interface{}) {
|
||||
code = stderr.Success
|
||||
cs := make([]currencymod.CurrencyApp, 0)
|
||||
//查询
|
||||
currencys, err := currencymod.List(commod.AiMateCoin)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser GetCurrencyList currencymod.List err", log.E(err))
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if len(currencys) > 0 {
|
||||
for _, c := range currencys {
|
||||
cs = append(cs, currencymod.CurrencyApp{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Type: c.Type,
|
||||
Coins: c.Coins,
|
||||
Price: c.Price,
|
||||
CouponDesc: c.CouponDesc,
|
||||
})
|
||||
}
|
||||
}
|
||||
data = cs
|
||||
return
|
||||
}
|
||||
|
||||
// ExchangeReq 兑换请求参数
|
||||
type ExchangeReq struct {
|
||||
Id primitive.ObjectID `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// Exchange 获取AI伴侣币兑换
|
||||
func Exchange(uid uint64, req ExchangeReq, ua ua.UA, ip string) (code stderr.Code) {
|
||||
code = stderr.Success
|
||||
//查询用户信息
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser Exchange usermod.FindUserByUID err", log.Any("uid", uid), log.E(err))
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
log.Error("ai_mate_ser Exchange user not exist", log.Any("uid", uid))
|
||||
code = stderr.UserIsNotExists
|
||||
return
|
||||
}
|
||||
//查询货币是否存在
|
||||
currency, err := currencymod.Get(req.Id)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser Exchange currencymod.List err", log.E(err))
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if currency.ID.IsZero() {
|
||||
log.Error("ai_mate_ser Exchange currency not exist", log.Any("id", req.Id.Hex()))
|
||||
code = stderr.CodeEmptyData
|
||||
return
|
||||
}
|
||||
if !currency.IsActive {
|
||||
log.Error("ai_mate_ser Exchange currency isActive is false", log.Any("id", req.Id.Hex()))
|
||||
code = stderr.ErrDataInvalid
|
||||
return
|
||||
}
|
||||
if currency.Type != commod.AiMateCoin {
|
||||
log.Error("ai_mate_ser Exchange currency type not commod.AiMateCoin", log.Any("id", req.Id.Hex()))
|
||||
code = stderr.ExchangeCodeInvalid
|
||||
return
|
||||
}
|
||||
//查询用户钱包
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
code = stderr.ErrDbQueryError
|
||||
log.Error("ai_mate_ser Exchange walletmod.GetWallet err:", log.E(err), log.Any("uid", uid))
|
||||
return
|
||||
}
|
||||
//需要支付的金额
|
||||
price := currency.Price
|
||||
if wallet == nil || wallet.Amount < price {
|
||||
code = stderr.InsufficientBalance
|
||||
log.Error("ai_mate_ser Exchange user Insufficient balance", log.Any("uid", uid), log.Any("id", currency.ID.Hex()))
|
||||
return
|
||||
}
|
||||
|
||||
//兑换业务
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
//扣除金币余额,增加AI伴侣余额
|
||||
cAmount := -price
|
||||
cAiMateBalance := float64(currency.Coins)
|
||||
p := walletmod.CreditPlan{
|
||||
Amount: &cAmount,
|
||||
AiMateBalance: &cAiMateBalance,
|
||||
}
|
||||
wallet, err := walletmod.Credit(t, p, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
//新增交易日志
|
||||
txnLog := &txnmod.TransactionLog{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -price,
|
||||
ActualAmount: float64(-price),
|
||||
TranType: txnmod.AiMateCurrencyExchange.Key(),
|
||||
TranTypeInt: int64(txnmod.AiMateCurrencyExchange),
|
||||
TransNo: currency.ID,
|
||||
Desc: "AI伴侣币购买-" + currency.Name,
|
||||
DiscDoc: user.DiscDoc,
|
||||
SysType: user.SysType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
AiMatePoint: cAiMateBalance,
|
||||
RealAiMatePoint: wallet.AiMateBalance,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err = txnmod.InsertTransactionLog(t, txnLog); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn("ai_mate_ser Exchange Transaction fail", log.Any("uid", uid), log.Any("id", currency.ID.Hex()), log.E(err))
|
||||
code = stderr.BuyFailed
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type BalanceResp struct {
|
||||
Balance float64 `json:"balance" bson:"balance"` // 余额
|
||||
LastOrderId string `json:"lastOrderId" bson:"lastOrderId"`
|
||||
}
|
||||
|
||||
// GetNewBalance 获取AI伴侣余余额
|
||||
func GetNewBalance(uid uint64) (BalanceResp, error) {
|
||||
var ret BalanceResp
|
||||
|
||||
// 查询用户信息
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser GetBalance usermod.FindUserByUID err:", log.E(err), log.Any("uid", uid))
|
||||
return ret, err
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
log.Error("ai_mate_ser GetBalance user not exist", log.Any("uid", uid))
|
||||
return ret, errors.New("ai_mate_ser GetBalance user not exist")
|
||||
|
||||
}
|
||||
|
||||
// 查询用户钱包ai伴侣余额
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser GetBalance walletmod.GetWallet err:", log.E(err), log.Any("uid", uid))
|
||||
return ret, err
|
||||
}
|
||||
|
||||
if wallet != nil && wallet.AiMateBalance > 0 {
|
||||
ret.Balance = wallet.AiMateBalance
|
||||
ret.LastOrderId = wallet.LastAiMateRecordId
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type SyncInfoRes struct {
|
||||
UID uint64 `json:"uid" bson:"uid"` // 用户id
|
||||
TotalTokens int `json:"totalTokens" bson:"totalTokens"` // 总共使用的 token 数量
|
||||
Amount float64 `json:"amount" bson:"amount"` // 花费积分
|
||||
OrderID string `json:"orderId" bson:"orderId"` // 订单ID
|
||||
}
|
||||
|
||||
func (q *SyncInfoRes) Sync() error {
|
||||
// 同步用户信息
|
||||
userInfo, err := usermod.FindUserByUIDForNoCache(q.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userInfo == nil || userInfo.ID.IsZero() {
|
||||
log.Warn(fmt.Sprintf("user is null"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取
|
||||
cond := bson.M{"tranTypeInt": txnmod.AiMateChat, "uid": q.UID, "productID": q.OrderID}
|
||||
transactionLog, err := txnmod.FindByCond(cond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重复上传
|
||||
if !transactionLog.ID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
// 获取余额
|
||||
wallet, err := walletmod.GetWallet(q.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if wallet == nil || wallet.ID.IsZero() {
|
||||
return errors.New("wallet is null")
|
||||
}
|
||||
|
||||
amt := -q.Amount
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(q.UID, []txnmod.TransType{
|
||||
txnmod.AiMateChat,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
// 新增日志流水记录
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if q.Amount > 0 {
|
||||
// 扣除余额
|
||||
w, err := walletmod.CreditAiMate(t, amt, q.UID, q.OrderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
po := int64(-q.Amount)
|
||||
|
||||
// 添加流水
|
||||
if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: userInfo.UID,
|
||||
TranType: txnmod.AiMateChat.Key(),
|
||||
TranTypeInt: int64(txnmod.AiMateChat),
|
||||
Desc: fmt.Sprintf("AI女友聊天-花费:%.2f积分", q.Amount),
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
Integral: po,
|
||||
ProductID: &q.OrderID,
|
||||
AiMatePoint: -q.Amount,
|
||||
RealAiMatePoint: w.AiMateBalance,
|
||||
IsRepurchase: isRepurchase,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("uid:%v,ai_mate sync trans err:%v", q.UID, err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package ai_mate_ser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestLoginCreatesAiMateUserAndCreditsLocalBalance(t *testing.T) {
|
||||
const uid = uint64(32239213)
|
||||
var (
|
||||
updatedAiMateUID string
|
||||
accessAiMateUID string
|
||||
creditedBalance float64
|
||||
)
|
||||
deps := loginDependencies{
|
||||
findUser: func(gotUID uint64) (*usermod.User, error) {
|
||||
if gotUID != uid {
|
||||
t.Fatalf("findUser uid = %d, want %d", gotUID, uid)
|
||||
}
|
||||
return &usermod.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Name: "tester",
|
||||
}, nil
|
||||
},
|
||||
updateUser: func(gotUID uint64, selector usermod.UserSelector) (*usermod.User, error) {
|
||||
if gotUID != uid || selector.AiMateUid == nil {
|
||||
t.Fatalf("unexpected updateUser arguments: uid=%d selector=%+v", gotUID, selector)
|
||||
}
|
||||
updatedAiMateUID = *selector.AiMateUid
|
||||
return &usermod.User{}, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{AiMateBalance: 12}, nil
|
||||
},
|
||||
getRemoteBalance: func(uint64, string) (float64, error) {
|
||||
t.Fatal("new AI mate user must not query a remote balance")
|
||||
return 0, nil
|
||||
},
|
||||
getAccessURL: func(gotUID uint64, aiMateUID, name string, balance float64) (string, error) {
|
||||
if gotUID != uid || name != "tester" {
|
||||
t.Fatalf("unexpected access URL arguments: uid=%d name=%q", gotUID, name)
|
||||
}
|
||||
accessAiMateUID = aiMateUID
|
||||
creditedBalance = balance
|
||||
return "https://example.com/login", nil
|
||||
},
|
||||
newUUID: func() string { return "new-ai-mate-uid" },
|
||||
}
|
||||
|
||||
got, err := login(uid, deps)
|
||||
if err != nil {
|
||||
t.Fatalf("login() error = %v", err)
|
||||
}
|
||||
if got.URL != "https://example.com/login" {
|
||||
t.Fatalf("login() URL = %q", got.URL)
|
||||
}
|
||||
if updatedAiMateUID != "new-ai-mate-uid" || accessAiMateUID != updatedAiMateUID {
|
||||
t.Fatalf("AI mate uid update=%q access=%q", updatedAiMateUID, accessAiMateUID)
|
||||
}
|
||||
if creditedBalance != 12 {
|
||||
t.Fatalf("credited balance = %v, want 12", creditedBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginOnlyCreditsBalanceDifference(t *testing.T) {
|
||||
const uid = uint64(32239213)
|
||||
var creditedBalance float64
|
||||
deps := loginDependencies{
|
||||
findUser: func(uint64) (*usermod.User, error) {
|
||||
return &usermod.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Name: "tester",
|
||||
AiMateUid: "existing-ai-mate-uid",
|
||||
}, nil
|
||||
},
|
||||
updateUser: func(uint64, usermod.UserSelector) (*usermod.User, error) {
|
||||
t.Fatal("existing AI mate user must not be updated")
|
||||
return nil, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{AiMateBalance: 15}, nil
|
||||
},
|
||||
getRemoteBalance: func(gotUID uint64, aiMateUID string) (float64, error) {
|
||||
if gotUID != uid || aiMateUID != "existing-ai-mate-uid" {
|
||||
t.Fatalf("unexpected remote balance arguments: uid=%d aiMateUID=%q", gotUID, aiMateUID)
|
||||
}
|
||||
return 9, nil
|
||||
},
|
||||
getAccessURL: func(_ uint64, _ string, _ string, balance float64) (string, error) {
|
||||
creditedBalance = balance
|
||||
return "https://example.com/login", nil
|
||||
},
|
||||
newUUID: func() string {
|
||||
t.Fatal("existing AI mate user must not generate another uid")
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := login(uid, deps); err != nil {
|
||||
t.Fatalf("login() error = %v", err)
|
||||
}
|
||||
if creditedBalance != 6 {
|
||||
t.Fatalf("credited balance = %v, want 6", creditedBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginStopsWhenRemoteBalanceCannotBeRead(t *testing.T) {
|
||||
expectedErr := errors.New("remote unavailable")
|
||||
accessCalled := false
|
||||
deps := loginDependencies{
|
||||
findUser: func(uint64) (*usermod.User, error) {
|
||||
return &usermod.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: 1,
|
||||
AiMateUid: "existing-ai-mate-uid",
|
||||
}, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{AiMateBalance: 10}, nil
|
||||
},
|
||||
getRemoteBalance: func(uint64, string) (float64, error) {
|
||||
return 0, expectedErr
|
||||
},
|
||||
getAccessURL: func(uint64, string, string, float64) (string, error) {
|
||||
accessCalled = true
|
||||
return "", nil
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := login(1, deps); !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("login() error = %v, want %v", err, expectedErr)
|
||||
}
|
||||
if accessCalled {
|
||||
t.Fatal("access URL must not be requested when the remote balance is unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalanceTopUp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
local float64
|
||||
remote float64
|
||||
want float64
|
||||
}{
|
||||
{name: "new credit", local: 10, remote: 4, want: 6},
|
||||
{name: "already synchronized", local: 10, remote: 10, want: 0},
|
||||
{name: "remote ahead", local: 8, remote: 10, want: 0},
|
||||
{name: "empty", local: 0, remote: 0, want: 0},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := balanceTopUp(test.local, test.remote); got != test.want {
|
||||
t.Fatalf("balanceTopUp(%v, %v) = %v, want %v", test.local, test.remote, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user