币商充值
This commit is contained in:
parent
f8a8d4d65f
commit
a2ef52c8d9
@ -19,6 +19,7 @@ import (
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/rechargeagency"
|
||||
"chatapp3-golang/internal/service/rechargereward"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||
@ -41,6 +42,7 @@ func main() {
|
||||
lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet)
|
||||
rechargeAgencyService := rechargeagency.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||
rechargeRewardService := rechargereward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||
@ -67,6 +69,7 @@ func main() {
|
||||
GameProviders: gameProviders,
|
||||
LuckyGift: luckyGiftService,
|
||||
HostCenter: hostCenterService,
|
||||
RechargeAgency: rechargeAgencyService,
|
||||
RechargeReward: rechargeRewardService,
|
||||
RegisterReward: registerRewardService,
|
||||
RoomTurnoverReward: roomTurnoverRewardService,
|
||||
|
||||
@ -19,6 +19,7 @@ type Gateways struct {
|
||||
Room *RoomGateway
|
||||
Team *TeamGateway
|
||||
Wallet *WalletGateway
|
||||
Freight *FreightGateway
|
||||
}
|
||||
|
||||
// NewGateways 基于共享 HTTP 客户端创建所有 Java 网关。
|
||||
@ -36,6 +37,7 @@ func NewGateways(cfg config.Config) Gateways {
|
||||
Room: &RoomGateway{client: client},
|
||||
Team: &TeamGateway{client: client},
|
||||
Wallet: &WalletGateway{client: client},
|
||||
Freight: &FreightGateway{client: client},
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,6 +186,11 @@ func (g *Gateways) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand
|
||||
return g.Wallet.ChangeGoldBalance(ctx, cmd)
|
||||
}
|
||||
|
||||
// PageFreightSellers 透传到 H5 币商列表接口。
|
||||
func (g *Gateways) PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (FreightSellerPage, error) {
|
||||
return g.Freight.PageFreightSellers(ctx, authorization, sysOrigin, cursor, limit)
|
||||
}
|
||||
|
||||
// AuthGateway 负责认证相关接口。
|
||||
type AuthGateway struct {
|
||||
client *Client
|
||||
@ -383,3 +390,13 @@ func (g *WalletGateway) ExistsGoldEvent(ctx context.Context, eventID string) (bo
|
||||
func (g *WalletGateway) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand) error {
|
||||
return g.client.ChangeGoldBalance(ctx, cmd)
|
||||
}
|
||||
|
||||
// FreightGateway 负责货运代理/币商相关接口。
|
||||
type FreightGateway struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// PageFreightSellers 查询 H5 可见币商列表。
|
||||
func (g *FreightGateway) PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (FreightSellerPage, error) {
|
||||
return g.client.PageFreightSellers(ctx, authorization, sysOrigin, cursor, limit)
|
||||
}
|
||||
|
||||
@ -2,10 +2,10 @@ package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"chatapp3-golang/internal/config"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -13,6 +13,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/config"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@ -213,6 +215,57 @@ type UserTotalRecharge struct {
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
type FreightSellerPage struct {
|
||||
Records []FreightSellerRecord `json:"records"`
|
||||
Total int64 `json:"total"`
|
||||
Current int `json:"current"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
type FreightSellerRecord struct {
|
||||
ID Int64Value `json:"id"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
UserID Int64Value `json:"userId"`
|
||||
EarnPoints DecimalString `json:"earnPoints"`
|
||||
ConsumptionPoints DecimalString `json:"consumptionPoints"`
|
||||
Balance DecimalString `json:"balance"`
|
||||
Close bool `json:"close"`
|
||||
Display bool `json:"display"`
|
||||
Dealer bool `json:"dealer"`
|
||||
SuperDealer bool `json:"superDealer"`
|
||||
SupportedCountries string `json:"supportedCountries"`
|
||||
ContactInfo string `json:"contactInfo"`
|
||||
H5Display bool `json:"h5Display"`
|
||||
SellerQuantity int `json:"sellerQuantity"`
|
||||
RealSellerQuantity int64 `json:"realSellerQuantity"`
|
||||
BecomeDays int `json:"becomeDays"`
|
||||
TransactionCount int64 `json:"transactionCount"`
|
||||
IsFollow bool `json:"isFollow"`
|
||||
UserBaseInfo FreightSellerProfile `json:"userBaseInfo"`
|
||||
NationalFlag []string `json:"nationalFlag"`
|
||||
OwnNationalFlag string `json:"ownNationalFlag"`
|
||||
}
|
||||
|
||||
type FreightSellerProfile struct {
|
||||
ID Int64Value `json:"id"`
|
||||
Account string `json:"account"`
|
||||
UserAvatar string `json:"userAvatar"`
|
||||
UserNickname string `json:"userNickname"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
CountryName string `json:"countryName"`
|
||||
RegionCode string `json:"regionCode"`
|
||||
OriginSys string `json:"originSys"`
|
||||
}
|
||||
|
||||
type freightSellerPageResponse struct {
|
||||
Status *bool `json:"status"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
Message string `json:"message"`
|
||||
Body *FreightSellerPage `json:"body"`
|
||||
Data *FreightSellerPage `json:"data"`
|
||||
FreightSellerPage
|
||||
}
|
||||
|
||||
type resultResponse[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@ -577,6 +630,42 @@ func (c *Client) GetThisMonthTotalPersonalRecharge(ctx context.Context, userID i
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (c *Client) PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (FreightSellerPage, error) {
|
||||
values := url.Values{}
|
||||
values.Set("sysOrigin", strings.TrimSpace(sysOrigin))
|
||||
values.Set("dealer", "true")
|
||||
values.Set("cursor", strconv.Itoa(cursor))
|
||||
values.Set("limit", strconv.Itoa(limit))
|
||||
|
||||
endpoint := strings.TrimRight(c.cfg.Java.OtherBaseURL, "/") + "/user/freight-balance/page?" + values.Encode()
|
||||
headers := javaAppHeadersFromAuthorization(authorization)
|
||||
if strings.TrimSpace(authorization) != "" {
|
||||
headers.Set("Authorization", strings.TrimSpace(authorization))
|
||||
}
|
||||
|
||||
var resp freightSellerPageResponse
|
||||
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
|
||||
return FreightSellerPage{}, err
|
||||
}
|
||||
if resp.Status != nil && !*resp.Status {
|
||||
message := strings.TrimSpace(resp.ErrorMsg)
|
||||
if message == "" {
|
||||
message = strings.TrimSpace(resp.Message)
|
||||
}
|
||||
if message == "" {
|
||||
message = "freight seller page failed"
|
||||
}
|
||||
return FreightSellerPage{}, errors.New(message)
|
||||
}
|
||||
if resp.Body != nil {
|
||||
return *resp.Body, nil
|
||||
}
|
||||
if resp.Data != nil {
|
||||
return *resp.Data, nil
|
||||
}
|
||||
return resp.FreightSellerPage, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile, error) {
|
||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByUserId?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
|
||||
var resp resultResponse[UserProfile]
|
||||
|
||||
17
internal/model/recharge_agency_models.go
Normal file
17
internal/model/recharge_agency_models.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// RechargeAgencySellerInfo stores H5-only profile fields for recharge agency sellers.
|
||||
type RechargeAgencySellerInfo struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_recharge_agency_seller_info_user,priority:1"`
|
||||
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_recharge_agency_seller_info_user,priority:2"`
|
||||
Whatsapp string `gorm:"column:whatsapp;size:64"`
|
||||
CreateTime time.Time `gorm:"column:create_time"`
|
||||
UpdateTime time.Time `gorm:"column:update_time"`
|
||||
}
|
||||
|
||||
func (RechargeAgencySellerInfo) TableName() string {
|
||||
return "recharge_agency_seller_info"
|
||||
}
|
||||
59
internal/router/recharge_agency_routes.go
Normal file
59
internal/router/recharge_agency_routes.go
Normal file
@ -0,0 +1,59 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chatapp3-golang/internal/service/rechargeagency"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// registerRechargeAgencyRoutes 注册充值代理 H5 币商名单路由。
|
||||
func registerRechargeAgencyRoutes(engine *gin.Engine, javaClient authGateway, service *rechargeagency.Service) {
|
||||
if service == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handler := func(c *gin.Context) {
|
||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||
|
||||
resp, err := service.PageSellers(c.Request.Context(), mustAuthUser(c), cursor, limit)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
}
|
||||
|
||||
listGroup := engine.Group("/app/h5/recharge-agency-list")
|
||||
listGroup.Use(authMiddleware(javaClient))
|
||||
listGroup.GET("/sellers", handler)
|
||||
|
||||
agencyGroup := engine.Group("/app/h5/recharge-agency")
|
||||
agencyGroup.Use(authMiddleware(javaClient))
|
||||
agencyGroup.GET("/sellers", handler)
|
||||
agencyGroup.GET("/profile", func(c *gin.Context) {
|
||||
resp, err := service.GetProfile(c.Request.Context(), mustAuthUser(c))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
agencyGroup.POST("/profile/whatsapp", func(c *gin.Context) {
|
||||
var req rechargeagency.UpdateWhatsappRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
writeError(c, rechargeagency.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.UpdateWhatsapp(c.Request.Context(), mustAuthUser(c), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
}
|
||||
161
internal/router/recharge_agency_routes_test.go
Normal file
161
internal/router/recharge_agency_routes_test.go
Normal file
@ -0,0 +1,161 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/integration"
|
||||
"chatapp3-golang/internal/model"
|
||||
"chatapp3-golang/internal/service/rechargeagency"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type rechargeAgencyRouteAuth struct{}
|
||||
|
||||
func (rechargeAgencyRouteAuth) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||
return integration.UserCredential{
|
||||
UserID: integration.Int64Value(7),
|
||||
SysOrigin: "LIKEI",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rechargeAgencyRouteAuth) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||
return integration.ConsoleAccount{}, nil
|
||||
}
|
||||
|
||||
type rechargeAgencyRouteJava struct {
|
||||
authorization string
|
||||
sysOrigin string
|
||||
}
|
||||
|
||||
func (f *rechargeAgencyRouteJava) PageFreightSellers(_ context.Context, authorization string, sysOrigin string, cursor int, limit int) (integration.FreightSellerPage, error) {
|
||||
f.authorization = authorization
|
||||
f.sysOrigin = sysOrigin
|
||||
return integration.FreightSellerPage{
|
||||
Total: 1,
|
||||
Current: cursor,
|
||||
Size: limit,
|
||||
Records: []integration.FreightSellerRecord{
|
||||
{
|
||||
ID: integration.Int64Value(1),
|
||||
SysOrigin: sysOrigin,
|
||||
UserID: integration.Int64Value(99),
|
||||
Balance: integration.DecimalString("10"),
|
||||
UserBaseInfo: integration.FreightSellerProfile{
|
||||
Account: "u99",
|
||||
UserNickname: "Nina",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestRechargeAgencySellersRoute(t *testing.T) {
|
||||
java := &rechargeAgencyRouteJava{}
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.RechargeAgencySellerInfo{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.RechargeAgencySellerInfo{
|
||||
SysOrigin: "LIKEI",
|
||||
UserID: 99,
|
||||
Whatsapp: "+8613800138000",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed whatsapp: %v", err)
|
||||
}
|
||||
|
||||
service := rechargeagency.NewService(config.Config{}, db, java)
|
||||
engine := NewRouter(config.Config{}, nil, rechargeAgencyRouteAuth{}, Services{
|
||||
RechargeAgency: service,
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/app/h5/recharge-agency-list/sellers?cursor=2&limit=1", nil)
|
||||
req.Header.Set("Authorization", "Bearer token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if java.authorization != "Bearer token" || java.sysOrigin != "LIKEI" {
|
||||
t.Fatalf("gateway authorization/sysOrigin = %q/%q", java.authorization, java.sysOrigin)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Body struct {
|
||||
Total int64 `json:"total"`
|
||||
Records []struct {
|
||||
UserID string `json:"userId"`
|
||||
UserNickname string `json:"userNickname"`
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
} `json:"records"`
|
||||
} `json:"body"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload.Body.Total != 1 || len(payload.Body.Records) != 1 {
|
||||
t.Fatalf("payload body mismatch: %+v", payload.Body)
|
||||
}
|
||||
if payload.Body.Records[0].UserID != "99" || payload.Body.Records[0].UserNickname != "Nina" {
|
||||
t.Fatalf("record mismatch: %+v", payload.Body.Records[0])
|
||||
}
|
||||
if payload.Body.Records[0].Whatsapp != "+8613800138000" {
|
||||
t.Fatalf("whatsapp mismatch: %+v", payload.Body.Records[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRechargeAgencyProfileRoutes(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.RechargeAgencySellerInfo{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
service := rechargeagency.NewService(config.Config{}, db, nil)
|
||||
engine := NewRouter(config.Config{}, nil, rechargeAgencyRouteAuth{}, Services{
|
||||
RechargeAgency: service,
|
||||
})
|
||||
|
||||
postReq := httptest.NewRequest(http.MethodPost, "/app/h5/recharge-agency/profile/whatsapp", bytes.NewBufferString(`{"whatsapp":"+8613800138000"}`))
|
||||
postReq.Header.Set("Authorization", "Bearer token")
|
||||
postReq.Header.Set("Content-Type", "application/json")
|
||||
postRec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(postRec, postReq)
|
||||
if postRec.Code != http.StatusOK {
|
||||
t.Fatalf("post status = %d body = %s", postRec.Code, postRec.Body.String())
|
||||
}
|
||||
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/app/h5/recharge-agency/profile", nil)
|
||||
getReq.Header.Set("Authorization", "Bearer token")
|
||||
getRec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(getRec, getReq)
|
||||
if getRec.Code != http.StatusOK {
|
||||
t.Fatalf("get status = %d body = %s", getRec.Code, getRec.Body.String())
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Body struct {
|
||||
UserID string `json:"userId"`
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
} `json:"body"`
|
||||
}
|
||||
if err := json.Unmarshal(getRec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode get response: %v", err)
|
||||
}
|
||||
if payload.Body.UserID != "7" || payload.Body.Whatsapp != "+8613800138000" {
|
||||
t.Fatalf("profile body mismatch: %+v", payload.Body)
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/rechargeagency"
|
||||
"chatapp3-golang/internal/service/rechargereward"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||
@ -32,6 +33,7 @@ type Services struct {
|
||||
GameProviders *gameprovider.Registry
|
||||
LuckyGift *luckygift.LuckyGiftService
|
||||
HostCenter *hostcenter.Service
|
||||
RechargeAgency *rechargeagency.Service
|
||||
RechargeReward *rechargereward.Service
|
||||
RegisterReward *registerreward.RegisterRewardService
|
||||
RoomTurnoverReward *roomturnoverreward.Service
|
||||
@ -52,6 +54,7 @@ func NewRouter(
|
||||
|
||||
registerHealthRoutes(engine, cfg, repository)
|
||||
registerInviteRoutes(engine, javaClient, services.Invite)
|
||||
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
|
||||
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
|
||||
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
||||
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
||||
|
||||
118
internal/service/rechargeagency/list.go
Normal file
118
internal/service/rechargeagency/list.go
Normal file
@ -0,0 +1,118 @@
|
||||
package rechargeagency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"chatapp3-golang/internal/integration"
|
||||
"chatapp3-golang/internal/model"
|
||||
)
|
||||
|
||||
// PageSellers 返回当前用户可见的 H5 币商名单。
|
||||
func (s *Service) PageSellers(ctx context.Context, user AuthUser, cursor int, limit int) (*SellerListResponse, error) {
|
||||
if s.java == nil {
|
||||
return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable")
|
||||
}
|
||||
if strings.TrimSpace(user.Authorization) == "" {
|
||||
return nil, NewAppError(http.StatusUnauthorized, "missing_authorization", "authorization is required")
|
||||
}
|
||||
|
||||
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
||||
if sysOrigin == "" {
|
||||
return nil, NewAppError(http.StatusUnauthorized, "invalid_sys_origin", "authenticated sysOrigin is required")
|
||||
}
|
||||
cursor, limit = normalizePage(cursor, limit)
|
||||
|
||||
page, err := s.java.PageFreightSellers(ctx, user.Authorization, sysOrigin, cursor, limit)
|
||||
if err != nil {
|
||||
return nil, NewAppError(http.StatusBadGateway, "freight_seller_list_failed", err.Error())
|
||||
}
|
||||
|
||||
records := make([]SellerView, 0, len(page.Records))
|
||||
for _, record := range page.Records {
|
||||
records = append(records, buildSellerView(record))
|
||||
}
|
||||
if s.db != nil {
|
||||
whatsapps, err := s.loadSellerWhatsapps(ctx, sysOrigin, collectSellerUserIDs(records))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range records {
|
||||
if whatsapp := strings.TrimSpace(whatsapps[records[i].UserID]); whatsapp != "" {
|
||||
records[i].Whatsapp = whatsapp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &SellerListResponse{
|
||||
SysOrigin: sysOrigin,
|
||||
Cursor: cursor,
|
||||
Limit: limit,
|
||||
Total: page.Total,
|
||||
Records: records,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildSellerView(record integration.FreightSellerRecord) SellerView {
|
||||
profile := record.UserBaseInfo
|
||||
return SellerView{
|
||||
ID: int64(record.ID),
|
||||
SysOrigin: strings.TrimSpace(record.SysOrigin),
|
||||
UserID: int64(record.UserID),
|
||||
Account: firstNonBlank(profile.Account, formatInt64(int64(record.UserID))),
|
||||
UserAvatar: strings.TrimSpace(profile.UserAvatar),
|
||||
UserNickname: firstNonBlank(profile.UserNickname, profile.Account, formatInt64(int64(record.UserID))),
|
||||
CountryCode: strings.TrimSpace(profile.CountryCode),
|
||||
CountryName: strings.TrimSpace(profile.CountryName),
|
||||
RegionCode: strings.TrimSpace(profile.RegionCode),
|
||||
Balance: normalizeDecimalString(string(record.Balance)),
|
||||
ContactInfo: strings.TrimSpace(record.ContactInfo),
|
||||
SupportedCountries: strings.TrimSpace(record.SupportedCountries),
|
||||
NationalFlag: normalizeFlags(record.NationalFlag),
|
||||
OwnNationalFlag: strings.TrimSpace(record.OwnNationalFlag),
|
||||
BecomeDays: record.BecomeDays,
|
||||
TransactionCount: record.TransactionCount,
|
||||
IsFollow: record.IsFollow,
|
||||
SuperDealer: record.SuperDealer,
|
||||
}
|
||||
}
|
||||
|
||||
func collectSellerUserIDs(records []SellerView) []int64 {
|
||||
seen := make(map[int64]struct{}, len(records))
|
||||
userIDs := make([]int64, 0, len(records))
|
||||
for _, record := range records {
|
||||
if record.UserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[record.UserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[record.UserID] = struct{}{}
|
||||
userIDs = append(userIDs, record.UserID)
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func (s *Service) loadSellerWhatsapps(ctx context.Context, sysOrigin string, userIDs []int64) (map[int64]string, error) {
|
||||
whatsapps := make(map[int64]string, len(userIDs))
|
||||
if len(userIDs) == 0 {
|
||||
return whatsapps, nil
|
||||
}
|
||||
|
||||
var rows []model.RechargeAgencySellerInfo
|
||||
err := s.db.WithContext(ctx).
|
||||
Select("user_id", "whatsapp").
|
||||
Where("sys_origin = ? AND user_id IN ?", sysOrigin, userIDs).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, NewAppError(http.StatusInternalServerError, "seller_whatsapp_query_failed", err.Error())
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if whatsapp := strings.TrimSpace(row.Whatsapp); whatsapp != "" {
|
||||
whatsapps[row.UserID] = whatsapp
|
||||
}
|
||||
}
|
||||
return whatsapps, nil
|
||||
}
|
||||
119
internal/service/rechargeagency/list_test.go
Normal file
119
internal/service/rechargeagency/list_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package rechargeagency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/integration"
|
||||
"chatapp3-golang/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type fakeJavaGateway struct {
|
||||
gotAuthorization string
|
||||
gotSysOrigin string
|
||||
gotCursor int
|
||||
gotLimit int
|
||||
page integration.FreightSellerPage
|
||||
}
|
||||
|
||||
func (f *fakeJavaGateway) PageFreightSellers(_ context.Context, authorization string, sysOrigin string, cursor int, limit int) (integration.FreightSellerPage, error) {
|
||||
f.gotAuthorization = authorization
|
||||
f.gotSysOrigin = sysOrigin
|
||||
f.gotCursor = cursor
|
||||
f.gotLimit = limit
|
||||
return f.page, nil
|
||||
}
|
||||
|
||||
func TestPageSellersUsesAuthenticatedUserAndMapsRecords(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.RechargeAgencySellerInfo{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.RechargeAgencySellerInfo{
|
||||
SysOrigin: "LIKEI",
|
||||
UserID: 99,
|
||||
Whatsapp: "+8613800138000",
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed whatsapp: %v", err)
|
||||
}
|
||||
|
||||
java := &fakeJavaGateway{
|
||||
page: integration.FreightSellerPage{
|
||||
Total: 1,
|
||||
Current: 1,
|
||||
Size: 20,
|
||||
Records: []integration.FreightSellerRecord{
|
||||
{
|
||||
ID: integration.Int64Value(11),
|
||||
SysOrigin: "LIKEI",
|
||||
UserID: integration.Int64Value(99),
|
||||
Balance: integration.DecimalString("123.45"),
|
||||
SuperDealer: true,
|
||||
NationalFlag: []string{"", "https://flag.example/ae.png"},
|
||||
OwnNationalFlag: "https://flag.example/own.png",
|
||||
BecomeDays: 7,
|
||||
TransactionCount: 12,
|
||||
UserBaseInfo: integration.FreightSellerProfile{
|
||||
Account: "u99",
|
||||
UserNickname: "Nina",
|
||||
UserAvatar: "https://avatar.example/u99.png",
|
||||
CountryCode: "AE",
|
||||
CountryName: "United Arab Emirates",
|
||||
RegionCode: "MENA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
service := NewService(config.Config{}, db, java)
|
||||
|
||||
resp, err := service.PageSellers(context.Background(), AuthUser{
|
||||
SysOrigin: "likei",
|
||||
Authorization: "Bearer token",
|
||||
}, 0, 200)
|
||||
if err != nil {
|
||||
t.Fatalf("PageSellers() error = %v", err)
|
||||
}
|
||||
|
||||
if java.gotAuthorization != "Bearer token" || java.gotSysOrigin != "LIKEI" {
|
||||
t.Fatalf("gateway auth/sysOrigin = %q/%q", java.gotAuthorization, java.gotSysOrigin)
|
||||
}
|
||||
if java.gotCursor != 1 || java.gotLimit != 100 {
|
||||
t.Fatalf("gateway cursor/limit = %d/%d", java.gotCursor, java.gotLimit)
|
||||
}
|
||||
if resp.Total != 1 || len(resp.Records) != 1 {
|
||||
t.Fatalf("response total/records = %d/%d", resp.Total, len(resp.Records))
|
||||
}
|
||||
record := resp.Records[0]
|
||||
if record.ID != 11 || record.UserID != 99 || record.Account != "u99" || record.UserNickname != "Nina" {
|
||||
t.Fatalf("mapped record identity mismatch: %+v", record)
|
||||
}
|
||||
if record.Balance != "123.45" || !record.SuperDealer || record.TransactionCount != 12 {
|
||||
t.Fatalf("mapped record stats mismatch: %+v", record)
|
||||
}
|
||||
if record.Whatsapp != "+8613800138000" {
|
||||
t.Fatalf("mapped whatsapp = %q", record.Whatsapp)
|
||||
}
|
||||
if len(record.NationalFlag) != 1 || record.NationalFlag[0] != "https://flag.example/ae.png" {
|
||||
t.Fatalf("mapped flags mismatch: %+v", record.NationalFlag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageSellersRequiresAuthorization(t *testing.T) {
|
||||
service := NewService(config.Config{}, nil, &fakeJavaGateway{})
|
||||
|
||||
_, err := service.PageSellers(context.Background(), AuthUser{SysOrigin: "LIKEI"}, 1, 20)
|
||||
if err == nil {
|
||||
t.Fatal("PageSellers() error is nil")
|
||||
}
|
||||
}
|
||||
60
internal/service/rechargeagency/normalize.go
Normal file
60
internal/service/rechargeagency/normalize.go
Normal file
@ -0,0 +1,60 @@
|
||||
package rechargeagency
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func normalizeSysOrigin(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func normalizePage(cursor int, limit int) (int, int) {
|
||||
if cursor <= 0 {
|
||||
cursor = 1
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
return cursor, limit
|
||||
}
|
||||
|
||||
func normalizeDecimalString(value string) string {
|
||||
normalized := strings.TrimSpace(value)
|
||||
if normalized == "" {
|
||||
return "0"
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeFlags(flags []string) []string {
|
||||
if len(flags) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
result := make([]string, 0, len(flags))
|
||||
for _, flag := range flags {
|
||||
if normalized := strings.TrimSpace(flag); normalized != "" {
|
||||
result = append(result, normalized)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func firstNonBlank(values ...string) string {
|
||||
for _, value := range values {
|
||||
if normalized := strings.TrimSpace(value); normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatInt64(value int64) string {
|
||||
if value <= 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(value, 10)
|
||||
}
|
||||
101
internal/service/rechargeagency/profile.go
Normal file
101
internal/service/rechargeagency/profile.go
Normal file
@ -0,0 +1,101 @@
|
||||
package rechargeagency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// GetProfile returns the current recharge agency seller extension profile.
|
||||
func (s *Service) GetProfile(ctx context.Context, user AuthUser) (*SellerProfileResponse, error) {
|
||||
sysOrigin, userID, err := normalizeSellerProfileUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.db == nil {
|
||||
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
||||
}
|
||||
|
||||
var row model.RechargeAgencySellerInfo
|
||||
err = s.db.WithContext(ctx).
|
||||
Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).
|
||||
First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &SellerProfileResponse{
|
||||
SysOrigin: sysOrigin,
|
||||
UserID: userID,
|
||||
Whatsapp: "",
|
||||
}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, NewAppError(http.StatusInternalServerError, "seller_profile_query_failed", err.Error())
|
||||
}
|
||||
|
||||
return &SellerProfileResponse{
|
||||
SysOrigin: sysOrigin,
|
||||
UserID: userID,
|
||||
Whatsapp: strings.TrimSpace(row.Whatsapp),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateWhatsapp stores the current recharge agency seller WhatsApp contact.
|
||||
func (s *Service) UpdateWhatsapp(ctx context.Context, user AuthUser, req UpdateWhatsappRequest) (*SellerProfileResponse, error) {
|
||||
sysOrigin, userID, err := normalizeSellerProfileUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.db == nil {
|
||||
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
||||
}
|
||||
|
||||
whatsapp := strings.TrimSpace(req.Whatsapp)
|
||||
if whatsapp == "" {
|
||||
return nil, NewAppError(http.StatusBadRequest, "whatsapp_required", "whatsapp is required")
|
||||
}
|
||||
if len([]rune(whatsapp)) > 64 {
|
||||
return nil, NewAppError(http.StatusBadRequest, "whatsapp_too_long", "whatsapp cannot exceed 64 characters")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := model.RechargeAgencySellerInfo{
|
||||
SysOrigin: sysOrigin,
|
||||
UserID: userID,
|
||||
Whatsapp: whatsapp,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
err = s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "user_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"whatsapp": whatsapp,
|
||||
"update_time": now,
|
||||
}),
|
||||
}).Create(&row).Error
|
||||
if err != nil {
|
||||
return nil, NewAppError(http.StatusInternalServerError, "seller_profile_save_failed", err.Error())
|
||||
}
|
||||
|
||||
return &SellerProfileResponse{
|
||||
SysOrigin: sysOrigin,
|
||||
UserID: userID,
|
||||
Whatsapp: whatsapp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeSellerProfileUser(user AuthUser) (string, int64, error) {
|
||||
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
||||
if sysOrigin == "" {
|
||||
return "", 0, NewAppError(http.StatusUnauthorized, "invalid_sys_origin", "authenticated sysOrigin is required")
|
||||
}
|
||||
if user.UserID <= 0 {
|
||||
return "", 0, NewAppError(http.StatusUnauthorized, "invalid_user", "authenticated user is required")
|
||||
}
|
||||
return sysOrigin, user.UserID, nil
|
||||
}
|
||||
80
internal/service/rechargeagency/profile_test.go
Normal file
80
internal/service/rechargeagency/profile_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package rechargeagency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newRechargeAgencyProfileTestService(t *testing.T) *Service {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.RechargeAgencySellerInfo{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
return NewService(config.Config{}, db, nil)
|
||||
}
|
||||
|
||||
func TestSellerProfileWhatsappUpsert(t *testing.T) {
|
||||
service := newRechargeAgencyProfileTestService(t)
|
||||
user := AuthUser{
|
||||
UserID: 2042274349343506434,
|
||||
SysOrigin: "likei",
|
||||
}
|
||||
|
||||
empty, err := service.GetProfile(context.Background(), user)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProfile() error = %v", err)
|
||||
}
|
||||
if empty.SysOrigin != "LIKEI" || empty.UserID != user.UserID || empty.Whatsapp != "" {
|
||||
t.Fatalf("empty profile = %+v", empty)
|
||||
}
|
||||
|
||||
saved, err := service.UpdateWhatsapp(context.Background(), user, UpdateWhatsappRequest{Whatsapp: " +8613800138000 "})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWhatsapp() error = %v", err)
|
||||
}
|
||||
if saved.Whatsapp != "+8613800138000" {
|
||||
t.Fatalf("saved whatsapp = %q", saved.Whatsapp)
|
||||
}
|
||||
|
||||
updated, err := service.UpdateWhatsapp(context.Background(), user, UpdateWhatsappRequest{Whatsapp: "+8613900139000"})
|
||||
if err != nil {
|
||||
t.Fatalf("second UpdateWhatsapp() error = %v", err)
|
||||
}
|
||||
if updated.Whatsapp != "+8613900139000" {
|
||||
t.Fatalf("updated whatsapp = %q", updated.Whatsapp)
|
||||
}
|
||||
|
||||
got, err := service.GetProfile(context.Background(), user)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProfile() after save error = %v", err)
|
||||
}
|
||||
if got.Whatsapp != "+8613900139000" {
|
||||
t.Fatalf("got whatsapp = %q", got.Whatsapp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWhatsappValidatesInput(t *testing.T) {
|
||||
service := newRechargeAgencyProfileTestService(t)
|
||||
user := AuthUser{
|
||||
UserID: 7,
|
||||
SysOrigin: "LIKEI",
|
||||
}
|
||||
|
||||
if _, err := service.UpdateWhatsapp(context.Background(), user, UpdateWhatsappRequest{}); err == nil {
|
||||
t.Fatal("UpdateWhatsapp() empty error is nil")
|
||||
}
|
||||
if _, err := service.UpdateWhatsapp(context.Background(), user, UpdateWhatsappRequest{Whatsapp: strings.Repeat("1", 65)}); err == nil {
|
||||
t.Fatal("UpdateWhatsapp() long error is nil")
|
||||
}
|
||||
}
|
||||
75
internal/service/rechargeagency/types.go
Normal file
75
internal/service/rechargeagency/types.go
Normal file
@ -0,0 +1,75 @@
|
||||
package rechargeagency
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"chatapp3-golang/internal/common"
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/integration"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AppError = common.AppError
|
||||
type AuthUser = common.AuthUser
|
||||
|
||||
var NewAppError = common.NewAppError
|
||||
|
||||
type javaGateway interface {
|
||||
PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (integration.FreightSellerPage, error)
|
||||
}
|
||||
|
||||
type rechargeAgencyDB interface {
|
||||
WithContext(ctx context.Context) *gorm.DB
|
||||
}
|
||||
|
||||
// Service 负责充值代理 H5 币商名单数据。
|
||||
type Service struct {
|
||||
cfg config.Config
|
||||
db rechargeAgencyDB
|
||||
java javaGateway
|
||||
}
|
||||
|
||||
func NewService(cfg config.Config, db rechargeAgencyDB, java javaGateway) *Service {
|
||||
return &Service{cfg: cfg, db: db, java: java}
|
||||
}
|
||||
|
||||
type SellerListResponse struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Cursor int `json:"cursor"`
|
||||
Limit int `json:"limit"`
|
||||
Total int64 `json:"total"`
|
||||
Records []SellerView `json:"records"`
|
||||
}
|
||||
|
||||
type SellerView struct {
|
||||
ID int64 `json:"id,string"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
UserID int64 `json:"userId,string"`
|
||||
Account string `json:"account"`
|
||||
UserAvatar string `json:"userAvatar"`
|
||||
UserNickname string `json:"userNickname"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
CountryName string `json:"countryName"`
|
||||
RegionCode string `json:"regionCode"`
|
||||
Balance string `json:"balance"`
|
||||
ContactInfo string `json:"contactInfo,omitempty"`
|
||||
Whatsapp string `json:"whatsapp,omitempty"`
|
||||
SupportedCountries string `json:"supportedCountries,omitempty"`
|
||||
NationalFlag []string `json:"nationalFlag"`
|
||||
OwnNationalFlag string `json:"ownNationalFlag,omitempty"`
|
||||
BecomeDays int `json:"becomeDays"`
|
||||
TransactionCount int64 `json:"transactionCount"`
|
||||
IsFollow bool `json:"isFollow"`
|
||||
SuperDealer bool `json:"superDealer"`
|
||||
}
|
||||
|
||||
type SellerProfileResponse struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
UserID int64 `json:"userId,string"`
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
}
|
||||
|
||||
type UpdateWhatsappRequest struct {
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
}
|
||||
10
migrations/021_recharge_agency_seller_info.sql
Normal file
10
migrations/021_recharge_agency_seller_info.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS `recharge_agency_seller_info` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
|
||||
`user_id` bigint NOT NULL COMMENT '币商用户ID',
|
||||
`whatsapp` varchar(64) NOT NULL DEFAULT '' COMMENT 'WhatsApp',
|
||||
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_recharge_agency_seller_info_user` (`sys_origin`, `user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='充值代理币商扩展资料';
|
||||
Loading…
x
Reference in New Issue
Block a user