Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+383
View File
@@ -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
}
+164
View File
@@ -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)
}
})
}
}