fix: align business pages and detail drawers with prototype
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
var (
|
||||
ErrChannelNotFound = errors.NotFound("SW_CHANNEL_NOT_FOUND", "渠道不存在")
|
||||
ErrChannelScope = errors.Forbidden("SW_CHANNEL_SCOPE", "无权查看该渠道")
|
||||
ErrChannelCreateScope = errors.Forbidden("SW_CHANNEL_CREATE_SCOPE", "仅三级商务可添加渠道")
|
||||
ErrChannelInput = errors.BadRequest("SW_CHANNEL_INPUT", "渠道查询参数不正确")
|
||||
ErrChannelAccount = errors.BadRequest("SW_CHANNEL_ACCOUNT", "渠道账号需为中文/英文/数字,长度 4-30 位")
|
||||
ErrChannelPassword = errors.BadRequest("SW_CHANNEL_PASSWORD", "渠道密码长度需为 6-15 位")
|
||||
@@ -35,6 +36,7 @@ type Channel struct {
|
||||
ChannelKey
|
||||
Account, Nickname, ParentName, RealName string
|
||||
Phone string
|
||||
BusinessPath []string
|
||||
SettlementDays *int
|
||||
Level, RegisterType, Status, LockStatus *int
|
||||
ParentID, TopPromoteID, BusierID uint64
|
||||
@@ -47,6 +49,9 @@ type Channel struct {
|
||||
}
|
||||
|
||||
func (c *Channel) BusinessDisplayName() string {
|
||||
if len(c.BusinessPath) > 0 {
|
||||
return strings.Join(c.BusinessPath, "→")
|
||||
}
|
||||
account, user := strings.TrimSpace(c.BindingAccount), strings.TrimSpace(c.BindingUserName)
|
||||
if account == "" {
|
||||
return user
|
||||
@@ -56,6 +61,26 @@ func (c *Channel) BusinessDisplayName() string {
|
||||
}
|
||||
return account + "(" + user + ")"
|
||||
}
|
||||
|
||||
func (c *Channel) ParentDisplayName() string {
|
||||
if c.Level != nil && *c.Level == 1 {
|
||||
if len(c.BusinessPath) > 0 {
|
||||
return c.BusinessPath[len(c.BusinessPath)-1]
|
||||
}
|
||||
return c.BusinessDisplayName()
|
||||
}
|
||||
return c.ParentName
|
||||
}
|
||||
|
||||
func ChannelLockStatusLabel(status *int) string {
|
||||
if status == nil || *status == 0 {
|
||||
return "正常"
|
||||
}
|
||||
if *status == -2 {
|
||||
return "解除合作"
|
||||
}
|
||||
return "锁定"
|
||||
}
|
||||
func (c *Channel) IncludesStat(other *Channel, includeSub bool) bool {
|
||||
if c.SourceDB != other.SourceDB {
|
||||
return false
|
||||
@@ -73,6 +98,7 @@ func (c *Channel) IncludesStat(other *Channel, includeSub bool) bool {
|
||||
}
|
||||
|
||||
type ChannelFilter struct {
|
||||
IncludeSub, IDCardDedup bool
|
||||
Effective *bool
|
||||
Sources []string
|
||||
BusierUserID *uint
|
||||
@@ -179,6 +205,9 @@ func normalizeIDCard(raw string) string {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// ChannelIdentity is shared by list metrics and storage-side metric ordering.
|
||||
func ChannelIdentity(raw string) string { return normalizeIDCard(raw) }
|
||||
func (uc *ChannelUsecase) List(ctx context.Context, filter ChannelFilter, options ...ListOption) (*ChannelPage, error) {
|
||||
scope, err := uc.scope(ctx)
|
||||
if err != nil {
|
||||
@@ -189,14 +218,15 @@ func (uc *ChannelUsecase) List(ctx context.Context, filter ChannelFilter, option
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
page, err := uc.repo.List(ctx, scope, filter, options...)
|
||||
if err != nil || len(page.Rows) == 0 {
|
||||
return page, err
|
||||
}
|
||||
pref, err := uc.preferences.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter.IncludeSub, filter.IDCardDedup = pref.IncludeSubPromote, pref.IDCardDedup
|
||||
page, err := uc.repo.List(ctx, scope, filter, options...)
|
||||
if err != nil || len(page.Rows) == 0 {
|
||||
return page, err
|
||||
}
|
||||
statChannels := page.Rows
|
||||
if pref.IncludeSubPromote {
|
||||
statChannels, err = uc.repo.StatChannels(ctx, page.Rows)
|
||||
@@ -290,6 +320,13 @@ func (uc *ChannelUsecase) Create(ctx context.Context, input *ChannelCreate) (*Ch
|
||||
if input == nil {
|
||||
return nil, ErrChannelInput
|
||||
}
|
||||
actor, accountOwner, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if actor.AuthorityID != 888 && accountOwner.Level != 3 {
|
||||
return nil, ErrChannelCreateScope
|
||||
}
|
||||
account := strings.TrimSpace(input.Account)
|
||||
if !channelAccountPattern.MatchString(account) {
|
||||
return nil, ErrChannelAccount
|
||||
|
||||
@@ -110,7 +110,12 @@ func TestChannelLevelTwoScopeAndUnavailableMetrics(t *testing.T) {
|
||||
func TestChannelCreateOwnsHashAndSettlementDefaults(t *testing.T) {
|
||||
uc, r, _, writer, encoder := channelFixture()
|
||||
input := &ChannelCreate{SourceDB: " LX ", Account: " 中文渠道 ", Password: " pass123 ", BusierID: 14, RegisterType: 1}
|
||||
result, err := uc.Create(accountContext(8, 902), input)
|
||||
for _, actor := range [][2]uint{{7, 901}, {8, 902}} {
|
||||
if _, err := uc.Create(accountContext(actor[0], actor[1]), input); err != ErrChannelCreateScope {
|
||||
t.Fatal("一级和二级商务不能添加渠道")
|
||||
}
|
||||
}
|
||||
result, err := uc.Create(accountContext(9, 903), input)
|
||||
if err != nil || result.ID != 123 || result.SourceDB != "lx" {
|
||||
t.Fatalf("创建失败: %+v %v", result, err)
|
||||
}
|
||||
@@ -118,17 +123,17 @@ func TestChannelCreateOwnsHashAndSettlementDefaults(t *testing.T) {
|
||||
t.Fatalf("创建参数或结算周期错误: %+v", writer.input)
|
||||
}
|
||||
input.BusierID = 999
|
||||
if _, err := uc.Create(accountContext(8, 902), input); err != ErrChannelBinding {
|
||||
if _, err := uc.Create(accountContext(9, 903), input); err != ErrChannelBinding {
|
||||
t.Fatal("选择了范围外的绑定")
|
||||
}
|
||||
input.BusierID = 14
|
||||
input.Account = "bad_underscore"
|
||||
if _, err := uc.Create(accountContext(8, 902), input); err != ErrChannelAccount {
|
||||
if _, err := uc.Create(accountContext(9, 903), input); err != ErrChannelAccount {
|
||||
t.Fatal("渠道账号字符限制丢失")
|
||||
}
|
||||
input.Account = "valid123"
|
||||
input.Password = "12345"
|
||||
if _, err := uc.Create(accountContext(8, 902), input); err != ErrChannelPassword {
|
||||
if _, err := uc.Create(accountContext(9, 903), input); err != ErrChannelPassword {
|
||||
t.Fatal("密码长度未校验")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func TestPerformanceRateSortKeepsUndefinedRatesLast(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformanceMatrixTotalsFollowDisplayedMemberUnion(t *testing.T) {
|
||||
func TestPerformanceMatrixTotalsKeepVisibleScopeWhenRowsAreFiltered(t *testing.T) {
|
||||
_, uc, _, teams, _ := managementFixture()
|
||||
teams.catalog.Groups[0].MemberIDs = []uint{8, 9, 10}
|
||||
ctx := accountContext(7, 901)
|
||||
@@ -181,17 +181,44 @@ func TestPerformanceMatrixTotalsFollowDisplayedMemberUnion(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page.Rows) != 1 || page.MatrixTotal.Open != 1 || page.MatrixTotal.Effective != 1 || page.MatrixTotal.Pay != "9007199254740993.03" || page.Groups[0].UniqueTotal != page.MatrixTotal {
|
||||
t.Fatalf("filtered member union/subtotal mismatch: %+v", page)
|
||||
if len(page.Rows) != 1 || page.MatrixTotal != page.Total || page.MatrixTotal.Open != 2 || page.Groups[0].UniqueTotal.Open != 1 {
|
||||
t.Fatalf("matrix total must retain the full visible scope, with a filtered group subtotal: %+v", page)
|
||||
}
|
||||
if page.Total.Open != 2 || page.Groups[0].Total.Open != 3 {
|
||||
t.Fatal("scope cards and team contributions must retain their own scope")
|
||||
}
|
||||
f.Name, f.GroupID = "", func() *uint64 { id := uint64(999); return &id }()
|
||||
page, err = uc.Query(ctx, f)
|
||||
if err != nil || len(page.Rows) != 0 || page.MatrixTotal.Open != 0 || page.MatrixTotal.Pay != "0.00" || page.Total.Open != 2 {
|
||||
t.Fatalf("empty matrix must have zero totals without changing scope cards: %+v %v", page, err)
|
||||
if err != nil || len(page.Rows) != 0 || page.MatrixTotal != page.Total || page.Total.Open != 2 {
|
||||
t.Fatalf("empty displayed rows must not erase visible-scope totals: %+v %v", page, err)
|
||||
}
|
||||
for period, total := range page.Trend {
|
||||
if page.MatrixTrend[period] != total {
|
||||
t.Fatalf("period total changed with filtering: %s", period)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusinessManagementIncludesCurrentGoalWithoutCurrentPerformance(t *testing.T) {
|
||||
_, uc, _, _, _ := managementFixture()
|
||||
amount := int64(2000000)
|
||||
uc.goals.(*managementGoalFake).values = []*PerformanceGoal{{Kind: "person", TargetID: 9, Month: "2026-11", Amount: &amount}}
|
||||
page, err := uc.Query(accountContext(7, 901), PerformanceFilter{ForManagement: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if page.End != "2026-10-31" {
|
||||
t.Fatal("business performance must still stop at the last completed month")
|
||||
}
|
||||
for _, row := range page.Rows {
|
||||
if row.Member.ID == 9 {
|
||||
if goal := row.Goals["2026-11"]; goal == nil || goal.Amount == nil || *goal.Amount != amount {
|
||||
t.Fatal("the business list must include this month's configured goal")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("visible member was lost")
|
||||
}
|
||||
|
||||
func TestPerformanceDetailSelectionPreservesContributionsAndScope(t *testing.T) {
|
||||
|
||||
@@ -173,7 +173,6 @@ func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ow
|
||||
return out, nil
|
||||
}
|
||||
all := newPerformanceBuckets(periods)
|
||||
matrix := newPerformanceBuckets(periods)
|
||||
needle := strings.ToLower(strings.TrimSpace(f.Name))
|
||||
visible := func(m *BusinessMember) bool {
|
||||
return (f.Level == nil || m.Level == *f.Level) && (needle == "" || strings.Contains(strings.ToLower(memberName(m)+" "+m.Username+" "+m.RealName), needle))
|
||||
@@ -209,9 +208,8 @@ func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ow
|
||||
}
|
||||
}
|
||||
type attribution struct {
|
||||
members []uint
|
||||
groups []uint64
|
||||
inMatrix bool
|
||||
members []uint
|
||||
groups []uint64
|
||||
}
|
||||
owners := map[ChannelKey]attribution{}
|
||||
forOwners := func(ownerIDs []uint) attribution {
|
||||
@@ -227,7 +225,6 @@ func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ow
|
||||
a := attribution{}
|
||||
for id := range members {
|
||||
a.members = append(a.members, id)
|
||||
a.inMatrix = a.inMatrix || matrixVisible(byID[id])
|
||||
}
|
||||
for id := range groups {
|
||||
a.groups = append(a.groups, id)
|
||||
@@ -262,9 +259,6 @@ func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ow
|
||||
return
|
||||
}
|
||||
all.add(key, metric, pay)
|
||||
if a.inMatrix {
|
||||
matrix.add(key, metric, pay)
|
||||
}
|
||||
for _, id := range a.members {
|
||||
memberBuckets[id].add(key, metric, pay)
|
||||
}
|
||||
@@ -309,7 +303,12 @@ func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ow
|
||||
}
|
||||
add(a, periodKey(cash.Date), "pay", pay)
|
||||
}
|
||||
goals, err := uc.goals.List(ctx, begin.Format("2006-01"), end.Format("2006-01"))
|
||||
goalEnd := end.Format("2006-01")
|
||||
if f.ForManagement {
|
||||
// The business list shows historical performance and this month's goal.
|
||||
goalEnd = snapshotTime.Format("2006-01")
|
||||
}
|
||||
goals, err := uc.goals.List(ctx, begin.Format("2006-01"), goalEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -362,9 +361,9 @@ func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ow
|
||||
}
|
||||
}
|
||||
out.Total = all.total.metrics()
|
||||
out.MatrixTotal = matrix.total.metrics()
|
||||
out.MatrixTotal = out.Total
|
||||
out.MatrixRate = performanceRate(out.MatrixTotal)
|
||||
out.MatrixTrend = matrix.values()
|
||||
out.MatrixTrend = all.values()
|
||||
out.Prior = prior.metrics()
|
||||
out.Rate = performanceRate(out.Total)
|
||||
out.Trend = all.values()
|
||||
|
||||
Reference in New Issue
Block a user