对接
This commit is contained in:
@@ -55,24 +55,27 @@ type LiveRoomRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
type LiveRoomResponse struct {
|
||||
ID uint `json:"id"`
|
||||
StreamID string `json:"streamId"`
|
||||
Name string `json:"name"`
|
||||
CategoryID uint `json:"categoryId"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
Intro string `json:"intro"`
|
||||
Sort int `json:"sort"`
|
||||
AnchorUserID uint `json:"anchorUserId"`
|
||||
AnchorName string `json:"anchorName"`
|
||||
AnchorAvatar string `json:"anchorAvatar"`
|
||||
StreamURL string `json:"streamUrl"`
|
||||
ReplayURL string `json:"replayUrl"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Status string `json:"status"`
|
||||
ViewerCount int `json:"viewerCount"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID uint `json:"id"`
|
||||
StreamID string `json:"streamId"`
|
||||
Name string `json:"name"`
|
||||
CategoryID uint `json:"categoryId"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
Intro string `json:"intro"`
|
||||
Sort int `json:"sort"`
|
||||
AnchorUserID uint `json:"anchorUserId"`
|
||||
AnchorName string `json:"anchorName"`
|
||||
AnchorAvatar string `json:"anchorAvatar"`
|
||||
StreamURL string `json:"streamUrl"`
|
||||
ReplayURL string `json:"replayUrl"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Status string `json:"status"`
|
||||
ViewerCount int `json:"viewerCount"`
|
||||
ViewerCountText string `json:"viewerCountText"`
|
||||
LikeCount int `json:"likeCount"`
|
||||
StartedAt *time.Time `json:"startedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
type LiveStatusRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@@ -9,6 +9,7 @@ type LiveClientLoginRequest struct {
|
||||
|
||||
type LiveClientRegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
ConfirmPassword string `json:"confirmPassword" binding:"required"`
|
||||
}
|
||||
@@ -34,6 +35,10 @@ type LiveClientAccountResponse struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type LiveClientBalanceResponse struct {
|
||||
DiamondBalance int64 `json:"diamondBalance"`
|
||||
}
|
||||
|
||||
type LiveClientAuthResponse struct {
|
||||
User *LiveClientAccountResponse `json:"user"`
|
||||
Token string `json:"token"`
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLiveClientRegisterRequestDecodesNickname(t *testing.T) {
|
||||
var request LiveClientRegisterRequest
|
||||
if err := json.Unmarshal([]byte(`{"username":"viewer","nickname":"直播用户","password":"password","confirmPassword":"password"}`), &request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if request.Nickname != "直播用户" {
|
||||
t.Fatalf("nickname = %q", request.Nickname)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@ type RechargePlan struct {
|
||||
AmountCents int64 `json:"amountCents"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
type RechargeProvider struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type RechargePlansRequest struct {
|
||||
Plans []*RechargePlan `json:"plans" binding:"required"`
|
||||
}
|
||||
|
||||
@@ -76,6 +76,18 @@ func (s *CatalogService) ListRooms(ctx context.Context, input *dto.LiveRoomListR
|
||||
}
|
||||
return result, total, nil
|
||||
}
|
||||
func (s *CatalogService) ListPublicRooms(ctx context.Context, input *dto.LiveRoomListRequest) ([]*dto.LiveRoomResponse, int64, error) {
|
||||
enabled := true
|
||||
items, total, err := s.rooms.List(ctx, bizlive.RoomFilter{Name: input.Name, CategoryID: input.CategoryID, Status: bizlive.RoomStatusLive, Enabled: &enabled, CategoryEnabled: &enabled}, liveListOptions(input.LiveListRequest))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
result := make([]*dto.LiveRoomResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, roomResponse(item))
|
||||
}
|
||||
return result, total, nil
|
||||
}
|
||||
func (s *CatalogService) GetRoom(ctx context.Context, id uint) (*dto.LiveRoomResponse, error) {
|
||||
item, err := s.rooms.Get(ctx, id)
|
||||
if err != nil {
|
||||
@@ -83,6 +95,13 @@ func (s *CatalogService) GetRoom(ctx context.Context, id uint) (*dto.LiveRoomRes
|
||||
}
|
||||
return roomResponse(item), nil
|
||||
}
|
||||
func (s *CatalogService) GetPublicRoom(ctx context.Context, id uint) (*dto.LiveRoomResponse, error) {
|
||||
item, err := s.rooms.GetPublic(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return roomResponse(item), nil
|
||||
}
|
||||
func liveRoomDO(id uint, input *dto.LiveRoomRequest) *bizlive.Room {
|
||||
return &bizlive.Room{ID: id, Name: input.Name, CategoryID: input.CategoryID, CoverURL: input.CoverURL, Intro: input.Intro, Sort: input.Sort, AnchorUserID: input.AnchorUserID, StreamURL: input.StreamURL, ReplayURL: input.ReplayURL, Enabled: input.Enabled}
|
||||
}
|
||||
@@ -113,5 +132,5 @@ func categoryResponse(item *bizlive.Category) *dto.LiveCategoryResponse {
|
||||
return &dto.LiveCategoryResponse{ID: item.ID, Name: item.Name, Icon: item.Icon, Sort: item.Sort, Enabled: item.Enabled, RoomCount: item.RoomCount, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt}
|
||||
}
|
||||
func roomResponse(item *bizlive.Room) *dto.LiveRoomResponse {
|
||||
return &dto.LiveRoomResponse{ID: item.ID, StreamID: fmt.Sprintf("room-%d", item.ID), Name: item.Name, CategoryID: item.CategoryID, CategoryName: item.CategoryName, CoverURL: item.CoverURL, Intro: item.Intro, Sort: item.Sort, AnchorUserID: item.AnchorUserID, AnchorName: item.AnchorName, AnchorAvatar: item.AnchorAvatar, StreamURL: item.StreamURL, ReplayURL: item.ReplayURL, Enabled: item.Enabled, Status: item.Status, ViewerCount: item.ViewerCount, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt}
|
||||
return &dto.LiveRoomResponse{ID: item.ID, StreamID: fmt.Sprintf("room-%d", item.ID), Name: item.Name, CategoryID: item.CategoryID, CategoryName: item.CategoryName, CoverURL: item.CoverURL, Intro: item.Intro, Sort: item.Sort, AnchorUserID: item.AnchorUserID, AnchorName: item.AnchorName, AnchorAvatar: item.AnchorAvatar, StreamURL: item.StreamURL, ReplayURL: item.ReplayURL, Enabled: item.Enabled, Status: item.Status, ViewerCount: item.ViewerCount, ViewerCountText: item.ViewerCountText, LikeCount: item.LikeCount, StartedAt: item.StartedAt, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
bizlive "kra/internal/biz/live"
|
||||
)
|
||||
|
||||
type FollowService struct{ uc *bizlive.FollowUsecase }
|
||||
|
||||
func NewFollowService(uc *bizlive.FollowUsecase) *FollowService { return &FollowService{uc: uc} }
|
||||
|
||||
func (s *FollowService) Status(ctx context.Context, userID, roomID uint) (bool, error) {
|
||||
_, err := s.uc.Get(ctx, userID, roomID)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if errors.Is(err, bizlive.ErrFollowNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (s *FollowService) Follow(ctx context.Context, userID, roomID uint) (bool, error) {
|
||||
_, err := s.uc.Follow(ctx, userID, roomID)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (s *FollowService) Unfollow(ctx context.Context, userID, roomID uint) error {
|
||||
return s.uc.Unfollow(ctx, userID, roomID)
|
||||
}
|
||||
@@ -2,4 +2,4 @@ package live
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewStreamService, NewCatalogService)
|
||||
var ProviderSet = wire.NewSet(NewStreamService, NewCatalogService, NewFollowService)
|
||||
|
||||
@@ -30,3 +30,7 @@ func (s *StreamService) Get(ctx context.Context, streamID string) (*dto.LiveStre
|
||||
}
|
||||
return &dto.LiveStreamResponse{StreamID: stream.StreamID, PublisherOnline: stream.PublisherOnline, SubscriberCount: stream.SubscriberCount}, nil
|
||||
}
|
||||
|
||||
func (s *StreamService) Like(ctx context.Context, streamID string) (int, error) {
|
||||
return s.uc.Like(ctx, streamID)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Service struct{ uc *biz.Usecase }
|
||||
func NewService(uc *biz.Usecase) *Service { return &Service{uc: uc} }
|
||||
|
||||
func (s *Service) Register(ctx context.Context, in *dto.LiveClientRegisterRequest, ip, agent string) (*dto.LiveClientAuthResponse, error) {
|
||||
result, err := s.uc.Register(ctx, &biz.RegisterInput{Username: in.Username, Password: in.Password, ConfirmPassword: in.ConfirmPassword}, ip, agent)
|
||||
result, err := s.uc.Register(ctx, &biz.RegisterInput{Username: in.Username, Nickname: in.Nickname, Password: in.Password, ConfirmPassword: in.ConfirmPassword}, ip, agent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -35,6 +35,14 @@ func (s *Service) Me(ctx context.Context, userID uint) (*dto.LiveClientAccountRe
|
||||
return accountResponse(account), nil
|
||||
}
|
||||
|
||||
func (s *Service) Balance(ctx context.Context, userID uint) (*dto.LiveClientBalanceResponse, error) {
|
||||
account, err := s.uc.Get(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.LiveClientBalanceResponse{DiamondBalance: account.DiamondBalance}, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateProfile(ctx context.Context, userID uint, in *dto.LiveClientProfileRequest) (*dto.LiveClientAccountResponse, error) {
|
||||
account, err := s.uc.UpdateProfile(ctx, &biz.ProfileInput{UserID: userID, Nickname: in.Nickname, Avatar: in.Avatar})
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package recharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
integrationbiz "kra/internal/biz/integration"
|
||||
)
|
||||
|
||||
type providerConfigRepo struct {
|
||||
configs []*integrationbiz.IntegrationConfig
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *providerConfigRepo) ListIntegrationConfigs(context.Context, string) ([]*integrationbiz.IntegrationConfig, error) {
|
||||
return r.configs, r.err
|
||||
}
|
||||
|
||||
func (*providerConfigRepo) FindIntegrationConfig(context.Context, string, string) (*integrationbiz.IntegrationConfig, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (*providerConfigRepo) SaveIntegrationConfig(context.Context, *integrationbiz.IntegrationConfig) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (*providerConfigRepo) DeleteIntegrationConfig(context.Context, string, string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func TestProvidersOnlyReturnsEnabledSupportedPaymentConfigs(t *testing.T) {
|
||||
repo := &providerConfigRepo{configs: []*integrationbiz.IntegrationConfig{
|
||||
{Kind: integrationbiz.IntegrationKindPayment, Provider: "wechat-v3", Enabled: true, Values: []byte(`{"api_key":"must-not-leak"}`)},
|
||||
{Kind: integrationbiz.IntegrationKindPayment, Provider: "alipay", Enabled: false},
|
||||
{Kind: integrationbiz.IntegrationKindPayment, Provider: "unknown-provider", Enabled: true},
|
||||
{Kind: integrationbiz.IntegrationKindPayment, Provider: "wechat-v3", Enabled: true},
|
||||
nil,
|
||||
}}
|
||||
integrations := integrationbiz.NewIntegrationConfigUsecase(repo, nil)
|
||||
service := NewService(nil, integrations)
|
||||
|
||||
providers, err := service.Providers(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(providers) != 1 || providers[0].ID != "wechat-v3" || providers[0].Name == "" {
|
||||
t.Fatalf("providers = %+v", providers)
|
||||
}
|
||||
encoded, err := json.Marshal(providers)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encoded), "must-not-leak") || strings.Contains(string(encoded), "api_key") {
|
||||
t.Fatalf("provider response leaked integration config: %s", encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersPropagatesConfigReadFailure(t *testing.T) {
|
||||
want := errors.New("load failed")
|
||||
integrations := integrationbiz.NewIntegrationConfigUsecase(&providerConfigRepo{err: want}, nil)
|
||||
service := NewService(nil, integrations)
|
||||
|
||||
if _, err := service.Providers(context.Background()); !errors.Is(err, want) {
|
||||
t.Fatalf("Providers() error = %v, want %v", err, want)
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,48 @@ package recharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
integrationbiz "kra/internal/biz/integration"
|
||||
bizpayment "kra/internal/biz/payment"
|
||||
biz "kra/internal/biz/recharge"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type Service struct{ uc *biz.Usecase }
|
||||
type Service struct {
|
||||
uc *biz.Usecase
|
||||
integrations *integrationbiz.IntegrationConfigUsecase
|
||||
}
|
||||
|
||||
func NewService(uc *biz.Usecase) *Service { return &Service{uc: uc} }
|
||||
func NewService(uc *biz.Usecase, integrations *integrationbiz.IntegrationConfigUsecase) *Service {
|
||||
return &Service{uc: uc, integrations: integrations}
|
||||
}
|
||||
|
||||
func (s *Service) Providers(ctx context.Context) ([]*dto.RechargeProvider, error) {
|
||||
configs, err := s.integrations.List(ctx, integrationbiz.IntegrationKindPayment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers := make([]*dto.RechargeProvider, 0, len(configs))
|
||||
seen := make(map[string]struct{}, len(configs))
|
||||
for _, config := range configs {
|
||||
if config == nil || !config.Enabled || !bizpayment.IsSupportedPaymentProvider(config.Provider) {
|
||||
continue
|
||||
}
|
||||
definition, ok := integrationbiz.IntegrationDefinition(integrationbiz.IntegrationKindPayment, config.Provider)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[definition.Provider]; exists {
|
||||
continue
|
||||
}
|
||||
seen[definition.Provider] = struct{}{}
|
||||
providers = append(providers, &dto.RechargeProvider{ID: definition.Provider, Name: definition.Name})
|
||||
}
|
||||
sort.Slice(providers, func(i, j int) bool { return providers[i].ID < providers[j].ID })
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func dates(a, b string) []time.Time {
|
||||
if a == "" || b == "" {
|
||||
@@ -76,6 +108,30 @@ func (s *Service) Get(ctx context.Context, no string) (*dto.RechargeOrderRespons
|
||||
return order(v), nil
|
||||
}
|
||||
|
||||
// GetForUser is the client-facing order lookup. It deliberately maps an
|
||||
// order owned by another account to not-found so order numbers cannot be used
|
||||
// to enumerate another user's payment or diamond balance details.
|
||||
func (s *Service) GetForUser(ctx context.Context, userID uint, no string) (*dto.RechargeOrderResponse, error) {
|
||||
if userID == 0 {
|
||||
return nil, biz.ErrNotFound
|
||||
}
|
||||
item, err := s.uc.Get(ctx, no)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.UserID != userID {
|
||||
return nil, biz.ErrNotFound
|
||||
}
|
||||
return order(item), nil
|
||||
}
|
||||
|
||||
// PaymentStatus is an explicit alias for client polling while a provider
|
||||
// payment is pending. The response includes both provider and fulfillment
|
||||
// state through the normal recharge order DTO.
|
||||
func (s *Service) PaymentStatus(ctx context.Context, userID uint, no string) (*dto.RechargeOrderResponse, error) {
|
||||
return s.GetForUser(ctx, userID, no)
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, userID uint, in *dto.RechargeOrderRequest) (*dto.RechargeCheckoutResponse, error) {
|
||||
v, result, err := s.uc.Create(ctx, userID, in.PlanID, biz.Checkout{Provider: in.Provider, NotifyURL: in.NotifyURL, ReturnURL: in.ReturnURL, ClientIP: in.ClientIP, UserAgent: in.UserAgent, DeviceID: in.DeviceID, Extra: in.Extra})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user