币商充值

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/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,

View File

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

View File

@ -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]

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/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)

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