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, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase, business.NewWorkbenchUsecase)
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWorkbenchSource = errors.BadRequest("SW_WORKBENCH_SOURCE", "请选择所属平台后查看数据")
|
||||
ErrWorkbenchPeriod = errors.BadRequest("SW_WORKBENCH_PERIOD", "时间维度无效,应为 today/yesterday/last7/last30")
|
||||
ErrWorkbenchUnavailable = errors.BadRequest("SW_WORKBENCH_UNAVAILABLE", "工作台所需业务数据尚未接入")
|
||||
)
|
||||
|
||||
type WorkbenchRankKind string
|
||||
|
||||
const (
|
||||
WorkbenchRegister WorkbenchRankKind = "register"
|
||||
WorkbenchPayHigh WorkbenchRankKind = "payHigh"
|
||||
WorkbenchPayLow WorkbenchRankKind = "payLow"
|
||||
WorkbenchUnlogin WorkbenchRankKind = "unlogin"
|
||||
)
|
||||
|
||||
type WorkbenchWindow struct{ Today, Yesterday, MonthStart, PreviousMonthStart, PreviousMonthEnd time.Time }
|
||||
type WorkbenchDay struct {
|
||||
Counts PortraitCounts
|
||||
Pay string
|
||||
ActiveChannels, NewChannels int64
|
||||
}
|
||||
type WorkbenchTotals struct {
|
||||
Today, Yesterday WorkbenchDay
|
||||
MonthPay, PreviousMonthPay string
|
||||
Available bool
|
||||
}
|
||||
type WorkbenchCard struct {
|
||||
Value, CompareValue, ChangeRate *string
|
||||
CompareLabel string
|
||||
}
|
||||
type WorkbenchSummary struct {
|
||||
TodayRegister, TodayActive, TodayPay, MonthPay, ActivePromote, NewPromote *WorkbenchCard
|
||||
Available bool
|
||||
}
|
||||
type WorkbenchRankRow struct {
|
||||
AnalyticsKey
|
||||
Rank int
|
||||
Name, Value string
|
||||
CreatedUnix, LastLoginUnix *int64
|
||||
}
|
||||
type WorkbenchRankData struct {
|
||||
Rows []*WorkbenchRankRow
|
||||
Available bool
|
||||
}
|
||||
type WorkbenchRepo interface {
|
||||
Summary(context.Context, AnalyticsScope, []string, WorkbenchWindow) (*WorkbenchTotals, error)
|
||||
Ranks(context.Context, AnalyticsScope, AnalyticsFilter, WorkbenchRankKind) (*WorkbenchRankData, error)
|
||||
}
|
||||
type WorkbenchUsecase struct {
|
||||
repo WorkbenchRepo
|
||||
accounts *AccountUsecase
|
||||
preferences *UserPreferenceUsecase
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewWorkbenchUsecase(repo WorkbenchRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *WorkbenchUsecase {
|
||||
return &WorkbenchUsecase{repo: repo, accounts: accounts, preferences: preferences, now: time.Now}
|
||||
}
|
||||
func (uc *WorkbenchUsecase) scope(ctx context.Context, sources []string) (AnalyticsScope, error) {
|
||||
actor, account, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return AnalyticsScope{}, err
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return AnalyticsScope{}, ErrWorkbenchSource
|
||||
}
|
||||
pref, err := uc.preferences.Current(ctx)
|
||||
if err != nil {
|
||||
return AnalyticsScope{}, err
|
||||
}
|
||||
viewer := portraitUserScope(account)
|
||||
viewer.All = actor.AuthorityID == 888
|
||||
return AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, nil
|
||||
}
|
||||
func workbenchWindow(now time.Time) WorkbenchWindow {
|
||||
now = now.In(portraitLocation)
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, portraitLocation)
|
||||
month := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, portraitLocation)
|
||||
previous := month.AddDate(0, -1, 0)
|
||||
lastDay := month.AddDate(0, 0, -1).Day()
|
||||
previousEnd := previous.AddDate(0, 0, min(today.Day(), lastDay)-1)
|
||||
return WorkbenchWindow{Today: today, Yesterday: today.AddDate(0, 0, -1), MonthStart: month, PreviousMonthStart: previous, PreviousMonthEnd: previousEnd}
|
||||
}
|
||||
func workbenchCard(current, previous, label string, available bool) (*WorkbenchCard, error) {
|
||||
card := &WorkbenchCard{CompareLabel: label}
|
||||
if !available {
|
||||
return card, nil
|
||||
}
|
||||
cur, err := portraitNumber(current)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prev, err := portraitNumber(previous)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
card.Value, card.CompareValue = stringPointer(portraitDecimal(cur, 2)), stringPointer(portraitDecimal(prev, 2))
|
||||
cur, _ = portraitNumber(*card.Value)
|
||||
prev, _ = portraitNumber(*card.CompareValue)
|
||||
if prev.Sign() != 0 {
|
||||
diff := new(big.Rat).Sub(cur, prev)
|
||||
diff.Mul(diff, big.NewRat(100, 1))
|
||||
diff.Quo(diff, prev)
|
||||
card.ChangeRate = stringPointer(portraitDecimal(diff, 1))
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
func (uc *WorkbenchUsecase) Summary(ctx context.Context, sources []string) (*WorkbenchSummary, error) {
|
||||
scope, err := uc.scope(ctx, sources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := uc.repo.Summary(ctx, scope, sources, workbenchWindow(uc.now()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.Today.Counts, data.Yesterday.Counts = analyticsCap(data.Today.Counts), analyticsCap(data.Yesterday.Counts)
|
||||
integer := func(n int64) string { return big.NewInt(n).String() }
|
||||
out := &WorkbenchSummary{Available: data.Available}
|
||||
for _, item := range []struct {
|
||||
field **WorkbenchCard
|
||||
current, previous, label string
|
||||
}{
|
||||
{&out.TodayRegister, integer(data.Today.Counts.Register), integer(data.Yesterday.Counts.Register), "环比昨日"},
|
||||
{&out.TodayActive, integer(data.Today.Counts.Active), integer(data.Yesterday.Counts.Active), "环比昨日"},
|
||||
{&out.TodayPay, data.Today.Pay, data.Yesterday.Pay, "环比昨日"},
|
||||
{&out.MonthPay, data.MonthPay, data.PreviousMonthPay, "环比上月"},
|
||||
{&out.ActivePromote, integer(data.Today.ActiveChannels), integer(data.Yesterday.ActiveChannels), "环比昨日"},
|
||||
{&out.NewPromote, integer(data.Today.NewChannels), integer(data.Yesterday.NewChannels), "环比昨日"},
|
||||
} {
|
||||
card, err := workbenchCard(item.current, item.previous, item.label, data.Available)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*item.field = card
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (uc *WorkbenchUsecase) Ranks(ctx context.Context, sources []string, period string, kind WorkbenchRankKind) ([]*WorkbenchRankRow, error) {
|
||||
scope, err := uc.scope(ctx, sources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kind != WorkbenchRegister && kind != WorkbenchPayHigh && kind != WorkbenchPayLow && kind != WorkbenchUnlogin {
|
||||
return nil, ErrAnalyticsInput
|
||||
}
|
||||
period = strings.ToLower(strings.TrimSpace(period))
|
||||
if period == "" {
|
||||
period = "today"
|
||||
if kind == WorkbenchPayLow {
|
||||
period = "last30"
|
||||
}
|
||||
}
|
||||
w := workbenchWindow(uc.now())
|
||||
begin, end := w.Today, w.Today
|
||||
if kind != WorkbenchUnlogin {
|
||||
switch period {
|
||||
case "today":
|
||||
case "yesterday":
|
||||
begin, end = w.Yesterday, w.Yesterday
|
||||
case "last7":
|
||||
begin = end.AddDate(0, 0, -6)
|
||||
case "last30":
|
||||
begin = end.AddDate(0, 0, -29)
|
||||
default:
|
||||
return nil, ErrWorkbenchPeriod
|
||||
}
|
||||
}
|
||||
data, err := uc.repo.Ranks(ctx, scope, AnalyticsFilter{Sources: sources, Begin: begin.Format("2006-01-02"), End: end.Format("2006-01-02")}, kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !data.Available {
|
||||
return nil, ErrWorkbenchUnavailable
|
||||
}
|
||||
rows := make([]*WorkbenchRankRow, 0, len(data.Rows))
|
||||
values := map[*WorkbenchRankRow]*big.Rat{}
|
||||
for _, row := range data.Rows {
|
||||
if kind == WorkbenchUnlogin {
|
||||
row.Value = big.NewInt(workbenchUnloginDays(row, w.Today)).String()
|
||||
}
|
||||
n, err := portraitNumber(row.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kind == WorkbenchRegister && n.Cmp(big.NewRat(2147483647, 1)) > 0 {
|
||||
n = big.NewRat(2147483647, 1)
|
||||
}
|
||||
if (kind == WorkbenchRegister || kind == WorkbenchPayHigh) && n.Sign() <= 0 {
|
||||
continue
|
||||
}
|
||||
values[row] = n
|
||||
rows = append(rows, row)
|
||||
}
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
a, b := rows[i], rows[j]
|
||||
if c := values[a].Cmp(values[b]); c != 0 {
|
||||
if kind == WorkbenchPayLow {
|
||||
return c < 0
|
||||
}
|
||||
return c > 0
|
||||
}
|
||||
if a.ID != b.ID {
|
||||
return a.ID < b.ID
|
||||
}
|
||||
return a.SourceDB < b.SourceDB
|
||||
})
|
||||
rows = rows[:min(10, len(rows))]
|
||||
for i, row := range rows {
|
||||
row.Rank = i + 1
|
||||
row.Name = (&LedgerNames{PromoteID: &row.ID, PromoteAccount: row.Name}).DisplayPromote()
|
||||
if kind != WorkbenchUnlogin {
|
||||
row.Value = portraitDecimal(values[row], 2)
|
||||
}
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
func workbenchUnloginDays(row *WorkbenchRankRow, today time.Time) int64 {
|
||||
stamp := row.LastLoginUnix
|
||||
if stamp == nil || *stamp <= 0 {
|
||||
stamp = row.CreatedUnix
|
||||
}
|
||||
if stamp == nil || *stamp <= 0 {
|
||||
return 99999
|
||||
}
|
||||
date := time.Unix(*stamp, 0).In(portraitLocation)
|
||||
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, portraitLocation)
|
||||
if date.After(today) {
|
||||
return 0
|
||||
}
|
||||
return min(int64(today.Sub(date).Hours()/24), 2147483647)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type workbenchRepoFake struct {
|
||||
totals *WorkbenchTotals
|
||||
data *WorkbenchRankData
|
||||
window WorkbenchWindow
|
||||
filter AnalyticsFilter
|
||||
scope AnalyticsScope
|
||||
}
|
||||
|
||||
func (r *workbenchRepoFake) Summary(_ context.Context, s AnalyticsScope, _ []string, w WorkbenchWindow) (*WorkbenchTotals, error) {
|
||||
r.window, r.scope = w, s
|
||||
c := *r.totals
|
||||
return &c, nil
|
||||
}
|
||||
func (r *workbenchRepoFake) Ranks(_ context.Context, s AnalyticsScope, f AnalyticsFilter, _ WorkbenchRankKind) (*WorkbenchRankData, error) {
|
||||
r.filter, r.scope = f, s
|
||||
out := &WorkbenchRankData{Available: r.data.Available}
|
||||
for _, row := range r.data.Rows {
|
||||
c := *row
|
||||
out.Rows = append(out.Rows, &c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func workbenchFixture() (*WorkbenchUsecase, *workbenchRepoFake) {
|
||||
r := &workbenchRepoFake{totals: &WorkbenchTotals{Available: true, Today: WorkbenchDay{Counts: PortraitCounts{Register: 3, Active: 4}, Pay: "20.005", ActiveChannels: 2, NewChannels: 1}, Yesterday: WorkbenchDay{Counts: PortraitCounts{Register: 2}, Pay: "10", ActiveChannels: 1}, MonthPay: "300", PreviousMonthPay: "200"}, data: &WorkbenchRankData{Available: true}}
|
||||
a := &accountRepoFake{accounts: map[uint]*Account{7: {ID: 7, Level: 1, IDPath: "0"}}}
|
||||
uc := NewWorkbenchUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(&channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}}))
|
||||
uc.now = func() time.Time { return time.Date(2026, 3, 31, 9, 0, 0, 0, portraitLocation) }
|
||||
return uc, r
|
||||
}
|
||||
func TestWorkbenchSummaryCalendarAndRatios(t *testing.T) {
|
||||
uc, r := workbenchFixture()
|
||||
out, err := uc.Summary(accountContext(7, 901), []string{"lx"})
|
||||
if err != nil || *out.TodayRegister.Value != "3.00" || *out.TodayRegister.ChangeRate != "50.0" || out.TodayActive.ChangeRate != nil || *out.TodayPay.Value != "20.01" || *out.TodayPay.ChangeRate != "100.1" || *out.MonthPay.ChangeRate != "50.0" || *out.ActivePromote.ChangeRate != "100.0" {
|
||||
t.Fatalf("卡片精度或环比错误: %+v %v", out, err)
|
||||
}
|
||||
if r.window.PreviousMonthEnd.Format("2006-01-02") != "2026-02-28" || r.window.PreviousMonthStart.Format("2006-01-02") != "2026-02-01" {
|
||||
t.Fatal("上月同期需要截断至月末")
|
||||
}
|
||||
r.totals.Available = false
|
||||
out, err = uc.Summary(accountContext(7, 901), []string{"lx"})
|
||||
if err != nil || out.TodayPay.Value != nil || out.TodayPay.CompareValue != nil || out.Available {
|
||||
t.Fatal("缺表不能返回零卡片")
|
||||
}
|
||||
if _, err := uc.Summary(accountContext(7, 901), nil); err != ErrWorkbenchSource {
|
||||
t.Fatal("必须选择平台")
|
||||
}
|
||||
}
|
||||
func TestWorkbenchRankLimitsExactSortDefaultsAndUnlogin(t *testing.T) {
|
||||
uc, r := workbenchFixture()
|
||||
ctx := accountContext(7, 901)
|
||||
for i := 0; i < 14; i++ {
|
||||
r.data.Rows = append(r.data.Rows, &WorkbenchRankRow{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: int64(i)}, Value: fmt.Sprint(i)})
|
||||
}
|
||||
rows, err := uc.Ranks(ctx, []string{"lx"}, "", WorkbenchPayHigh)
|
||||
if err != nil || len(rows) != 10 || rows[0].ID != 13 || rows[9].Rank != 10 {
|
||||
t.Fatal("榜单排序或 TOP10 截断错误")
|
||||
}
|
||||
rows, err = uc.Ranks(ctx, []string{"lx"}, "", WorkbenchPayLow)
|
||||
if err != nil || rows[0].ID != 0 || rows[0].Value != "0.00" || rows[0].Name != "官方渠道" || r.filter.Begin != "2026-03-02" {
|
||||
t.Fatal("最低榜应包含零值,默认近30天")
|
||||
}
|
||||
r.data.Rows = []*WorkbenchRankRow{{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 1}, Value: "9007199254740993.00"}, {AnalyticsKey: AnalyticsKey{SourceDB: "hw", ID: 2}, Value: "9007199254740992.99"}}
|
||||
rows, err = uc.Ranks(ctx, []string{"lx", "hw"}, "last7", WorkbenchPayHigh)
|
||||
if err != nil || rows[0].ID != 1 || r.filter.Begin != "2026-03-25" {
|
||||
t.Fatal("金额排序丢失精度或近7天范围错误")
|
||||
}
|
||||
if _, err := uc.Ranks(ctx, []string{"lx"}, "invalid", WorkbenchRegister); err != ErrWorkbenchPeriod {
|
||||
t.Fatal("无效时间维度必须拒绝")
|
||||
}
|
||||
today := workbenchWindow(uc.now()).Today
|
||||
created := today.AddDate(0, 0, -5).Unix()
|
||||
future := today.AddDate(0, 0, 1).Unix()
|
||||
r.data.Rows = []*WorkbenchRankRow{{AnalyticsKey: AnalyticsKey{ID: 1}}, {AnalyticsKey: AnalyticsKey{ID: 2}, CreatedUnix: &created}, {AnalyticsKey: AnalyticsKey{ID: 3}, CreatedUnix: &created, LastLoginUnix: &future}}
|
||||
rows, err = uc.Ranks(ctx, []string{"lx"}, "ignored", WorkbenchUnlogin)
|
||||
if err != nil || rows[0].Value != "99999" || rows[1].Value != "5" || rows[2].Value != "0" {
|
||||
t.Fatal("未登录回退创建时间、无时间或未来时间错误")
|
||||
}
|
||||
r.data.Available = false
|
||||
if _, err := uc.Ranks(ctx, []string{"lx"}, "", WorkbenchUnlogin); err != ErrWorkbenchUnavailable {
|
||||
t.Fatal("缺数据不能返回空榜单")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
type workbenchRepo struct{ data Provider }
|
||||
|
||||
func NewWorkbenchRepo(data Provider) business.WorkbenchRepo { return &workbenchRepo{data: data} }
|
||||
func workbenchAvailable(db *gorm.DB, scope business.AnalyticsScope, sources []string, kind business.WorkbenchRankKind) (bool, bool, error) {
|
||||
empty, available, err := prepareAnalytics(db, scope, business.AnalyticsFilter{Sources: sources})
|
||||
if err != nil || empty || !available {
|
||||
return empty, available, err
|
||||
}
|
||||
tables := []string{}
|
||||
if kind == "" || kind == business.WorkbenchPayHigh || kind == business.WorkbenchPayLow {
|
||||
tables = append(tables, "tab_sw_report_day")
|
||||
}
|
||||
if kind == "" || kind == business.WorkbenchRegister {
|
||||
tables = append(tables, "tab_sw_report_day_key")
|
||||
}
|
||||
for _, table := range tables {
|
||||
available, err := businessTableAvailable(db, table)
|
||||
if err != nil || !available {
|
||||
return false, available, err
|
||||
}
|
||||
}
|
||||
return false, true, nil
|
||||
}
|
||||
func (r *workbenchRepo) Summary(ctx context.Context, scope business.AnalyticsScope, sources []string, w business.WorkbenchWindow) (*business.WorkbenchTotals, error) {
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
out := &business.WorkbenchTotals{Available: true}
|
||||
empty, available, err := workbenchAvailable(db, scope, sources, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Available = available
|
||||
if empty || !available {
|
||||
return out, nil
|
||||
}
|
||||
mode := 0
|
||||
if scope.IDCardDedup {
|
||||
mode = 1
|
||||
}
|
||||
for _, period := range []struct {
|
||||
day time.Time
|
||||
target *business.WorkbenchDay
|
||||
}{{w.Today, &out.Today}, {w.Yesterday, &out.Yesterday}} {
|
||||
f := business.AnalyticsFilter{Sources: sources, Begin: period.day.Format("2006-01-02"), End: period.day.Format("2006-01-02")}
|
||||
var counts []portraitCountPO
|
||||
if err := analyticsQuery(db, scope, f, "tab_sw_report_day_key", mode).Select("r.metric,COUNT(DISTINCT " + portraitIdentity(db, scope.IDCardDedup) + ") AS count").Where("r.metric IN (1,2)").Group("r.metric").Scan(&counts).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
period.target.Counts = toPortraitCounts(counts)
|
||||
if err := analyticsQuery(db, scope, f, "tab_sw_report_day", 0).Select("COALESCE(SUM(r.total_pay),0)").Scan(&period.target.Pay).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
// 活跃渠道按自身注册或正充值统计,不能归并一级渠道后再计数。
|
||||
active := map[business.AnalyticsKey]bool{}
|
||||
var registrations, pays []analyticsAggPO
|
||||
if err := analyticsQuery(db, scope, f, "tab_sw_report_day_key", mode).Where("r.metric=1").Select("r.source_db,r.promote_id AS entity_id").Group("r.source_db,r.promote_id").Having("COUNT(DISTINCT r.identity_key)>0").Scan(®istrations).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
if err := analyticsQuery(db, scope, f, "tab_sw_report_day", 0).Select("r.source_db,r.promote_id AS entity_id").Group("r.source_db,r.promote_id").Having("SUM(r.total_pay)>0").Scan(&pays).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
for _, po := range append(registrations, pays...) {
|
||||
if po.ID != nil {
|
||||
active[business.AnalyticsKey{SourceDB: po.SourceDB, ID: *po.ID}] = true
|
||||
}
|
||||
}
|
||||
period.target.ActiveChannels = int64(len(active))
|
||||
if err := analyticsBasePromotes(db, scope, f, "p").Where("p.create_time BETWEEN ? AND ?", period.day.Unix(), period.day.AddDate(0, 0, 1).Unix()-1).Count(&period.target.NewChannels).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
}
|
||||
for _, period := range []struct {
|
||||
begin, end time.Time
|
||||
target *string
|
||||
}{{w.MonthStart, w.Today, &out.MonthPay}, {w.PreviousMonthStart, w.PreviousMonthEnd, &out.PreviousMonthPay}} {
|
||||
f := business.AnalyticsFilter{Sources: sources, Begin: period.begin.Format("2006-01-02"), End: period.end.Format("2006-01-02")}
|
||||
if err := analyticsQuery(db, scope, f, "tab_sw_report_day", 0).Select("COALESCE(SUM(r.total_pay),0)").Scan(period.target).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type workbenchPromotePO struct {
|
||||
ID int64
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
Account string
|
||||
CreateTime, LastLoginTime *int64
|
||||
}
|
||||
|
||||
func (r *workbenchRepo) Ranks(ctx context.Context, scope business.AnalyticsScope, f business.AnalyticsFilter, kind business.WorkbenchRankKind) (*business.WorkbenchRankData, error) {
|
||||
db := r.data.DB().WithContext(ctx)
|
||||
out := &business.WorkbenchRankData{Rows: []*business.WorkbenchRankRow{}, Available: true}
|
||||
empty, available, err := workbenchAvailable(db, scope, f.Sources, kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Available = available
|
||||
if empty || !available {
|
||||
return out, nil
|
||||
}
|
||||
rows := map[business.AnalyticsKey]*business.WorkbenchRankRow{}
|
||||
rowFor := func(source string, id int64) *business.WorkbenchRankRow {
|
||||
key := business.AnalyticsKey{SourceDB: source, ID: id}
|
||||
if rows[key] == nil {
|
||||
rows[key] = &business.WorkbenchRankRow{AnalyticsKey: key, Value: "0"}
|
||||
}
|
||||
return rows[key]
|
||||
}
|
||||
if kind == business.WorkbenchPayLow || kind == business.WorkbenchUnlogin {
|
||||
var channels []workbenchPromotePO
|
||||
if err := analyticsBasePromotes(db, scope, f, "p").Select("p.id,p.source_db,p.account,p.create_time,p.last_login_time").Where("p.promote_level=1").Scan(&channels).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
for _, po := range channels {
|
||||
row := rowFor(po.SourceDB, po.ID)
|
||||
row.Name, row.CreatedUnix, row.LastLoginUnix = po.Account, po.CreateTime, po.LastLoginTime
|
||||
}
|
||||
}
|
||||
if kind != business.WorkbenchUnlogin {
|
||||
mode, table := 0, "tab_sw_report_day"
|
||||
if kind == business.WorkbenchRegister {
|
||||
table = "tab_sw_report_day_key"
|
||||
if scope.IDCardDedup {
|
||||
mode = 1
|
||||
}
|
||||
}
|
||||
q := analyticsQuery(db, scope, f, table, mode)
|
||||
id := "r.promote_id"
|
||||
if kind == business.WorkbenchRegister {
|
||||
q = q.Where("r.metric=1")
|
||||
if scope.IncludeSub {
|
||||
// 旧工作台的注册榜使用 INNER JOIN + CASE,区别于渠道统计的历史 LEFT JOIN 口径。
|
||||
q = q.Joins("INNER JOIN tab_promote root_p ON root_p.source_db=r.source_db AND root_p.id=r.promote_id")
|
||||
id = "CASE WHEN root_p.promote_level=1 THEN root_p.id WHEN root_p.top_promote_id>0 THEN root_p.top_promote_id ELSE root_p.id END"
|
||||
}
|
||||
} else {
|
||||
q = q.Joins("LEFT JOIN tab_promote root_p ON root_p.source_db=r.source_db AND root_p.id=r.promote_id")
|
||||
id = "CASE WHEN root_p.promote_level=1 THEN root_p.id WHEN root_p.top_promote_id>0 THEN root_p.top_promote_id ELSE COALESCE(root_p.id,r.promote_id) END"
|
||||
}
|
||||
value := "COALESCE(SUM(r.total_pay),0)"
|
||||
if kind == business.WorkbenchRegister {
|
||||
value = "COUNT(DISTINCT r.identity_key)"
|
||||
}
|
||||
var agg []analyticsAggPO
|
||||
if err := q.Select("r.source_db," + id + " AS entity_id," + value + " AS amount").Group("r.source_db," + id).Scan(&agg).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
for _, po := range agg {
|
||||
if po.ID != nil {
|
||||
rowFor(po.SourceDB, *po.ID).Value = po.Amount
|
||||
}
|
||||
}
|
||||
keys := make([]business.AnalyticsKey, 0, len(rows))
|
||||
for key := range rows {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
for start := 0; start < len(keys); start += 300 {
|
||||
var names []workbenchPromotePO
|
||||
if err := db.Table("tab_promote").Select("id,source_db,account").Where("(source_db,id) IN ?", analyticsPairs(keys[start:min(start+300, len(keys))])).Scan(&names).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
for _, po := range names {
|
||||
rowFor(po.SourceDB, po.ID).Name = po.Account
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, row := range rows {
|
||||
out.Rows = append(out.Rows, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkbenchStorageSummaryAndRankRules(t *testing.T) {
|
||||
db, _ := prepareChannels(t)
|
||||
ctx := context.Background()
|
||||
day := time.Date(2026, 11, 4, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
|
||||
for _, sql := range []string{
|
||||
"ALTER TABLE tab_promote ADD COLUMN last_login_time INTEGER",
|
||||
"INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id,create_time) VALUES(105,'lx','zero-channel',1,0,0,14,1000)",
|
||||
"DELETE FROM tab_sw_report_day",
|
||||
"DELETE FROM tab_sw_report_day_key",
|
||||
"INSERT INTO tab_sw_report_day VALUES('lx',100,1,0,'2026-11-04',10),('lx',101,1,0,'2026-11-04',20),('hw',100,1,0,'2026-11-04',40),('lx',103,1,0,'2026-11-04',-5),('lx',0,1,0,'2026-11-04',5),('lx',999,1,0,'2026-11-04',7),('lx',100,1,1,'2026-11-04',9999),('lx',100,1,0,'2026-11-03',5),('hw',100,1,0,'2026-11-03',5),('lx',100,1,0,'2026-11-01',1),('lx',100,1,0,'2026-10-01',8),('lx',100,1,0,'2026-10-04',12),('lx',100,1,0,'2026-10-05',999)",
|
||||
"INSERT INTO tab_sw_report_day_key VALUES('lx',100,1,1,'2026-11-04',1,'A'),('lx',101,1,1,'2026-11-04',1,'A'),('hw',100,1,1,'2026-11-04',1,'A'),('lx',100,1,1,'2026-11-04',2,'A'),('lx',101,1,1,'2026-11-04',2,'B'),('hw',100,1,1,'2026-11-04',2,'A'),('lx',100,1,0,'2026-11-04',1,'1'),('lx',101,1,0,'2026-11-04',1,'2'),('hw',100,1,0,'2026-11-04',1,'1'),('lx',0,1,1,'2026-11-04',1,'official'),('lx',999,1,1,'2026-11-04',1,'orphan')",
|
||||
} {
|
||||
if err := db.Exec(sql).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Exec("UPDATE tab_promote SET create_time=? WHERE source_db='lx' AND id IN(100,101)", day.Unix()).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := business.WorkbenchWindow{Today: day, Yesterday: day.AddDate(0, 0, -1), MonthStart: day.AddDate(0, 0, -3), PreviousMonthStart: day.AddDate(0, -1, -3), PreviousMonthEnd: day.AddDate(0, -1, 0)}
|
||||
r := NewWorkbenchRepo(testData{db})
|
||||
scope := business.AnalyticsScope{Viewer: channelTestScope(), IncludeSub: true, IDCardDedup: true}
|
||||
sources := []string{"lx", "hw"}
|
||||
out, err := r.Summary(ctx, scope, sources, w)
|
||||
if err != nil || out.Today.Counts.Register != 1 || out.Today.Counts.Active != 2 || portraitTestMoney(out.Today.Pay) != "65.00" || out.Today.ActiveChannels != 3 || out.Today.NewChannels != 2 || portraitTestMoney(out.MonthPay) != "76.00" || portraitTestMoney(out.PreviousMonthPay) != "20.00" {
|
||||
t.Fatalf("工作台统计口径错误: %+v %v", out, err)
|
||||
}
|
||||
scope.IDCardDedup = false
|
||||
out, err = r.Summary(ctx, scope, sources, w)
|
||||
if err != nil || out.Today.Counts.Register != 3 || portraitTestMoney(out.Today.Pay) != "65.00" {
|
||||
t.Fatal("模式0应按平台前缀去重,金额不变")
|
||||
}
|
||||
scope.IncludeSub = false
|
||||
out, err = r.Summary(ctx, scope, sources, w)
|
||||
if err != nil || out.Today.Counts.Register != 2 || out.Today.NewChannels != 1 || out.Today.ActiveChannels != 2 || portraitTestMoney(out.Today.Pay) != "50.00" {
|
||||
t.Fatal("下级偏好未作用到渠道数和金额")
|
||||
}
|
||||
scope = business.AnalyticsScope{Viewer: business.UserScope{All: true}, IncludeSub: true, IDCardDedup: true}
|
||||
f := business.AnalyticsFilter{Sources: sources, Begin: "2026-11-04", End: "2026-11-04"}
|
||||
rows, err := r.Ranks(ctx, scope, f, business.WorkbenchRegister)
|
||||
if err != nil || len(rows.Rows) != 2 {
|
||||
t.Fatalf("注册榜含下级时应排除无渠道实体的官方/孤立记录: %+v %v", rows, err)
|
||||
}
|
||||
for _, row := range rows.Rows {
|
||||
if row.ID != 100 || row.Value != "1" {
|
||||
t.Fatal("注册榜应整棵树去重")
|
||||
}
|
||||
}
|
||||
rows, err = r.Ranks(ctx, scope, f, business.WorkbenchPayLow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
values := map[business.AnalyticsKey]string{}
|
||||
for _, row := range rows.Rows {
|
||||
values[row.AnalyticsKey] = portraitTestMoney(row.Value)
|
||||
}
|
||||
for id, want := range map[int64]string{0: "5.00", 100: "30.00", 103: "-5.00", 105: "0.00", 999: "7.00"} {
|
||||
if values[business.AnalyticsKey{SourceDB: "lx", ID: id}] != want {
|
||||
t.Fatalf("最低榜漏掉零实体或汇总错误: %+v", values)
|
||||
}
|
||||
}
|
||||
scope.Viewer = channelTestScope()
|
||||
rows, err = r.Ranks(ctx, scope, f, business.WorkbenchUnlogin)
|
||||
if err != nil || len(rows.Rows) != 3 {
|
||||
t.Fatal("未登录榜只取可见一级实体")
|
||||
}
|
||||
if err := db.Exec("DROP TABLE tab_sw_report_day").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, err = r.Ranks(ctx, scope, f, business.WorkbenchRegister)
|
||||
if err != nil || !rows.Available {
|
||||
t.Fatal("注册榜不应依赖金额汇总表")
|
||||
}
|
||||
out, err = r.Summary(ctx, scope, sources, w)
|
||||
if err != nil || out.Available {
|
||||
t.Fatal("六卡缺表应明确未接入")
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ var ProviderSet = wire.NewSet(
|
||||
databusiness.NewLedgerRepo,
|
||||
databusiness.NewAnalyticsRepo,
|
||||
databusiness.NewServerAnalysisRepo,
|
||||
databusiness.NewWorkbenchRepo,
|
||||
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 != 17 || apis != 62 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 17/62", menus, apis)
|
||||
if menus != 17 || apis != 66 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 17/66", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -41,6 +41,10 @@ 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/workbench/summary": {group: "工作台", description: "查询商务六项指标与同期对照"},
|
||||
"GET /sw/workbench/registerTop": {group: "工作台", description: "查询渠道注册排行"},
|
||||
"GET /sw/workbench/payRank": {group: "工作台", description: "查询渠道充值最高与最低排行"},
|
||||
"GET /sw/workbench/unloginTop": {group: "工作台", description: "查询未登录渠道排行"},
|
||||
"GET /sw/serverAnalysis/servers": {group: "区服分析", description: "查询游戏区服选项"},
|
||||
"GET /sw/serverAnalysis/list": {group: "区服分析", description: "查询区服分析与汇总"},
|
||||
"GET /sw/channelData/list": {group: "渠道统计", description: "查询渠道数据与汇总"},
|
||||
|
||||
@@ -28,6 +28,7 @@ func emptyHandlers() *handler.Set {
|
||||
return &handler.Set{
|
||||
Analytics: &handler.Analytics{},
|
||||
ServerAnalysis: &handler.ServerAnalysis{},
|
||||
Workbench: &handler.Workbench{},
|
||||
Ledger: &handler.Ledger{},
|
||||
ChannelPortrait: &handler.ChannelPortrait{},
|
||||
BusinessPortrait: &handler.BusinessPortrait{},
|
||||
@@ -108,7 +109,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":245`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":249`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewLedger,
|
||||
NewAnalytics,
|
||||
NewServerAnalysis,
|
||||
NewWorkbench,
|
||||
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 {
|
||||
Workbench *Workbench
|
||||
ServerAnalysis *ServerAnalysis
|
||||
Analytics *Analytics
|
||||
Ledger *Ledger
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
businessbiz "kra/internal/biz/business"
|
||||
"kra/internal/service/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type Workbench struct{ service *business.WorkbenchService }
|
||||
|
||||
func NewWorkbench(service *business.WorkbenchService) *Workbench { return &Workbench{service: service} }
|
||||
func (h *Workbench) Summary(c *gin.Context) {
|
||||
var req dto.WorkbenchQuery
|
||||
if c.ShouldBindQuery(&req) != nil {
|
||||
Fail(c, "工作台查询参数不正确")
|
||||
return
|
||||
}
|
||||
data, err := h.service.Summary(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, data)
|
||||
}
|
||||
func (h *Workbench) ranks(c *gin.Context, kind businessbiz.WorkbenchRankKind) {
|
||||
var req dto.WorkbenchQuery
|
||||
if c.ShouldBindQuery(&req) != nil {
|
||||
Fail(c, "工作台查询参数不正确")
|
||||
return
|
||||
}
|
||||
data, err := h.service.Ranks(c.Request.Context(), &req, kind)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, data)
|
||||
}
|
||||
func (h *Workbench) RegisterTop(c *gin.Context) { h.ranks(c, businessbiz.WorkbenchRegister) }
|
||||
func (h *Workbench) PayRank(c *gin.Context) { h.ranks(c, businessbiz.WorkbenchPayHigh) }
|
||||
func (h *Workbench) UnloginTop(c *gin.Context) { h.ranks(c, businessbiz.WorkbenchUnlogin) }
|
||||
@@ -20,6 +20,12 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En
|
||||
return
|
||||
}
|
||||
registrations := []func(){
|
||||
func() {
|
||||
private.GET("/sw/workbench/summary", r.handlers.Workbench.Summary)
|
||||
private.GET("/sw/workbench/registerTop", r.handlers.Workbench.RegisterTop)
|
||||
private.GET("/sw/workbench/payRank", r.handlers.Workbench.PayRank)
|
||||
private.GET("/sw/workbench/unloginTop", r.handlers.Workbench.UnloginTop)
|
||||
},
|
||||
func() {
|
||||
private.GET("/sw/serverAnalysis/servers", r.handlers.ServerAnalysis.Servers)
|
||||
private.GET("/sw/serverAnalysis/list", r.handlers.ServerAnalysis.List)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WorkbenchUsecase interface {
|
||||
Summary(context.Context, []string) (*business.WorkbenchSummary, error)
|
||||
Ranks(context.Context, []string, string, business.WorkbenchRankKind) ([]*business.WorkbenchRankRow, error)
|
||||
}
|
||||
type WorkbenchService struct{ uc WorkbenchUsecase }
|
||||
|
||||
func NewWorkbenchService(uc *business.WorkbenchUsecase) *WorkbenchService {
|
||||
return &WorkbenchService{uc: uc}
|
||||
}
|
||||
func workbenchRequest(req *dto.WorkbenchQuery) ([]string, error) {
|
||||
if req == nil {
|
||||
return nil, business.ErrWorkbenchSource
|
||||
}
|
||||
return normalizePlatforms(req.Sources)
|
||||
}
|
||||
func workbenchCardResponse(card *business.WorkbenchCard) *dto.WorkbenchCard {
|
||||
return &dto.WorkbenchCard{Value: card.Value, CompareValue: card.CompareValue, ChangeRate: card.ChangeRate, CompareLabel: card.CompareLabel}
|
||||
}
|
||||
func (s *WorkbenchService) Summary(ctx context.Context, req *dto.WorkbenchQuery) (*dto.WorkbenchSummary, error) {
|
||||
sources, err := workbenchRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.uc.Summary(ctx, sources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.WorkbenchSummary{TodayRegister: workbenchCardResponse(data.TodayRegister), TodayActive: workbenchCardResponse(data.TodayActive), TodayPay: workbenchCardResponse(data.TodayPay), MonthPay: workbenchCardResponse(data.MonthPay), ActivePromote: workbenchCardResponse(data.ActivePromote), NewPromote: workbenchCardResponse(data.NewPromote), Available: data.Available}, nil
|
||||
}
|
||||
func (s *WorkbenchService) Ranks(ctx context.Context, req *dto.WorkbenchQuery, kind business.WorkbenchRankKind) ([]*dto.WorkbenchRankRow, error) {
|
||||
sources, err := workbenchRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kind == business.WorkbenchPayHigh && strings.EqualFold(strings.TrimSpace(req.Order), "asc") {
|
||||
kind = business.WorkbenchPayLow
|
||||
}
|
||||
rows, err := s.uc.Ranks(ctx, sources, req.Period, kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*dto.WorkbenchRankRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, &dto.WorkbenchRankRow{Rank: row.Rank, SourceDB: row.SourceDB, SourceDBLabel: business.PlatformName(row.SourceDB), PromoteID: row.ID, PromoteName: row.Name, Value: row.Value})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dto
|
||||
|
||||
type WorkbenchQuery struct {
|
||||
Sources []string `form:"sourceDbList"`
|
||||
Period string `form:"period"`
|
||||
Order string `form:"order"`
|
||||
}
|
||||
type WorkbenchCard struct {
|
||||
Value *string `json:"value"`
|
||||
CompareValue *string `json:"compareValue"`
|
||||
ChangeRate *string `json:"changeRate"`
|
||||
ChangeDiff *string `json:"changeDiff"`
|
||||
CompareLabel string `json:"compareLabel"`
|
||||
}
|
||||
type WorkbenchSummary struct {
|
||||
TodayRegister *WorkbenchCard `json:"todayRegister"`
|
||||
TodayActive *WorkbenchCard `json:"todayActive"`
|
||||
TodayPay *WorkbenchCard `json:"todayPay"`
|
||||
MonthPay *WorkbenchCard `json:"monthPay"`
|
||||
ActivePromote *WorkbenchCard `json:"activePromote"`
|
||||
NewPromote *WorkbenchCard `json:"newPromote"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
type WorkbenchRankRow struct {
|
||||
Rank int `json:"rank"`
|
||||
SourceDB string `json:"sourceDb"`
|
||||
SourceDBLabel string `json:"sourceDbLabel"`
|
||||
PromoteID int64 `json:"promoteId"`
|
||||
PromoteName string `json:"promoteName"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@@ -26,6 +26,7 @@ var ProviderSet = wire.NewSet(
|
||||
businessservice.NewLedgerService,
|
||||
businessservice.NewAnalyticsService,
|
||||
businessservice.NewServerAnalysisService,
|
||||
businessservice.NewWorkbenchService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
Reference in New Issue
Block a user