feat: 迁移商务旗下渠道查询和抽屉
This commit is contained in:
+1
-1
@@ -11,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package business
|
||||
|
||||
import "context"
|
||||
|
||||
type BusinessPromote struct {
|
||||
ID uint64
|
||||
Account, SourceDB string
|
||||
BusierID uint64
|
||||
}
|
||||
type BusinessPromoteFilter struct {
|
||||
UserID uint
|
||||
SourceDB string
|
||||
BusierID uint64
|
||||
}
|
||||
type BusinessPromotePage struct {
|
||||
Rows []*BusinessPromote
|
||||
Total int64
|
||||
Available bool
|
||||
}
|
||||
type BusinessPromoteRepo interface {
|
||||
ListDirect(context.Context, BusinessPromoteFilter, ...ListOption) (*BusinessPromotePage, error)
|
||||
}
|
||||
type BusinessPromoteUsecase struct {
|
||||
repo BusinessPromoteRepo
|
||||
accounts *AccountUsecase
|
||||
}
|
||||
|
||||
func NewBusinessPromoteUsecase(repo BusinessPromoteRepo, accounts *AccountUsecase) *BusinessPromoteUsecase {
|
||||
return &BusinessPromoteUsecase{repo: repo, accounts: accounts}
|
||||
}
|
||||
func (uc *BusinessPromoteUsecase) List(ctx context.Context, filter BusinessPromoteFilter, options ...ListOption) (*BusinessPromotePage, error) {
|
||||
if _, err := uc.accounts.Find(ctx, filter.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.ListDirect(ctx, filter, options...)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type directPromoteRepoFake struct{ calls int }
|
||||
|
||||
func (r *directPromoteRepoFake) ListDirect(context.Context, BusinessPromoteFilter, ...ListOption) (*BusinessPromotePage, error) {
|
||||
r.calls++
|
||||
return &BusinessPromotePage{Available: true}, nil
|
||||
}
|
||||
func TestDirectPromotesCheckUserScopeBeforeReadingChannels(t *testing.T) {
|
||||
a := &accountRepoFake{accounts: map[uint]*Account{12: {ID: 12, Level: 1, IDPath: "0"}, 13: {ID: 13, Level: 2, IDPath: "0,12"}, 99: {ID: 99, Level: 2, IDPath: "0,123"}}}
|
||||
r := &directPromoteRepoFake{}
|
||||
uc := NewBusinessPromoteUsecase(r, NewAccountUsecase(a))
|
||||
if _, err := uc.List(accountContext(12, 901), BusinessPromoteFilter{UserID: 99}); err != ErrAccountScope || r.calls != 0 {
|
||||
t.Fatal("越权请求触达了渠道仓储")
|
||||
}
|
||||
if _, err := uc.List(accountContext(12, 901), BusinessPromoteFilter{UserID: 13}); err != nil || r.calls != 1 {
|
||||
t.Fatal("下级渠道查询未执行")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
type businessPromoteRepo struct{ data Provider }
|
||||
|
||||
func NewBusinessPromoteRepo(data Provider) business.BusinessPromoteRepo {
|
||||
return &businessPromoteRepo{data: data}
|
||||
}
|
||||
|
||||
type directPromotePO struct {
|
||||
ID uint64
|
||||
Account string
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
BusierID uint64
|
||||
}
|
||||
|
||||
func (r *businessPromoteRepo) ListDirect(ctx context.Context, filter business.BusinessPromoteFilter, options ...business.ListOption) (*business.BusinessPromotePage, error) {
|
||||
page := &business.BusinessPromotePage{Rows: []*business.BusinessPromote{}, Available: true}
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
bindings := func() *gorm.DB {
|
||||
q := db.Table("sys_user_bind_platform b").Where("b.user_id = ? AND b.status = ?", filter.UserID, "0")
|
||||
if filter.SourceDB != "" {
|
||||
q = q.Where("b.source_db = ?", filter.SourceDB)
|
||||
}
|
||||
if filter.BusierID != 0 {
|
||||
q = q.Where("b.platform_busier_id = ?", filter.BusierID)
|
||||
}
|
||||
return q
|
||||
}
|
||||
var count int64
|
||||
if err := bindings().Count(&count).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
if count == 0 {
|
||||
return page, nil
|
||||
}
|
||||
available, err := businessTableAvailable(db, "tab_promote")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !available {
|
||||
page.Available = false
|
||||
return page, nil
|
||||
}
|
||||
q := db.Table("tab_promote p").Where("p.promote_level = ?", 1).Where("EXISTS (?)", bindings().Select("1").Where("b.source_db = p.source_db AND b.platform_busier_id = p.busier_id"))
|
||||
if err := q.Count(&page.Total).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
o := business.NewListOptions(options...)
|
||||
var pos []directPromotePO
|
||||
if err := q.Select("p.id,p.account,p.source_db,p.busier_id").Order("p.id DESC,p.source_db ASC").Offset(o.Offset).Limit(o.Limit).Scan(&pos).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
for _, po := range pos {
|
||||
page.Rows = append(page.Rows, &business.BusinessPromote{ID: po.ID, Account: po.Account, SourceDB: po.SourceDB, BusierID: po.BusierID})
|
||||
}
|
||||
return page, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDirectPromotesUseApprovedBindingPairsAndStablePaging(t *testing.T) {
|
||||
db := prepareAccounts(t)
|
||||
for _, sql := range []string{
|
||||
"ALTER TABLE tab_promote ADD COLUMN account TEXT",
|
||||
"INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES(7,'lx',10,'0'),(7,'hw',10,'0'),(7,'yq',10,'3'),(8,'lx',20,'0')",
|
||||
"INSERT INTO tab_promote(id,source_db,busier_id,promote_level,account) VALUES(1,'lx',10,1,'a'),(2,'lx',10,2,'sub'),(1,'hw',10,1,'b'),(3,'yq',10,1,'pending'),(4,'lx',20,1,'other'),(5,'lx',10,1,'c')",
|
||||
} {
|
||||
if err := db.Exec(sql).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
r := NewBusinessPromoteRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
page, err := r.ListDirect(ctx, business.BusinessPromoteFilter{UserID: 7})
|
||||
if err != nil || !page.Available || page.Total != 3 || len(page.Rows) != 3 {
|
||||
t.Fatalf("直属渠道范围错误: %+v %v", page, err)
|
||||
}
|
||||
if page.Rows[0].ID != 5 || page.Rows[1].SourceDB != "hw" {
|
||||
t.Fatal("同号跨平台分页未使用稳定排序")
|
||||
}
|
||||
page, err = r.ListDirect(ctx, business.BusinessPromoteFilter{UserID: 7, SourceDB: "hw", BusierID: 10})
|
||||
if err != nil || page.Total != 1 || page.Rows[0].Account != "b" {
|
||||
t.Fatal("平台与商务筛选未同时生效")
|
||||
}
|
||||
page, err = r.ListDirect(ctx, business.BusinessPromoteFilter{UserID: 7}, business.ListOffset(1), business.ListLimit(1))
|
||||
if err != nil || page.Total != 3 || len(page.Rows) != 1 || page.Rows[0].SourceDB != "hw" {
|
||||
t.Fatal("分页或总数不正确")
|
||||
}
|
||||
if err := db.Exec("DROP TABLE tab_promote").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
page, err = r.ListDirect(ctx, business.BusinessPromoteFilter{UserID: 7})
|
||||
if err != nil || page.Available {
|
||||
t.Fatal("未接入的渠道数据不能返回正常空列表")
|
||||
}
|
||||
page, err = r.ListDirect(ctx, business.BusinessPromoteFilter{UserID: 999})
|
||||
if err != nil || !page.Available || page.Total != 0 {
|
||||
t.Fatal("无绑定用户的直属渠道应为确定的空集合")
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ var ProviderSet = wire.NewSet(
|
||||
databusiness.NewBindingRepo,
|
||||
databusiness.NewPlatformAccountRepo,
|
||||
databusiness.NewPasswordResetRepo,
|
||||
databusiness.NewBusinessPromoteRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
@@ -33,8 +33,8 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 6 || apis != 28 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 6/28", menus, apis)
|
||||
if menus != 6 || apis != 29 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 6/29", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -41,6 +41,7 @@ type routeValue struct {
|
||||
// Gin router files still bind handlers; tests keep those registrations aligned
|
||||
// with this catalog while public, audit and API metadata live only here.
|
||||
var routes = map[string]routeValue{
|
||||
"GET /sw/business/:userId/promotes": {group: "商务管理", description: "查询商务旗下直属一级渠道"},
|
||||
"POST /sw/auth/forgot-password/check": {group: "商务登录", description: "校验找回密码账号", public: true},
|
||||
"POST /sw/auth/forgot-password/send-code": {group: "商务登录", description: "发送找回密码验证码", public: true},
|
||||
"POST /sw/auth/forgot-password/verify-code": {group: "商务登录", description: "验证短信并签发重置凭证", public: true},
|
||||
|
||||
@@ -26,8 +26,9 @@ func NewGinEngine(runtime *config.Store, access *systemservice.AccessControlServ
|
||||
|
||||
func emptyHandlers() *handler.Set {
|
||||
return &handler.Set{
|
||||
Business: &handler.Business{},
|
||||
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
||||
BusinessPromote: &handler.BusinessPromote{},
|
||||
Business: &handler.Business{},
|
||||
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
||||
Permission: &handler.Permission{}, Organization: &handler.Organization{},
|
||||
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Payment: &handler.Payment{}, Task: &handler.Task{},
|
||||
Media: &handler.Media{}, Audit: &handler.Audit{}, Export: &handler.Export{},
|
||||
@@ -98,7 +99,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":211`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":212`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/service/business"
|
||||
"kra/internal/service/dto"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type BusinessPromote struct {
|
||||
service *business.BusinessPromoteService
|
||||
}
|
||||
|
||||
func NewBusinessPromote(service *business.BusinessPromoteService) *BusinessPromote {
|
||||
return &BusinessPromote{service: service}
|
||||
}
|
||||
func (h *BusinessPromote) List(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
Fail(c, "商务用户 ID 不正确")
|
||||
return
|
||||
}
|
||||
var req dto.BusinessPromoteQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
Fail(c, "查询参数格式错误")
|
||||
return
|
||||
}
|
||||
value, err := h.service.List(c.Request.Context(), uint(id), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, value)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import "github.com/google/wire"
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewBusiness,
|
||||
NewBusinessPassword,
|
||||
NewBusinessPromote,
|
||||
NewAuthority, NewMenu, NewAPI, NewPermission, NewOrganization,
|
||||
NewAnnouncement, NewEmail, NewPayment, NewTask, NewMedia, NewAudit,
|
||||
NewExport, NewVersion, NewDictionary, NewParameter, NewAPIToken,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
type Set struct {
|
||||
BusinessPromote *BusinessPromote
|
||||
Business *Business
|
||||
BusinessPassword *BusinessPassword
|
||||
SMS *SMS
|
||||
|
||||
@@ -20,6 +20,7 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En
|
||||
return
|
||||
}
|
||||
registrations := []func(){
|
||||
func() { private.GET("/sw/business/:userId/promotes", r.handlers.BusinessPromote.List) },
|
||||
func() { RegisterBusiness(private, r.handlers.Business) },
|
||||
func() { RegisterBusinessPassword(public, r.handlers.BusinessPassword) },
|
||||
func() { RegisterPublic(public, engine, r.handlers.Public) },
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type BusinessPromoteUsecase interface {
|
||||
List(context.Context, business.BusinessPromoteFilter, ...business.ListOption) (*business.BusinessPromotePage, error)
|
||||
}
|
||||
type BusinessPromoteService struct{ uc BusinessPromoteUsecase }
|
||||
|
||||
func NewBusinessPromoteService(uc *business.BusinessPromoteUsecase) *BusinessPromoteService {
|
||||
return &BusinessPromoteService{uc: uc}
|
||||
}
|
||||
func (s *BusinessPromoteService) List(ctx context.Context, id uint, req *dto.BusinessPromoteQuery) (*dto.BusinessPromotePage, error) {
|
||||
if req == nil || id == 0 {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
filter := business.BusinessPromoteFilter{UserID: id, BusierID: req.PlatformBusierID}
|
||||
if req.SourceDB != "" {
|
||||
source, err := business.NormalizePlatform(req.SourceDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter.SourceDB = source
|
||||
}
|
||||
page, size := req.Page, req.PageSize
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 10
|
||||
}
|
||||
if size > 500 {
|
||||
size = 500
|
||||
}
|
||||
if page > int(^uint(0)>>1)/size {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
result, err := s.uc.List(ctx, filter, business.ListOffset((page-1)*size), business.ListLimit(size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.BusinessPromotePage{Rows: make([]*dto.BusinessPromoteResponse, 0, len(result.Rows)), Total: result.Total, Available: result.Available}
|
||||
for _, row := range result.Rows {
|
||||
out.Rows = append(out.Rows, &dto.BusinessPromoteResponse{PromoteID: row.ID, PromoteName: row.Account, SourceDB: row.SourceDB, PlatformName: business.PlatformName(row.SourceDB), PlatformBusierID: row.BusierID})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package dto
|
||||
|
||||
type BusinessPromoteQuery struct {
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
SourceDB string `form:"sourceDb"`
|
||||
PlatformBusierID uint64 `form:"platformBusierId"`
|
||||
}
|
||||
type BusinessPromoteResponse struct {
|
||||
PromoteID uint64 `json:"promoteId"`
|
||||
PromoteName string `json:"promoteName"`
|
||||
SourceDB string `json:"sourceDb"`
|
||||
PlatformName string `json:"platformName"`
|
||||
PlatformBusierID uint64 `json:"platformBusierId"`
|
||||
}
|
||||
type BusinessPromotePage struct {
|
||||
Rows []*BusinessPromoteResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
@@ -16,6 +16,7 @@ var ProviderSet = wire.NewSet(
|
||||
businessservice.NewAccountService,
|
||||
businessservice.NewBindingService,
|
||||
businessservice.NewPasswordResetService,
|
||||
businessservice.NewBusinessPromoteService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
Reference in New Issue
Block a user