币商充值

This commit is contained in:
hy001 2026-04-30 13:29:22 +08:00
parent f8a8d4d65f
commit a2ef52c8d9
14 changed files with 913 additions and 1 deletions

View File

@ -19,6 +19,7 @@ import (
"chatapp3-golang/internal/service/invite" "chatapp3-golang/internal/service/invite"
"chatapp3-golang/internal/service/lingxian" "chatapp3-golang/internal/service/lingxian"
"chatapp3-golang/internal/service/luckygift" "chatapp3-golang/internal/service/luckygift"
"chatapp3-golang/internal/service/rechargeagency"
"chatapp3-golang/internal/service/rechargereward" "chatapp3-golang/internal/service/rechargereward"
"chatapp3-golang/internal/service/registerreward" "chatapp3-golang/internal/service/registerreward"
"chatapp3-golang/internal/service/roomturnoverreward" "chatapp3-golang/internal/service/roomturnoverreward"
@ -41,6 +42,7 @@ func main() {
lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis) lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways) gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet) 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) registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
rechargeRewardService := rechargereward.NewService(app.Config, app.Repository.DB, &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) signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
@ -67,6 +69,7 @@ func main() {
GameProviders: gameProviders, GameProviders: gameProviders,
LuckyGift: luckyGiftService, LuckyGift: luckyGiftService,
HostCenter: hostCenterService, HostCenter: hostCenterService,
RechargeAgency: rechargeAgencyService,
RechargeReward: rechargeRewardService, RechargeReward: rechargeRewardService,
RegisterReward: registerRewardService, RegisterReward: registerRewardService,
RoomTurnoverReward: roomTurnoverRewardService, RoomTurnoverReward: roomTurnoverRewardService,

View File

@ -19,6 +19,7 @@ type Gateways struct {
Room *RoomGateway Room *RoomGateway
Team *TeamGateway Team *TeamGateway
Wallet *WalletGateway Wallet *WalletGateway
Freight *FreightGateway
} }
// NewGateways 基于共享 HTTP 客户端创建所有 Java 网关。 // NewGateways 基于共享 HTTP 客户端创建所有 Java 网关。
@ -36,6 +37,7 @@ func NewGateways(cfg config.Config) Gateways {
Room: &RoomGateway{client: client}, Room: &RoomGateway{client: client},
Team: &TeamGateway{client: client}, Team: &TeamGateway{client: client},
Wallet: &WalletGateway{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) 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 负责认证相关接口。 // AuthGateway 负责认证相关接口。
type AuthGateway struct { type AuthGateway struct {
client *Client 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 { func (g *WalletGateway) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand) error {
return g.client.ChangeGoldBalance(ctx, cmd) 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)
}

View File

@ -2,10 +2,10 @@ package integration
import ( import (
"bytes" "bytes"
"chatapp3-golang/internal/config"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -13,6 +13,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"chatapp3-golang/internal/config"
) )
type Client struct { type Client struct {
@ -213,6 +215,57 @@ type UserTotalRecharge struct {
UpdateTime string `json:"updateTime"` 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 { type resultResponse[T any] struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`
@ -577,6 +630,42 @@ func (c *Client) GetThisMonthTotalPersonalRecharge(ctx context.Context, userID i
return resp.Body, nil 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) { 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)) endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByUserId?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
var resp resultResponse[UserProfile] var resp resultResponse[UserProfile]

View 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"
}

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

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

View File

@ -13,6 +13,7 @@ import (
"chatapp3-golang/internal/service/invite" "chatapp3-golang/internal/service/invite"
"chatapp3-golang/internal/service/lingxian" "chatapp3-golang/internal/service/lingxian"
"chatapp3-golang/internal/service/luckygift" "chatapp3-golang/internal/service/luckygift"
"chatapp3-golang/internal/service/rechargeagency"
"chatapp3-golang/internal/service/rechargereward" "chatapp3-golang/internal/service/rechargereward"
"chatapp3-golang/internal/service/registerreward" "chatapp3-golang/internal/service/registerreward"
"chatapp3-golang/internal/service/roomturnoverreward" "chatapp3-golang/internal/service/roomturnoverreward"
@ -32,6 +33,7 @@ type Services struct {
GameProviders *gameprovider.Registry GameProviders *gameprovider.Registry
LuckyGift *luckygift.LuckyGiftService LuckyGift *luckygift.LuckyGiftService
HostCenter *hostcenter.Service HostCenter *hostcenter.Service
RechargeAgency *rechargeagency.Service
RechargeReward *rechargereward.Service RechargeReward *rechargereward.Service
RegisterReward *registerreward.RegisterRewardService RegisterReward *registerreward.RegisterRewardService
RoomTurnoverReward *roomturnoverreward.Service RoomTurnoverReward *roomturnoverreward.Service
@ -52,6 +54,7 @@ func NewRouter(
registerHealthRoutes(engine, cfg, repository) registerHealthRoutes(engine, cfg, repository)
registerInviteRoutes(engine, javaClient, services.Invite) registerInviteRoutes(engine, javaClient, services.Invite)
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward) registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward) registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
registerSignInRewardRoutes(engine, javaClient, services.SignInReward) registerSignInRewardRoutes(engine, javaClient, services.SignInReward)

View 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
}

View 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")
}
}

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

View 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
}

View 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")
}
}

View 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"`
}

View 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='充值代理币商扩展资料';