package biz import ( "context" "encoding/json" "fmt" "strconv" "strings" "time" "sandc/app/eonline/internal/conf" "sandc/app/eonline/internal/config" "sandc/pkg/geo" "sandc/pkg/middleware/xhttp" "sandc/pkg/utils" "github.com/go-kratos/kratos/v2/log" "github.com/hibiken/asynq" "github.com/tx7do/kratos-transport/broker" ) type Eonline struct { Hello string } type EonlineRepo interface { CreateEonline(context.Context, *Eonline) error UpdateEonline(context.Context, *Eonline) error GetUserByUuid(ctx context.Context, uuid string) (*PayoutUser, error) CreatePayoutUser(ctx context.Context, item *PayoutUser) (*PayoutUser, error) SearchPayoutRecord(ctx context.Context, opts *SearchPayoutRecordReq) (*ListPayoutRecord, error) GetPayoutLeftum(ctx context.Context, uuid string) (int, error) GetPayoutRecordCountByAccount(ctx context.Context, account string) (int, error) CreatePayoutRecord(ctx context.Context, item *PayoutRecord) (*PayoutRecord, error) UpdatePayoutRecordNotify(ctx context.Context, recordNo string, payoutId string, status uint8, fail string) error GetPayoutRecordListByUuid(ctx context.Context, uuid string) ([]*PayoutRecord, error) // GetPayoutRecordByRecordNo 获取提现记录 GetPayoutRecordByRecordNo(ctx context.Context, recordNo string) (*PayoutRecord, error) // 根据account、status 获取提现记录 GetPayoutRecordByAccountStatus(ctx context.Context, account string, status, pageIndex, pageSize int) ([]*PayoutRecord, error) // UpdatePayoutUserLoginDays 更新用户登录天数 UpdatePayoutUserLoginDays(ctx context.Context, uuid string, loginDays uint) error // 更新 客户端上传的、需要保持的数据 UpdatePayoutUserClientData(ctx context.Context, uuid string, clientData string) error CreateBonusRecord(ctx context.Context, item *BonusRecord) (*BonusRecord, error) UpdateBonusCur(ctx context.Context, id uint32, bonusCur uint32) error } type EonlineUsecase struct { repo EonlineRepo conf *conf.Bootstrap log *log.Helper tx Transaction pscli *PagsmileClient cache Cache geocli *geo.GeoClient } func NewEonlineUsecase(repo EonlineRepo, conf *conf.Bootstrap, tx Transaction, logger log.Logger, cache Cache) *EonlineUsecase { return &EonlineUsecase{ repo: repo, tx: tx, log: log.NewHelper(logger), pscli: NewPagsmileClient(&PayoutClient{ AppId: conf.Pagsmile.Payout.AppId, AppKey: conf.Pagsmile.Payout.AppKey, ApiUrl: conf.Pagsmile.Payout.ApiUrl, NotifyUrl: conf.Pagsmile.Payout.NotifyUrl, }, logger), conf: conf, cache: cache, geocli: geo.NewGeoClient(conf.Server.GeoFile), } } func (uc *EonlineUsecase) Create(ctx context.Context, g *Eonline) error { return uc.repo.CreateEonline(ctx, g) } func (uc *EonlineUsecase) Update(ctx context.Context, g *Eonline) error { return uc.repo.UpdateEonline(ctx, g) } func (uc *EonlineUsecase) KakfaExampleConsumer(_ context.Context, topic string, headers broker.Headers, msg *ExampleStruct) error { log.Infof("Topic %s, Headers: %+v, Payload: %+v\n", topic, headers, msg) return nil } const EonlineTaskName = "eonline:task" // AsynqEonlineTaskHandler Asynq handler task func (uc *EonlineUsecase) AsynqEonlineTaskHandler(ctx context.Context, t *asynq.Task) error { // do something // do some else logging log.Info("AsynqEonlineTaskHandle: ", string(t.Payload())) return nil } // PayInitReq 支付初始化请求 type PayInitReq struct { DeviceId string } // PayInitReply 支付初始化响应 type PayInitReply struct { UUID string ClientData string Days uint32 // 登录天数 Items []*PayoutItem } // PayoutItem 支付项目 type PayoutItem struct { Id uint Amount float64 Status uint8 } // PayInit func (uc *EonlineUsecase) PayInit(ctx context.Context, req *PayoutAddtionalReq) (*PayInitReply, error) { // 生成唯一用户ID uuid := GenPayoutUuid(req.DeviceId) err := uc.initUserLoginDays(ctx, uuid, req) if err != nil { return nil, fmt.Errorf("init user login days error: %v", err) } // 判断是否提现 payoutValidRes, err, _ := uc.checkPayoutValid(ctx, uuid, req) if err != nil { // 如果不允许提现,状态全部更改为不可提现 payoutValidRes.LeftNum[uint(PayoutItemId1)] = -1 payoutValidRes.LeftNum[uint(PayoutItemId2)] = -1 payoutValidRes.LeftNum[uint(PayoutItemId3)] = -1 payoutValidRes.LeftNum[uint(PayoutItemId4)] = -1 } items := make([]*PayoutItem, 0) for k, v := range PayoutItemIdAmountes { if k == 0 { continue } // 生成对应的payoutItem payoutItem := &PayoutItem{ Id: uint(k), Amount: v, Status: uint8(uc.getPayoutItemStatus(ctx, uuid, req.Ts, uint(k), payoutValidRes)), } items = append(items, payoutItem) } reply := &PayInitReply{ UUID: uuid, ClientData: payoutValidRes.ClientData, Days: payoutValidRes.Days, Items: items, } return reply, nil } // initUserLoginDays 初始化用户登录天数 func (uc *EonlineUsecase) initUserLoginDays(ctx context.Context, uuid string, req *PayoutAddtionalReq) error { // 插入用户登录信息 // 判断当前用户是否存在 user, err := uc.repo.GetUserByUuid(ctx, uuid) if err != nil { return err } // 定义用户登录redis key keyDay := "" if uc.conf.Server.Env == "qa" { ts, _ := strconv.ParseInt(req.Ts, 10, 64) keyDay = time.Unix(ts, 0).Format("20060102") } userLoginRedisKey := GetUserLoginRedisKey(uuid, keyDay) // 创建用户 if user.Id == 0 { // 记录登录用户 _, err = uc.repo.CreatePayoutUser(ctx, &PayoutUser{ Uuid: uuid, DeviceId: req.DeviceId, Platform: req.Platform, Version: req.Version, Country: req.Country, Ip: req.Ip, LoginDays: 1, }) if err != nil { return err } // 是否已记录当前登录行为 err = uc.cache.WriteValue(ctx, userLoginRedisKey, "1", 24*3600) if err != nil { return fmt.Errorf("write redis error: %v", err) } } else { // 更新登录天数 currentValue, err := uc.cache.GetValue(ctx, userLoginRedisKey) if err != nil { return fmt.Errorf("get redis value error: %v", err) } if currentValue == "" { // 更新登录天数 loginDays := user.LoginDays + 1 err = uc.repo.UpdatePayoutUserLoginDays(ctx, uuid, loginDays) if err != nil { return fmt.Errorf("update payout user login days error: %v", err) } // 记录当前登录行为 err = uc.cache.WriteValue(ctx, userLoginRedisKey, "1", 24*3600) if err != nil { return fmt.Errorf("write redis error: %v", err) } } } return nil } func (uc *EonlineUsecase) updateUserClientData(ctx context.Context, uuid string, clientData string) error { err := uc.repo.UpdatePayoutUserClientData(ctx, uuid, clientData) if err != nil { return fmt.Errorf("update payout user clientData error: %v", err) } return nil } // getPayoutItemStatus 获取提现项目状态 func (uc *EonlineUsecase) getPayoutItemStatus(ctx context.Context, uuid, ts string, itemId uint, payoutValidRes *PayoutValidRes) PayoutItemStatus { leftNum, ok := payoutValidRes.LeftNum[itemId] if !ok || leftNum <= 0 { return PayoutItemStatusForbidden } if _, ok = payoutValidRes.RecordStatusMap[itemId]; ok { switch payoutValidRes.RecordStatusMap[itemId] { // 提现成功的状态,返回已提现 case uint8(PayoutStatusPayouted): return PayoutItemStatusPayouted // 提现失败的状态,返回可提现 case uint8(PayoutStatusPayoutFailed): return PayoutItemStatusAvailable // 提现中的状态,返回提现中 case uint8(PayoutStatusPayouting): return PayoutItemStatusPayouting default: return PayoutItemStatusForbidden } } // 如果没有提现记录,返回可提现 if itemId == 1 { return PayoutItemStatusAvailable } // 如果没有提现记录,返回可提现 // 根据uuid查询用户是否满足第二天的提现条件 // user, err := uc.repo.GetUserByUuid(ctx, uuid) // if err != nil { // return PayoutItemStatusForbidden // } // // if user.LoginDays >= 3 && payoutValidRes.LeftNum[itemId] == 1 { // return PayoutItemStatusAvailable // } else { // return PayoutItemStatusUnfinished // } keyDay := "" if uc.conf.Server.Env == "qa" { ts, _ := strconv.ParseInt(ts, 10, 64) keyDay = time.Unix(ts, 0).Format("20060102") } uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay, uint32(itemId)) v, err := uc.cache.GetValue(ctx, uuidRedisKey) if err != nil { // uc.log.WithContext(ctx).Errorf("get redis value error: %v", err) log.Infof("get redis value error: %v", err) return PayoutItemStatusUnfinished } if v == "" && leftNum > 0 { return PayoutItemStatusAvailable } else { return PayoutItemStatusUnfinished } } // PayoutValidRes 提现是否有效响应 type PayoutValidRes struct { Days uint32 LeftNum map[uint]int RecordStatusMap map[uint]uint8 ClientData string } // true表示可以提现的国家 func canPayoutCountry(country string) bool { if country == "US" || country == "CA" || country == "IN" || country == "PH" || country == "ID" || country == "VN" || country == "TH" || country == "BR" { return true } return false } // 转换国家代码 func convertCountry(country string) string { if country == "US" { // 美国 return "USA" } else if country == "CA" { // 加拿大 return "CAN" } else if country == "IN" { // 印度 return "IND" } else if country == "PH" { // 菲律宾 return "PHL" } else if country == "ID" { // 印度尼西亚 return "IDN" } else if country == "VN" { // 越南 return "VNM" } else if country == "TH" { // 泰国 return "THA" } else if country == "BR" { // 巴西 return "BRA" } log.Infof("convertCountry error: country[%s]", country) return country } // checkPayoutValid 检查提现是否有效,1+2位数 func (uc *EonlineUsecase) checkPayoutValid(ctx context.Context, uuid string, opts *PayoutAddtionalReq) (*PayoutValidRes, error, int32) { payoutValidRes := &PayoutValidRes{ Days: 0, LeftNum: make(map[uint]int), RecordStatusMap: make(map[uint]uint8), } // 判断是否可以提现,国家判断 // if opts.Country != "US" && opts.Country != "CA" && opts.Country != "IN" && opts.Country != "PH" { // if !canPayoutCountry(opts.Country) { // return payoutValidRes, fmt.Errorf("country [%s] not support", opts.Country), 101 // } if opts.ItemId > 0 { if opts.ItemId <= uint32(len(PayoutItemIdAmountes)) { // 判断金额是否一致 if int32(opts.Amount*100) != int32(PayoutItemIdAmountes[int(opts.ItemId)]*100) { return payoutValidRes, fmt.Errorf("amount error"), 102 } } else { return payoutValidRes, fmt.Errorf("ItemId error"), 103 } } user, err := uc.repo.GetUserByUuid(ctx, uuid) if err != nil { return payoutValidRes, err, 1 } payoutValidRes.Days = uint32(user.LoginDays) payoutValidRes.ClientData = user.ClientData records, err := uc.repo.GetPayoutRecordListByUuid(ctx, uuid) if err != nil { return payoutValidRes, err, 1 } var num1, num2, num3, num4 int var lastest1, lastest2, lastest3, lastest4 *PayoutRecord var leftAmount float64 for _, record := range records { if record.ItemId == uint(PayoutItemId4) { continue } switch record.ItemId { case uint(PayoutItemId1): if record.Status == uint8(PayoutStatusPayouted) || record.Status == uint8(PayoutStatusPayouting) { num1++ leftAmount += record.Amount } if lastest1 != nil { if lastest1.CreatedAt.Compare(record.CreatedAt) < 0 { lastest1 = record } } else { lastest1 = record } case uint(PayoutItemId2): if record.Status == uint8(PayoutStatusPayouted) || record.Status == uint8(PayoutStatusPayouting) { num2++ leftAmount += record.Amount } if lastest2 != nil { if lastest2.CreatedAt.Compare(record.CreatedAt) < 0 { lastest2 = record } } else { lastest2 = record } case uint(PayoutItemId3): if record.Status == uint8(PayoutStatusPayouted) || record.Status == uint8(PayoutStatusPayouting) { num3++ leftAmount += record.Amount } if lastest3 != nil { if lastest3.CreatedAt.Compare(record.CreatedAt) < 0 { lastest3 = record } } else { lastest3 = record } case uint(PayoutItemId4): if record.Status == uint8(PayoutStatusPayouted) || record.Status == uint8(PayoutStatusPayouting) { num4++ } if lastest4 != nil { if lastest4.CreatedAt.Compare(record.CreatedAt) < 0 { lastest4 = record } } else { lastest4 = record } default: log.Infof("checkPayoutValid error: uuid[%s] record.ItemId[%d] not dispatch No.1", uuid, record.ItemId) } } ts, _ := strconv.ParseInt(opts.Ts, 10, 64) keyDay := "" if uc.conf.Server.Env == "qa" { keyDay = time.Unix(ts, 0).Format("20060102") } var now time.Time now = time.Unix(ts, 0) recordStatusMap := make(map[uint]uint8) if lastest1 != nil { if isSameDay(now, lastest1.CreatedAt) { recordStatusMap[lastest1.ItemId] = lastest1.Status } } if lastest2 != nil { if isSameDay(now, lastest2.CreatedAt) { recordStatusMap[lastest2.ItemId] = lastest2.Status } } if lastest3 != nil { if isSameDay(now, lastest3.CreatedAt) { recordStatusMap[lastest3.ItemId] = lastest3.Status } } if lastest4 != nil { if isSameDay(now, lastest4.CreatedAt) { recordStatusMap[lastest4.ItemId] = lastest4.Status } } // true表示今天已提现过 // callCheckTodayPayoutStatus := func(itemId PayoutItemId) bool { // uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay, uint32(itemId)) // value, err := uc.cache.GetValue(ctx, uuidRedisKey) // if err == nil { // if value != "" { // return true // } // } // return false // } // 检查已提现次数 if num1 < PayoutRecordLimit1 { payoutValidRes.LeftNum[uint(PayoutItemId1)] = PayoutRecordLimit1 - num1 } else { payoutValidRes.LeftNum[uint(PayoutItemId1)] = -1 } if num2 < PayoutRecordLimit2 { payoutValidRes.LeftNum[uint(PayoutItemId2)] = PayoutRecordLimit2 - num2 } else { payoutValidRes.LeftNum[uint(PayoutItemId2)] = -1 } if num3 < PayoutRecordLimit3 { payoutValidRes.LeftNum[uint(PayoutItemId3)] = PayoutRecordLimit3 - num3 } else { payoutValidRes.LeftNum[uint(PayoutItemId3)] = -1 } if num4 < PayoutRecordLimit4 { payoutValidRes.LeftNum[uint(PayoutItemId4)] = PayoutRecordLimit4 - num4 } else { payoutValidRes.LeftNum[uint(PayoutItemId4)] = -1 } payoutValidRes.RecordStatusMap = recordStatusMap // 判断金额 leftAmount += opts.Amount if leftAmount > PayoutAmountLimit { return payoutValidRes, fmt.Errorf("amount limit"), 104 } if opts.ItemId > 0 { // 首次提现必须是0.1 if num1+num2+num3+num4 <= 0 { if opts.ItemId != uint32(PayoutItemId1) { return payoutValidRes, fmt.Errorf("ItemId error"), 108 } } switch opts.ItemId { case 1: if num1 >= PayoutRecordLimit1 { return payoutValidRes, fmt.Errorf("no payout chance"), 105 } case 2: if num2 >= PayoutRecordLimit2 { return payoutValidRes, fmt.Errorf("no payout chance"), 105 } case 3: if num3 >= PayoutRecordLimit3 { return payoutValidRes, fmt.Errorf("no payout chance"), 105 } case 4: if num4 >= PayoutRecordLimit4 { return payoutValidRes, fmt.Errorf("no payout chance"), 105 } default: log.Infof("checkPayoutValid error: uuid[%s] record.ItemId[%d] not dispatch No.2", uuid, opts.ItemId) return payoutValidRes, fmt.Errorf("no payout chance"), 105 } } else { } // if leftNum1 <= 0 { // return payoutValidRes, fmt.Errorf("no payout chance"), 105 // } if opts.ItemId > 0 { uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay, opts.ItemId) value, err := uc.cache.GetValue(ctx, uuidRedisKey) if err != nil { return payoutValidRes, err, 1 } // 次日才能进行第二次提现(逻辑保留) if value != "" { return payoutValidRes, fmt.Errorf("no payout chance, please try tomorrow"), 106 } } // 登录第三日才能体现第二次 // if leftNum1 == 1 && user.LoginDays < 3 && opts.ItemId > 0 { // return payoutValidRes, fmt.Errorf("no payout chance, please try tomorrow") // } if len(opts.Account) > 0 { // 如果是qa环境,test账号不限制提现次数 // if uc.conf.Server.Env == "qa" && opts.Account == "pagsmile_test_success@pagsmile.com" { if uc.conf.Server.Env == "qa" && strings.Contains(opts.Account, "@pagsmile.com") { return payoutValidRes, nil, 0 } // 一个账号只能提现2次 count, err := uc.repo.GetPayoutRecordCountByAccount(ctx, opts.Account) if err != nil { return payoutValidRes, err, 1 } if count >= PayoutRecordLimit1+PayoutRecordLimit2+PayoutRecordLimit3+PayoutRecordLimit4 { return payoutValidRes, fmt.Errorf("account payouted limit"), 107 } } return payoutValidRes, nil, 0 } // 1+2位数 func (uc *EonlineUsecase) checkPayoutValidNew(ctx context.Context, uuid string, opts *PayoutAddtionalReq) (*PayoutValidRes, error, int32) { x, err := uc.CheckInfo(ctx, &CheckInfoReq{ Uuid: uuid, Ts: opts.Ts, }) if err != nil { return nil, fmt.Errorf("error: %w", err), 1 } if x.CanCheckPayOut == 1 { return nil, fmt.Errorf("can't payout"), 150 } payoutValidRes := &PayoutValidRes{ LeftNum: make(map[uint]int), RecordStatusMap: make(map[uint]uint8), } // 判断是否可以提现,国家判断 // if opts.Country != "US" && opts.Country != "CA" && opts.Country != "IN" && opts.Country != "PH" { // if !canPayoutCountry(opts.Country) { // return payoutValidRes, fmt.Errorf("country [%s] not support", opts.Country), 101 // } // 判断金额是否一致 if int32(opts.Amount*100) != config.PublicCheckCoin { return payoutValidRes, fmt.Errorf("amount error"), 102 } records, err := uc.repo.GetPayoutRecordListByUuid(ctx, uuid) if err != nil { return payoutValidRes, err, 1 } recordStatusMap := make(map[uint]uint8) for _, record := range records { if _, ok := recordStatusMap[record.ItemId]; !ok { recordStatusMap[record.ItemId] = record.Status } } payoutValidRes.RecordStatusMap = recordStatusMap status, ok := recordStatusMap[uint(PayoutItemId4)] if ok { if status == uint8(PayoutStatusPayouted) || status == uint8(PayoutStatusPayouting) { return payoutValidRes, fmt.Errorf("no payout chance"), 105 } } // keyDay := "" // if uc.conf.Server.Env == "qa" { // ts, _ := strconv.ParseInt(opts.Ts, 10, 64) // keyDay = time.Unix(ts, 0).Format("20060102") // } // // uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay) // value, err := uc.cache.GetValue(ctx, uuidRedisKey) // if err != nil { // return payoutValidRes, err // } // // // 次日才能进行第二次体现(逻辑保留) // if value != "" && opts.ItemId > 0 { // return payoutValidRes, fmt.Errorf("no payout chance, please try tomorrow") // } return payoutValidRes, nil, 0 } // PayoutAddtionalReq 支付附加请求 type PayoutAddtionalReq struct { Platform string DeviceId string Version string EcpmLvl uint32 ItemId uint32 Ip string Amount float64 Country string Account string Ts string Email string } // Payout 支付请求 func (uc *EonlineUsecase) Payout(ctx context.Context, uuid string, opts *PayoutAddtionalReq, req *PayoutReq) (*PayoutReplyData, error, int32) { // 定义一个uuid的redis key keyDay := "" if uc.conf.Server.Env == "qa" { ts, _ := strconv.ParseInt(opts.Ts, 10, 64) keyDay = time.Unix(ts, 0).Format("20060102") } uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay, opts.ItemId) // 定义一个uuid的redis lock key, 用于锁定, 防止并发 uuidRedisLockKey := GetUuidLockRedisKey(uuid, keyDay) lock, _ := uc.cache.RedisLock(ctx, uuidRedisLockKey, 1, 20) if !lock { return nil, fmt.Errorf("lock error"), 1 } defer func() { uc.cache.RedisUnLock(ctx, uuidRedisLockKey) }() country, currentIp := uc.GetCountryCodeByIp(ctx, opts.Ip) opts.Country = country opts.Ip = currentIp // 验证提现是否有效 var err error var errCode int32 if opts.ItemId == uint32(PayoutItemId4) { _, err, errCode = uc.checkPayoutValidNew(ctx, uuid, opts) } else { _, err, errCode = uc.checkPayoutValid(ctx, uuid, opts) } if err != nil { switch errCode { case 101: errCode = 5 case 102: errCode = 6 case 103: errCode = 7 case 104: errCode = 8 case 105: errCode = 9 case 106: errCode = 10 case 107: errCode = 11 case 150: errCode = 12 default: log.Infof("checkPayoutValid error: errCode[%d]", errCode) errCode = 1 } return nil, err, errCode } // 判断当前用户是否存在 user, err := uc.repo.GetUserByUuid(ctx, uuid) if err != nil { return nil, err, 1 } // 支付的时候需要替换掉country code // if country == "US" { // 美国 // req.Country = "USA" // } else if country == "CA" { // 加拿大 // req.Country = "CAN" // } else if country == "IN" { // 印度 // req.Country = "IND" // } else if country == "PH" { // 菲律宾 // req.Country = "PHL" // } req.Country = convertCountry(country) // 创建提现用户 if user.Id == 0 { // 记录提现的用户 _, err = uc.repo.CreatePayoutUser(ctx, &PayoutUser{ Uuid: uuid, DeviceId: opts.DeviceId, Platform: opts.Platform, Version: opts.Version, Country: country, Ip: opts.Ip, LoginDays: 1, }) if err != nil { return nil, err, 1 } } // 记录提现及操作 var payoutReplyData PayoutReplyData var payoutStatus uint8 // 记录提现状态 err = uc.tx.InTx(ctx, func(ctx context.Context) error { // 创建提现记录 // 以下参数固定 req.AccountType = "EMAIL" req.Method = "WALLET" req.Channel = "PayPal" req.FeeBear = "merchant" // beneficiary , merchant // 如果金额大于等于0.3, 收款方承担手续费 if opts.Amount > 0.3 { req.FeeBear = "beneficiary" } req.SourceCurrency = "USD" req.ArrivalCurrency = "USD" reqMd5 := utils.MD5Any(req) customCode := fmt.Sprintf("PGS_%s", utils.MD5Any(fmt.Sprintf("%s-%d", reqMd5, time.Now().Unix()))) // 类似唯一订单号 req.CustomCode = customCode record, err := uc.repo.CreatePayoutRecord(ctx, &PayoutRecord{ Uuid: uuid, Channel: req.Channel, ItemId: uint(opts.ItemId), Amount: opts.Amount, Account: req.Account, Currency: req.SourceCurrency, Status: uint8(PayoutStatusPayouting), RecordNo: req.CustomCode, Version: opts.Version, Email: opts.Email, }) if err != nil { return fmt.Errorf("create payout record error: %v", err) } // 第三方提现 x, err := uc.pscli.out.Payout(ctx, req) if err != nil { return err } payoutReplyData = *x payoutStatus = uint8(PayoutStatusPayouting) // 等待回调更新状态 err = uc.repo.UpdatePayoutRecordNotify(ctx, payoutReplyData.CustomCode, payoutReplyData.Id, payoutStatus, "") if err != nil { return fmt.Errorf("update payout record_%d status error: %v", record.Id, err) } return nil }) if err != nil { return nil, err, 1 } // 记录此次提现,用于判断是否可以继续提现 uc.cache.WriteValue(ctx, uuidRedisKey, "1", 24*3600) return &payoutReplyData, nil, 0 } // PayoutBrazil 支付请求 func (uc *EonlineUsecase) PayoutBrazil(ctx context.Context, uuid string, opts *PayoutAddtionalReq, req *PayoutReq) (*PayoutReplyData, error, int32) { // 定义一个uuid的redis key keyDay := "" if uc.conf.Server.Env == "qa" { ts, _ := strconv.ParseInt(opts.Ts, 10, 64) keyDay = time.Unix(ts, 0).Format("20060102") } uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay, opts.ItemId) // 定义一个uuid的redis lock key, 用于锁定, 防止并发 uuidRedisLockKey := GetUuidLockRedisKey(uuid, keyDay) lock, _ := uc.cache.RedisLock(ctx, uuidRedisLockKey, 1, 20) if !lock { return nil, fmt.Errorf("lock error"), 1 } defer func() { uc.cache.RedisUnLock(ctx, uuidRedisLockKey) }() country, currentIp := uc.GetCountryCodeByIp(ctx, opts.Ip) if country != "BR" { // return nil, fmt.Errorf("Not Brazil") country = "BR" } opts.Country = country opts.Ip = currentIp // 验证提现是否有效 var err error var errCode int32 if opts.ItemId == uint32(PayoutItemId4) { _, err, errCode = uc.checkPayoutValidNew(ctx, uuid, opts) } else { _, err, errCode = uc.checkPayoutValid(ctx, uuid, opts) } if err != nil { switch errCode { case 101: errCode = 5 case 102: errCode = 6 case 103: errCode = 7 case 104: errCode = 8 case 105: errCode = 9 case 106: errCode = 10 case 107: errCode = 11 case 108: errCode = 25 case 150: errCode = 12 default: log.Infof("checkPayoutValid error: errCode[%d]", errCode) errCode = 1 } return nil, err, errCode } // 判断当前用户是否存在 user, err := uc.repo.GetUserByUuid(ctx, uuid) if err != nil { return nil, err, 1 } // 支付的时候需要替换掉country code req.Country = convertCountry(country) // 创建提现用户 if user.Id == 0 { // 记录提现的用户 _, err = uc.repo.CreatePayoutUser(ctx, &PayoutUser{ Uuid: uuid, DeviceId: opts.DeviceId, Platform: opts.Platform, Version: opts.Version, Country: country, Ip: opts.Ip, LoginDays: 1, }) if err != nil { return nil, err, 1 } } if len(req.ClientData) > 0 { uc.updateUserClientData(ctx, uuid, req.ClientData) } // 记录提现及操作 var payoutReplyData PayoutReplyData var payoutStatus uint8 // 记录提现状态 err = uc.tx.InTx(ctx, func(ctx context.Context) error { // 创建提现记录 // 以下参数固定 req.Method = "PIX" // req.Channel = "PayPal" req.FeeBear = "merchant" // beneficiary , merchant // 如果金额大于等于0.3, 收款方承担手续费 if opts.Amount > 0.3 { req.FeeBear = "beneficiary" } req.SourceCurrency = "USD" req.ArrivalCurrency = "BRL" reqMd5 := utils.MD5Any(req) customCode := fmt.Sprintf("PGS_%s", utils.MD5Any(fmt.Sprintf("%s-%d", reqMd5, time.Now().Unix()))) // 类似唯一订单号 req.CustomCode = customCode record, err := uc.repo.CreatePayoutRecord(ctx, &PayoutRecord{ Uuid: uuid, Channel: req.Channel, ItemId: uint(opts.ItemId), Amount: opts.Amount, Account: req.Account, Currency: req.SourceCurrency, Status: uint8(PayoutStatusPayouting), RecordNo: req.CustomCode, Version: opts.Version, }) if err != nil { return fmt.Errorf("create payout record error: %v", err) } // 第三方提现 x, err := uc.pscli.out.Payout(ctx, req) if err != nil { return err } payoutReplyData = *x payoutStatus = uint8(PayoutStatusPayouting) // 等待回调更新状态 err = uc.repo.UpdatePayoutRecordNotify(ctx, payoutReplyData.CustomCode, payoutReplyData.Id, payoutStatus, "") if err != nil { return fmt.Errorf("update payout record_%d status error: %v", record.Id, err) } return nil }) if err != nil { return nil, err, 1 } // 记录此次提现,用于判断是否可以继续提现 uc.cache.WriteValue(ctx, uuidRedisKey, "1", 24*3600) orderId := GetPayoutOrderIdRedisKey(payoutReplyData.Id) uc.cache.WriteValue(ctx, orderId, uuidRedisKey, 15*3600) return &payoutReplyData, nil, 0 } // PayoutNotify 支付回调 func (uc *EonlineUsecase) PayoutNotify(ctx context.Context, req *PayoutNotifyReq) error { value, _ := json.Marshal(req) fmt.Printf("[%s] payout notify: %s \n", time.Now().Format("2006-01-02 15:04:05"), string(value)) log.Infof("[%s] payout notify: %s \n", time.Now().Format("2006-01-02 15:04:05"), string(value)) err := uc.pscli.out.PayoutNotify(ctx, req) if err != nil { return err } // 测试用代码 // callTest := func() { // uuidRedisKey := "pt_6b59480226f21f10a8939807dded2679_20250518_1" // orderTs := "TS202505080744025sxBs5nA47qPB" // err = uc.cache.WriteValue(ctx, uuidRedisKey, "1", 24*3600) // if err != nil { // err = nil // } // orderId := GetPayoutOrderIdRedisKey(orderTs) // err = uc.cache.WriteValue(ctx, orderId, uuidRedisKey, 15*3600) // if err != nil { // err = nil // } // // // 查找删除 // ctx2 := ctx // orderId = GetPayoutOrderIdRedisKey(orderTs) // value2, err2 := uc.cache.GetValue(ctx2, orderId) // if err2 == nil { // if value2 != "" { // err2 = uc.cache.DelValue(ctx2, value2) // if err2 == nil { // log.Infof("del GetUuidPayoutRedisKey: %s", value2) // // err2 = uc.cache.DelValue(ctx2, orderId) // if err2 == nil { // log.Infof("del redis key orderId: %s", orderId) // } else { // log.Infof("del redis key orderId: %s err[%v]", orderId, err2) // } // } else { // log.Infof("del GetUuidPayoutRedisKey error: %s err[%v]", value2, err2) // } // } else { // log.Infof("GetPayoutOrderIdRedisKey error: key[%s] value2 is null", orderId) // } // } else { // log.Infof("GetPayoutOrderIdRedisKey error: key[%s] err[%v]", orderId, err2) // } // } // 判断回调是否有效 _, err = uc.repo.GetPayoutRecordByRecordNo(ctx, req.CustomCode) if err != nil { return fmt.Errorf("get payout record error: %v", err) } // callTest() reportData := GetReportData(req.PayoutId) // 更新提现记录状态 var payoutStatus uint8 switch req.Status { case "PAID": payoutStatus = uint8(PayoutStatusPayouted) if reportData != nil && reportData.Adjust != nil && reportData.ShuShu != nil { adjust := GetAdjustData2(reportData.Adjust, "") shuShu := GetShuShuData2(reportData.ShuShu, "") SendReport(ctx, adjust, shuShu) } case "REJECTED": payoutStatus = uint8(PayoutStatusPayoutFailed) ctx2 := ctx // ctx2 := context.Background() orderId := GetPayoutOrderIdRedisKey(req.PayoutId) value2, err2 := uc.cache.GetValue(ctx2, orderId) if err2 == nil { if value2 != "" { err2 = uc.cache.DelValue(ctx2, value2) if err2 == nil { log.Infof("del GetUuidPayoutRedisKey: %s", value2) // err2 = uc.cache.DelValue(ctx2, orderId) // if err2 == nil { // log.Infof("del redis key orderId: %s", orderId) // } else { // log.Infof("del redis key orderId: %s err[%v]", orderId, err2) // } } else { log.Infof("del GetUuidPayoutRedisKey error: %s err[%v]", value2, err2) } } else { log.Infof("GetPayoutOrderIdRedisKey error: key[%s] value2 is null", orderId) } } else { log.Infof("GetPayoutOrderIdRedisKey error: key[%s] err[%v]", orderId, err2) } if reportData != nil && reportData.Adjust != nil && reportData.ShuShu != nil { adjust := GetAdjustData2(reportData.Adjust, req.Msg) shuShu := GetShuShuData2(reportData.ShuShu, req.Msg) SendReport(ctx, adjust, shuShu) } default: payoutStatus = uint8(PayoutStatusPayouting) } err = uc.repo.UpdatePayoutRecordNotify(ctx, req.CustomCode, req.PayoutId, payoutStatus, req.Msg) if err != nil { return fmt.Errorf("update payout record status error: %v", err) } // //存入redis给请求提现的用户 // payoutNotifyRedisKey := GetPayoutNotifyRedisKey(req.PayoutId) // err = uc.cache.WriteValue(ctx, payoutNotifyRedisKey, string(value), 24*3600) // if err != nil { // return fmt.Errorf("write redis error: %v", err) // } return nil } // PayoutNotify 支付回调 func (uc *EonlineUsecase) PayoutNotify2(ctx context.Context, req *PayoutNotifyReq) error { value, _ := json.Marshal(req) fmt.Printf("[%s] payout notify: %s \n", time.Now().Format("2006-01-02 15:04:05"), string(value)) log.Infof("[%s] payout notify: %s \n", time.Now().Format("2006-01-02 15:04:05"), string(value)) // err := uc.pscli.out.PayoutNotify(ctx, req) // if err != nil { // return err // } // 判断回调是否有效 _, err := uc.repo.GetPayoutRecordByRecordNo(ctx, req.CustomCode) if err != nil { return fmt.Errorf("get payout record error: %v", err) } // callTest() reportData := GetReportData(req.PayoutId) // 更新提现记录状态 var payoutStatus uint8 switch req.Status { case "PAID": payoutStatus = uint8(PayoutStatusPayouted) if reportData != nil && reportData.Adjust != nil && reportData.ShuShu != nil { adjust := GetAdjustData2(reportData.Adjust, "") shuShu := GetShuShuData2(reportData.ShuShu, "") SendReport(ctx, adjust, shuShu) } case "REJECTED": payoutStatus = uint8(PayoutStatusPayoutFailed) ctx2 := ctx // ctx2 := context.Background() orderId := GetPayoutOrderIdRedisKey(req.PayoutId) value2, err2 := uc.cache.GetValue(ctx2, orderId) if err2 == nil { if value2 != "" { err2 = uc.cache.DelValue(ctx2, value2) if err2 == nil { log.Infof("del GetUuidPayoutRedisKey: %s", value2) // err2 = uc.cache.DelValue(ctx2, orderId) // if err2 == nil { // log.Infof("del redis key orderId: %s", orderId) // } else { // log.Infof("del redis key orderId: %s err[%v]", orderId, err2) // } } else { log.Infof("del GetUuidPayoutRedisKey error: %s err[%v]", value2, err2) } } else { log.Infof("GetPayoutOrderIdRedisKey error: key[%s] value2 is null", orderId) } } else { log.Infof("GetPayoutOrderIdRedisKey error: key[%s] err[%v]", orderId, err2) } if reportData != nil && reportData.Adjust != nil && reportData.ShuShu != nil { adjust := GetAdjustData2(reportData.Adjust, req.Msg) shuShu := GetShuShuData2(reportData.ShuShu, req.Msg) SendReport(ctx, adjust, shuShu) } default: payoutStatus = uint8(PayoutStatusPayouting) } err = uc.repo.UpdatePayoutRecordNotify(ctx, req.CustomCode, req.PayoutId, payoutStatus, req.Msg) if err != nil { return fmt.Errorf("update payout record status error: %v", err) } return nil } // ParseIp 解析IP func (uc *EonlineUsecase) ParseIp(ctx context.Context, ip string) (*geo.GeoInfo, error) { return uc.geocli.Parse(ip) } // 通过IP获取对应country code 和 country ip, 如果IP为空,从服务端获取 func (uc *EonlineUsecase) GetCountryCodeByIp(ctx context.Context, ip string) (string, string) { var country string var currentIp string currentIp = ip // 获取国家 // 强制通过服务端获取IP,不支持客户端传递, QA环境仍然支持客户端传递 if ip == "" || uc.conf.Server.Env != "qa" { httpCtx := xhttp.RequestFromContext(ctx) if httpCtx != nil { // 获取当前的IP地址 currentIp = utils.GetClientPublicIP(httpCtx.Request()) } else { uc.log.WithContext(ctx).Errorf("get http context error") return "", "" } } // 解析IP地址 geonInfo, err := uc.geocli.Parse(currentIp) if err != nil { uc.log.WithContext(ctx).Errorf("parse ip(%s) error: %v", currentIp, err) return currentIp, "" } country = geonInfo.Country.ISOCode fmt.Println("parse ip success: ", currentIp, country) return country, currentIp } // PayoutCheck 检查提现状态 func (uc *EonlineUsecase) PayoutCheck(ctx context.Context, recordNo string) (uint8, error) { record, err := uc.repo.GetPayoutRecordByRecordNo(ctx, recordNo) if err != nil { return 0, err } return record.Status, nil } // PayoutCheck 检查提现状态 func (uc *EonlineUsecase) GetPayoutRecordByAccount(ctx context.Context, account string, status, pageIndex, pageSize int) ([]*PayoutRecord, error) { record, err := uc.repo.GetPayoutRecordByAccountStatus(ctx, account, int(status), int(pageIndex), int(pageSize)) if err != nil { return nil, err } return record, nil } type SubmitCheckReq struct { Account string Uuid string } type SubmitCheckReply struct { Result int32 // 0成功,1失败,2以前提交过,还没审核结果,3以前提交过,并审核通过(以前提交过,但审核失败的,可以继续提交), } const ( StringNull = "" // 审核无结果 CheckResultFail = "0" // 审核没通过 CheckResultSuccess = "1" // 审核通过 ) // 提交审核,0成功,1失败,2以前提交过,还没审核结果,3以前提交过,并审核通过(以前提交过,但审核失败的,可以继续提交), func (uc *EonlineUsecase) SubmitCheck(ctx context.Context, req *SubmitCheckReq) (*SubmitCheckReply, error) { reply := &SubmitCheckReply{} // 查询是否已经提交过 v, err := uc.getCheckSubmit(ctx, req.Account) if err != nil { reply.Result = 1 return reply, err } if v == 1 { // 提交过,检查审核结果 v2, err := uc.getCheckResult(ctx, req.Account) if err != nil { reply.Result = 1 return reply, err } // 还没审核结果 if v2 == 0 { reply.Result = 2 return reply, nil } // 审核通过的 if v2 == 2 { reply.Result = 3 return reply, nil } } // 检查提交人数 // v3, err := uc.getCheckSubmitNum(ctx) // _ = v3 // 提交计数 err = uc.increaseCheckSubmitNum(ctx) if err != nil { reply.Result = 1 return reply, err } // 建立[uuid, paypal] err = uc.setUuid2Paypal(ctx, req.Account, req.Uuid) if err != nil { reply.Result = 1 return reply, err } // 不加redis锁,并发写入提交记录,也没什么影响 // 存入redis key := GetCheckSubmitRedisKey(req.Account) uc.cache.WriteValue(ctx, key, req.Uuid, 0) return reply, nil } type CheckInfoReq struct { Uuid string Ts string } type CheckInfoReply struct { CanCheckSubmit uint32 CheckSubmit int32 CheckResult int32 CheckPayout int32 CanCheckPayOut int32 CheckResultFailedDesc string } func (uc *EonlineUsecase) CheckInfo(ctx context.Context, req *CheckInfoReq) (*CheckInfoReply, error) { reply := &CheckInfoReply{} acc, err := uc.getUuid2Paypal(ctx, req.Uuid) if err != nil { return nil, err } if acc == StringNull { reply.CheckSubmit = 0 reply.CheckResult = 0 reply.CheckPayout = 0 } else { // 提交身份文件验证情况 v, err := uc.getCheckSubmit(ctx, acc) if err != nil { return nil, err } if v == 0 { reply.CheckSubmit = 0 } else { reply.CheckSubmit = 1 } // 身份文件审核有反馈的情况 v2, err := uc.getCheckResult(ctx, acc) if err != nil { return nil, err } if v2 != 0 { if v2 == 2 { reply.CheckResult = 2 } else { reply.CheckResult = 1 reply.CheckResultFailedDesc, err = uc.getCheckResultFailedDesc(ctx, acc) if err != nil { return nil, err } } } // 提交身份文件奖励5美元领取的情况,0没有提现记录,1提现中,2提现成功,3提现失败 records, err := uc.repo.GetPayoutRecordListByUuid(ctx, req.Uuid) if err != nil { return nil, err } for _, record := range records { if record.ItemId == uint(PayoutItemId4) { switch record.Status { case uint8(PayoutStatusPayouting): reply.CheckPayout = 1 case uint8(PayoutStatusPayouted): reply.CheckPayout = 2 case uint8(PayoutStatusPayoutFailed): reply.CheckPayout = 3 } break } } } // 获得当前提交审核计数 v3, err := uc.getCheckSubmitNum(ctx) if err != nil { return nil, err } reply.CanCheckSubmit = v3 log.Infof("CheckInfo: getCheckSubmitNum[%d]", v3) if reply.CanCheckSubmit < config.PublicCheckNum { reply.CanCheckSubmit = 1 } else { reply.CanCheckSubmit = 0 } var resultTixian int reply.CanCheckPayOut = 1 // 身份文件审核过的提现奖励5美元 能否 提现,0能提现,1条件不满足,不能提现,2当天已经提现过,不能再次提现 if reply.CheckSubmit == 1 && reply.CheckResult == 2 && reply.CheckPayout == 0 { resultTixian, err = uc.GetCashStatus(ctx, req.Uuid, req.Ts, uint32(PayoutItemId4)) if err != nil { return nil, fmt.Errorf("error: %w", err) } if resultTixian == 1 { reply.CanCheckPayOut = 2 } else { reply.CanCheckPayOut = 0 } } return reply, nil } // 获取已经提交身份审核计数 func (uc *EonlineUsecase) getCheckSubmitNum(ctx context.Context) (uint32, error) { key := GetCheckSubmitNumRedisKey() v, err := uc.cache.GetValue(ctx, key) if err != nil { return 0, err } if v == StringNull { return 0, nil } else { x, err := strconv.Atoi(v) if err != nil { return 0, err } return uint32(x), nil } } // 提交身份审核计数 自增 func (uc *EonlineUsecase) increaseCheckSubmitNum(ctx context.Context) error { key := GetCheckSubmitNumRedisKey() v, err := uc.cache.GetValue(ctx, key) if err != nil { return err } var x int if v != StringNull { x, err = strconv.Atoi(v) if err != nil { return err } } err = uc.cache.WriteValue(ctx, key, x+1, 0) if err != nil { return err } return nil } // 提交身份文件验证情况,0没有提交,1提交过 func (uc *EonlineUsecase) getCheckSubmit(ctx context.Context, account string) (int32, error) { key := GetCheckSubmitRedisKey(account) v, err := uc.cache.GetValue(ctx, key) if err != nil { return 0, err } if v == StringNull { return 0, nil } else { return 1, nil } } // 身份文件审核有反馈的情况,0没有记录,1审核没通过,2审核通过 func (uc *EonlineUsecase) getCheckResult(ctx context.Context, account string) (int32, error) { key := GetCheckResultRedisKey(account) v, err := uc.cache.GetValue(ctx, key) if err != nil { return 0, err } if v == StringNull { return 0, nil } if v == CheckResultSuccess { return 2, nil } else { return 1, nil } } // 提交身份文件奖励5美元领取的情况,0没有领取记录,1有领取记录 func (uc *EonlineUsecase) getCheckPayout(ctx context.Context, account string) (int32, error) { return 0, nil } // 存储[uuid, paypal] func (uc *EonlineUsecase) setUuid2Paypal(ctx context.Context, account, uuid string) error { key := GetUuid2PaypalRedisKey(uuid) err := uc.cache.WriteValue(ctx, key, account, 0) if err != nil { return err } return nil } // 根据uuid获取paypal func (uc *EonlineUsecase) getUuid2Paypal(ctx context.Context, uuid string) (string, error) { key := GetUuid2PaypalRedisKey(uuid) v, err := uc.cache.GetValue(ctx, key) if err != nil { return StringNull, err } return v, nil } // 查询当天是否有提现记录,0没有,1有 func (uc *EonlineUsecase) GetCashStatus(ctx context.Context, uuid, ts string, itemId uint32) (int, error) { keyDay := "" if uc.conf.Server.Env == "qa" { ts, _ := strconv.ParseInt(ts, 10, 64) keyDay = time.Unix(ts, 0).Format("20060102") } uuidRedisKey := GetUuidPayoutRedisKey(uuid, keyDay, itemId) value, err := uc.cache.GetValue(ctx, uuidRedisKey) if err != nil { return 0, err } if value != "" { return 1, nil } return 0, nil } // 获取审核没通过的原因描述信息 func (uc *EonlineUsecase) getCheckResultFailedDesc(ctx context.Context, account string) (string, error) { key := GetCheckResultFailRedisKey(account) v, err := uc.cache.GetValue(ctx, key) if err != nil { return "", err } return v, nil }