修改kpi制定规则

This commit is contained in:
zhx 2026-07-03 22:42:48 +08:00
parent e8d2fd37a2
commit 026a354b04
6 changed files with 406 additions and 32 deletions

View File

@ -422,6 +422,21 @@ func (DatabiKpiTarget) TableName() string {
return "admin_databi_kpi_targets" return "admin_databi_kpi_targets"
} }
// DatabiAppKpiTarget 是 App 级充值 KPI 月度目标:不分运营的全员共同目标。
type DatabiAppKpiTarget struct {
ID uint `gorm:"primaryKey" json:"id"`
AppCode string `gorm:"size:32;index:uk_admin_databi_app_kpi_target,unique;not null" json:"appCode"`
PeriodMonth string `gorm:"size:7;column:period_month;index:uk_admin_databi_app_kpi_target,unique;not null" json:"periodMonth"`
TargetUSDMinor int64 `gorm:"column:target_usd_minor;not null;default:0" json:"targetUsdMinor"`
DailyTargetUSDMinor int64 `gorm:"column:daily_target_usd_minor;not null;default:0" json:"dailyTargetUsdMinor"`
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli" json:"updatedAtMs"`
}
func (DatabiAppKpiTarget) TableName() string {
return "admin_databi_app_kpi_targets"
}
type TemporaryPaymentLinkOwner struct { type TemporaryPaymentLinkOwner struct {
AppCode string `gorm:"size:32;primaryKey" json:"appCode"` AppCode string `gorm:"size:32;primaryKey" json:"appCode"`
OrderID string `gorm:"size:96;primaryKey" json:"orderId"` OrderID string `gorm:"size:96;primaryKey" json:"orderId"`

View File

@ -110,7 +110,12 @@ func (h *Handler) ListKpiTargets(c *gin.Context) {
response.ServerError(c, "获取 KPI 目标失败") response.ServerError(c, "获取 KPI 目标失败")
return return
} }
response.OK(c, gin.H{"items": targets, "total": len(targets)}) appTargets, err := h.store.ListDatabiAppKpiTargets(months)
if err != nil {
response.ServerError(c, "获取 App KPI 目标失败")
return
}
response.OK(c, gin.H{"appItems": appTargets, "items": targets, "total": len(targets)})
} }
type kpiTargetItemRequest struct { type kpiTargetItemRequest struct {
@ -122,8 +127,17 @@ type kpiTargetItemRequest struct {
DailyTargetUSDMinor int64 `json:"dailyTargetUsdMinor"` DailyTargetUSDMinor int64 `json:"dailyTargetUsdMinor"`
} }
type kpiAppTargetItemRequest struct {
AppCode string `json:"appCode"`
PeriodMonth string `json:"periodMonth"`
TargetUSDMinor int64 `json:"targetUsdMinor"`
DailyTargetUSDMinor int64 `json:"dailyTargetUsdMinor"`
}
type kpiTargetReplaceRequest struct { type kpiTargetReplaceRequest struct {
Items []kpiTargetItemRequest `json:"items"` Items []kpiTargetItemRequest `json:"items"`
// AppItems 是 App 级总目标(不分运营的全员共同目标)。
AppItems []kpiAppTargetItemRequest `json:"appItems"`
} }
func (h *Handler) ReplaceKpiTargets(c *gin.Context) { func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
@ -132,7 +146,7 @@ func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
response.BadRequest(c, "KPI 目标参数不正确") response.BadRequest(c, "KPI 目标参数不正确")
return return
} }
if len(request.Items) == 0 { if len(request.Items) == 0 && len(request.AppItems) == 0 {
response.BadRequest(c, "KPI 目标不能为空") response.BadRequest(c, "KPI 目标不能为空")
return return
} }
@ -147,17 +161,38 @@ func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
DailyTargetUSDMinor: item.DailyTargetUSDMinor, DailyTargetUSDMinor: item.DailyTargetUSDMinor,
}) })
} }
if err := h.store.UpsertDatabiKpiTargets(targets); err != nil { appTargets := make([]model.DatabiAppKpiTarget, 0, len(request.AppItems))
response.BadRequest(c, "保存 KPI 目标失败: "+err.Error()) for _, item := range request.AppItems {
return appTargets = append(appTargets, model.DatabiAppKpiTarget{
AppCode: item.AppCode,
PeriodMonth: item.PeriodMonth,
TargetUSDMinor: item.TargetUSDMinor,
DailyTargetUSDMinor: item.DailyTargetUSDMinor,
})
} }
shared.OperationLogWithResourceID(c, h.audit, "replace-databi-kpi-targets", "admin_databi_kpi_targets", "", "success", fmt.Sprintf("items=%d", len(request.Items))) if len(targets) > 0 {
if err := h.store.UpsertDatabiKpiTargets(targets); err != nil {
response.BadRequest(c, "保存 KPI 目标失败: "+err.Error())
return
}
}
if len(appTargets) > 0 {
if err := h.store.UpsertDatabiAppKpiTargets(appTargets); err != nil {
response.BadRequest(c, "保存 App KPI 目标失败: "+err.Error())
return
}
}
shared.OperationLogWithResourceID(c, h.audit, "replace-databi-kpi-targets", "admin_databi_kpi_targets", "", "success",
fmt.Sprintf("items=%d appItems=%d", len(request.Items), len(request.AppItems)))
months := map[string]struct{}{} months := map[string]struct{}{}
userIDs := map[uint]struct{}{} userIDs := map[uint]struct{}{}
for _, item := range request.Items { for _, item := range request.Items {
months[strings.TrimSpace(item.PeriodMonth)] = struct{}{} months[strings.TrimSpace(item.PeriodMonth)] = struct{}{}
userIDs[item.UserID] = struct{}{} userIDs[item.UserID] = struct{}{}
} }
for _, item := range request.AppItems {
months[strings.TrimSpace(item.PeriodMonth)] = struct{}{}
}
monthList := make([]string, 0, len(months)) monthList := make([]string, 0, len(months))
for month := range months { for month := range months {
monthList = append(monthList, month) monthList = append(monthList, month)
@ -171,7 +206,12 @@ func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
response.ServerError(c, "获取 KPI 目标失败") response.ServerError(c, "获取 KPI 目标失败")
return return
} }
response.OK(c, gin.H{"items": targetsAfter, "total": len(targetsAfter)}) appTargetsAfter, err := h.store.ListDatabiAppKpiTargets(monthList)
if err != nil {
response.ServerError(c, "获取 App KPI 目标失败")
return
}
response.OK(c, gin.H{"appItems": appTargetsAfter, "items": targetsAfter, "total": len(targetsAfter)})
} }
// canViewAllKpi显式授权 databi-kpi:view-all 或拥有全量数据范围的管理员可以看全员绩效。 // canViewAllKpi显式授权 databi-kpi:view-all 或拥有全量数据范围的管理员可以看全员绩效。

View File

@ -403,6 +403,41 @@ type KpiRow struct {
DataError string `json:"data_error,omitempty"` DataError string `json:"data_error,omitempty"`
} }
// KpiOperatorAppRow 是 人×App 级小计:该运营在此 App 下全部负责区域的合计,
// 目标取自 admin_databi_kpi_targets 的 region_id=0 行(该运营的整 App 目标)。
type KpiOperatorAppRow struct {
OperatorUserID uint `json:"operator_user_id"`
OperatorName string `json:"operator_name"`
OperatorAccount string `json:"operator_account"`
Team string `json:"team"`
TeamID *uint `json:"team_id"`
AppCode string `json:"app_code"`
AppName string `json:"app_name"`
RegionCount int `json:"region_count"`
RegionNames string `json:"region_names"`
WholeAppScope bool `json:"whole_app_scope"`
RangeRechargeUSDMinor *int64 `json:"range_recharge_usd_minor"`
RangeNewUsers *int64 `json:"range_new_users"`
MonthRechargeUSDMinor *int64 `json:"month_recharge_usd_minor"`
MonthTargetUSDMinor int64 `json:"month_target_usd_minor"`
DailyTargetUSDMinor int64 `json:"daily_target_usd_minor"`
MonthAttainmentRate *float64 `json:"month_attainment_rate"`
DataError string `json:"data_error,omitempty"`
}
// KpiAppRow 是 App 级总目标行:实际值为整 App 充值(不分运营的全员共同目标)。
type KpiAppRow struct {
AppCode string `json:"app_code"`
AppName string `json:"app_name"`
OperatorCount int `json:"operator_count"`
RangeRechargeUSDMinor *int64 `json:"range_recharge_usd_minor"`
MonthRechargeUSDMinor *int64 `json:"month_recharge_usd_minor"`
MonthTargetUSDMinor int64 `json:"month_target_usd_minor"`
DailyTargetUSDMinor int64 `json:"daily_target_usd_minor"`
MonthAttainmentRate *float64 `json:"month_attainment_rate"`
DataError string `json:"data_error,omitempty"`
}
type KpiSummary struct { type KpiSummary struct {
RangeRechargeUSDMinor int64 `json:"range_recharge_usd_minor"` RangeRechargeUSDMinor int64 `json:"range_recharge_usd_minor"`
MonthRechargeUSDMinor int64 `json:"month_recharge_usd_minor"` MonthRechargeUSDMinor int64 `json:"month_recharge_usd_minor"`
@ -412,12 +447,14 @@ type KpiSummary struct {
} }
type KpiResult struct { type KpiResult struct {
PeriodMonth string `json:"period_month"` PeriodMonth string `json:"period_month"`
MonthStartMS int64 `json:"month_start_ms"` MonthStartMS int64 `json:"month_start_ms"`
MonthEndMS int64 `json:"month_end_ms"` MonthEndMS int64 `json:"month_end_ms"`
Items []KpiRow `json:"items"` Items []KpiRow `json:"items"`
Summary KpiSummary `json:"summary"` OperatorAppRows []KpiOperatorAppRow `json:"operator_app_rows"`
AppErrors map[string]any `json:"app_errors,omitempty"` AppRows []KpiAppRow `json:"app_rows"`
Summary KpiSummary `json:"summary"`
AppErrors map[string]any `json:"app_errors,omitempty"`
} }
// Kpi 按运营人员负责范围App×区域聚合充值/新增,并对齐月度 KPI 目标计算达成率。 // Kpi 按运营人员负责范围App×区域聚合充值/新增,并对齐月度 KPI 目标计算达成率。
@ -487,13 +524,6 @@ func (s *Service) Kpi(ctx context.Context, access repository.MoneyAccess, query
return nil, err return nil, err
} }
type appKpiData struct {
rangeTotal map[string]any
rangeRegion map[int64]map[string]any
monthTotal map[string]any
monthRegion map[int64]map[string]any
err error
}
dataByApp := make(map[string]*appKpiData, len(relevantApps)) dataByApp := make(map[string]*appKpiData, len(relevantApps))
var mu sync.Mutex var mu sync.Mutex
var wg sync.WaitGroup var wg sync.WaitGroup
@ -628,34 +658,248 @@ func (s *Service) Kpi(ctx context.Context, access repository.MoneyAccess, query
return left.RegionID < right.RegionID return left.RegionID < right.RegionID
}) })
appTargets, err := s.store.ListDatabiAppKpiTargets([]string{periodMonth})
if err != nil {
return nil, err
}
appTargetByCode := map[string]model.DatabiAppKpiTarget{}
for _, target := range appTargets {
appTargetByCode[appctx.Normalize(target.AppCode)] = target
}
operatorAppRows := buildOperatorAppRows(rows, targetByKey)
appRows := buildAppRows(relevantApps, dataByApp, appTargetByCode, rows)
summary := buildKpiSummary(rows, operatorAppRows, appRows)
result := &KpiResult{
PeriodMonth: periodMonth,
MonthStartMS: monthStart.UnixMilli(),
MonthEndMS: monthEnd.UnixMilli(),
Items: rows,
OperatorAppRows: operatorAppRows,
AppRows: appRows,
Summary: summary,
}
if len(appErrors) > 0 {
result.AppErrors = appErrors
}
return result, nil
}
type appKpiData struct {
rangeTotal map[string]any
rangeRegion map[int64]map[string]any
monthTotal map[string]any
monthRegion map[int64]map[string]any
err error
}
func nullableAdd(sum *int64, value *int64) *int64 {
if value == nil {
return sum
}
if sum == nil {
out := *value
return &out
}
out := *sum + *value
return &out
}
// buildOperatorAppRows 把 人×App×区域 明细行归并成 人×App 小计:
// 该运营有整 App 范围region 0时直接取那一行其值即整 App 口径),否则各区域可空求和。
func buildOperatorAppRows(rows []KpiRow, targetByKey map[string]model.DatabiKpiTarget) []KpiOperatorAppRow {
type group struct {
row KpiOperatorAppRow
regionNames []string
wholeRow *KpiRow
}
groups := map[string]*group{}
order := []string{}
for index := range rows {
row := rows[index]
key := strconv.FormatUint(uint64(row.OperatorUserID), 10) + "|" + row.AppCode
item := groups[key]
if item == nil {
item = &group{row: KpiOperatorAppRow{
AppCode: row.AppCode,
AppName: row.AppName,
OperatorAccount: row.OperatorAccount,
OperatorName: row.OperatorName,
OperatorUserID: row.OperatorUserID,
Team: row.Team,
TeamID: row.TeamID,
}}
groups[key] = item
order = append(order, key)
}
item.row.RegionCount++
item.regionNames = append(item.regionNames, row.RegionName)
if row.DataError != "" && item.row.DataError == "" {
item.row.DataError = row.DataError
}
if row.RegionID == 0 {
item.row.WholeAppScope = true
item.wholeRow = &rows[index]
continue
}
item.row.RangeRechargeUSDMinor = nullableAdd(item.row.RangeRechargeUSDMinor, row.RangeRechargeUSDMinor)
item.row.RangeNewUsers = nullableAdd(item.row.RangeNewUsers, row.RangeNewUsers)
item.row.MonthRechargeUSDMinor = nullableAdd(item.row.MonthRechargeUSDMinor, row.MonthRechargeUSDMinor)
}
out := make([]KpiOperatorAppRow, 0, len(order))
for _, key := range order {
item := groups[key]
if item.wholeRow != nil {
// 整 App 范围行的取数已是整 App 口径,区域行是它的子集,避免相加重复计数。
item.row.RangeRechargeUSDMinor = item.wholeRow.RangeRechargeUSDMinor
item.row.RangeNewUsers = item.wholeRow.RangeNewUsers
item.row.MonthRechargeUSDMinor = item.wholeRow.MonthRechargeUSDMinor
}
item.row.RegionNames = joinRegionNames(item.regionNames)
if target, ok := targetByKey[kpiTargetKey(item.row.OperatorUserID, item.row.AppCode, 0)]; ok {
item.row.MonthTargetUSDMinor = target.TargetUSDMinor
item.row.DailyTargetUSDMinor = target.DailyTargetUSDMinor
} else if item.wholeRow != nil {
// 整 App 范围的运营其明细行region 0目标与 人×App 目标同键,直接沿用。
item.row.MonthTargetUSDMinor = item.wholeRow.MonthTargetUSDMinor
item.row.DailyTargetUSDMinor = item.wholeRow.DailyTargetUSDMinor
}
if item.row.MonthTargetUSDMinor > 0 && item.row.MonthRechargeUSDMinor != nil {
rate := safeRatio(*item.row.MonthRechargeUSDMinor, item.row.MonthTargetUSDMinor)
item.row.MonthAttainmentRate = &rate
}
out = append(out, item.row)
}
sort.Slice(out, func(i, j int) bool {
left := int64(0)
if out[i].MonthRechargeUSDMinor != nil {
left = *out[i].MonthRechargeUSDMinor
}
right := int64(0)
if out[j].MonthRechargeUSDMinor != nil {
right = *out[j].MonthRechargeUSDMinor
}
if left != right {
return left > right
}
if out[i].OperatorUserID != out[j].OperatorUserID {
return out[i].OperatorUserID < out[j].OperatorUserID
}
return out[i].AppCode < out[j].AppCode
})
return out
}
func joinRegionNames(names []string) string {
if len(names) <= 3 {
return strings.Join(names, " / ")
}
return strings.Join(names[:3], " / ") + fmt.Sprintf(" 等%d个", len(names))
}
// buildAppRows 生成 App 级总目标行:实际值 = 整 App 充值(与运营/区域无关的共同目标口径)。
func buildAppRows(apps []AppInfo, dataByApp map[string]*appKpiData, appTargetByCode map[string]model.DatabiAppKpiTarget, rows []KpiRow) []KpiAppRow {
operatorsByApp := map[string]map[uint]struct{}{}
for _, row := range rows {
set := operatorsByApp[row.AppCode]
if set == nil {
set = map[uint]struct{}{}
operatorsByApp[row.AppCode] = set
}
set[row.OperatorUserID] = struct{}{}
}
out := make([]KpiAppRow, 0, len(apps))
for _, app := range apps {
row := KpiAppRow{
AppCode: app.AppCode,
AppName: app.AppName,
OperatorCount: len(operatorsByApp[app.AppCode]),
}
if target, ok := appTargetByCode[app.AppCode]; ok {
row.MonthTargetUSDMinor = target.TargetUSDMinor
row.DailyTargetUSDMinor = target.DailyTargetUSDMinor
}
data := dataByApp[app.AppCode]
if data == nil {
row.DataError = "该 App 未接入统计数据"
} else if data.err != nil {
row.DataError = fmt.Sprintf("获取统计数据失败: %v", data.err)
} else {
row.RangeRechargeUSDMinor = metricPointer(data.rangeTotal, "recharge_usd_minor")
row.MonthRechargeUSDMinor = metricPointer(data.monthTotal, "recharge_usd_minor")
}
if row.MonthTargetUSDMinor > 0 && row.MonthRechargeUSDMinor != nil {
rate := safeRatio(*row.MonthRechargeUSDMinor, row.MonthTargetUSDMinor)
row.MonthAttainmentRate = &rate
}
out = append(out, row)
}
sort.Slice(out, func(i, j int) bool {
left := int64(0)
if out[i].MonthRechargeUSDMinor != nil {
left = *out[i].MonthRechargeUSDMinor
}
right := int64(0)
if out[j].MonthRechargeUSDMinor != nil {
right = *out[j].MonthRechargeUSDMinor
}
if left != right {
return left > right
}
return out[i].AppCode < out[j].AppCode
})
return out
}
// buildKpiSummary 的口径:实际充值按 App 行求和(跨运营/区域去重);
// 月目标按 App 逐个取"最权威"的一层——App 总目标 > 人×App 目标合计 > 人×区域明细目标合计。
func buildKpiSummary(rows []KpiRow, operatorAppRows []KpiOperatorAppRow, appRows []KpiAppRow) KpiSummary {
summary := KpiSummary{} summary := KpiSummary{}
operatorSeen := map[uint]struct{}{} operatorSeen := map[uint]struct{}{}
regionTargetByApp := map[string]int64{}
for _, row := range rows { for _, row := range rows {
operatorSeen[row.OperatorUserID] = struct{}{}
if row.RegionID != 0 {
regionTargetByApp[row.AppCode] += row.MonthTargetUSDMinor
}
}
summary.OperatorCount = len(operatorSeen)
operatorAppTargetByApp := map[string]int64{}
for _, row := range operatorAppRows {
operatorAppTargetByApp[row.AppCode] += row.MonthTargetUSDMinor
}
for _, row := range appRows {
if row.RangeRechargeUSDMinor != nil { if row.RangeRechargeUSDMinor != nil {
summary.RangeRechargeUSDMinor += *row.RangeRechargeUSDMinor summary.RangeRechargeUSDMinor += *row.RangeRechargeUSDMinor
} }
if row.MonthRechargeUSDMinor != nil { if row.MonthRechargeUSDMinor != nil {
summary.MonthRechargeUSDMinor += *row.MonthRechargeUSDMinor summary.MonthRechargeUSDMinor += *row.MonthRechargeUSDMinor
} }
summary.MonthTargetUSDMinor += row.MonthTargetUSDMinor summary.MonthTargetUSDMinor += pickEffectiveAppTarget(
operatorSeen[row.OperatorUserID] = struct{}{} row.MonthTargetUSDMinor,
operatorAppTargetByApp[row.AppCode],
regionTargetByApp[row.AppCode],
)
} }
summary.OperatorCount = len(operatorSeen)
if summary.MonthTargetUSDMinor > 0 { if summary.MonthTargetUSDMinor > 0 {
rate := safeRatio(summary.MonthRechargeUSDMinor, summary.MonthTargetUSDMinor) rate := safeRatio(summary.MonthRechargeUSDMinor, summary.MonthTargetUSDMinor)
summary.MonthAttainmentRate = &rate summary.MonthAttainmentRate = &rate
} }
result := &KpiResult{ return summary
PeriodMonth: periodMonth, }
MonthStartMS: monthStart.UnixMilli(),
MonthEndMS: monthEnd.UnixMilli(), // pickEffectiveAppTarget一个 App 的"生效月目标"取最高层已配置的那级,避免多级目标重复相加。
Items: rows, func pickEffectiveAppTarget(appTarget int64, operatorAppTargets int64, regionTargets int64) int64 {
Summary: summary, if appTarget > 0 {
return appTarget
} }
if len(appErrors) > 0 { if operatorAppTargets > 0 {
result.AppErrors = appErrors return operatorAppTargets
} }
return result, nil return regionTargets
} }
func (s *Service) kpiWindow(ctx context.Context, app AppInfo, resolve func(map[string]any) (RegionInfo, bool), statTZ string, startMS int64, endMS int64) (map[string]any, map[int64]map[string]any, error) { func (s *Service) kpiWindow(ctx context.Context, app AppInfo, resolve func(map[string]any) (RegionInfo, bool), statTZ string, startMS int64, endMS int64) (map[string]any, map[int64]map[string]any, error) {

View File

@ -83,6 +83,67 @@ func (s *Store) UpsertDatabiKpiTargets(targets []model.DatabiKpiTarget) error {
}) })
} }
func (s *Store) ListDatabiAppKpiTargets(periodMonths []string) ([]model.DatabiAppKpiTarget, error) {
query := s.db.Model(&model.DatabiAppKpiTarget{})
months := normalizeDatabiPeriodMonths(periodMonths)
if len(months) > 0 {
query = query.Where("period_month IN ?", months)
}
var targets []model.DatabiAppKpiTarget
err := query.Order("app_code ASC, period_month ASC").Find(&targets).Error
return targets, err
}
// UpsertDatabiAppKpiTargets 逐条 upsert App 级目标;月目标和日目标同时为 0 视为清除。
func (s *Store) UpsertDatabiAppKpiTargets(targets []model.DatabiAppKpiTarget) error {
seen := map[string]struct{}{}
upserts := make([]model.DatabiAppKpiTarget, 0, len(targets))
removals := make([]model.DatabiAppKpiTarget, 0)
for _, target := range targets {
target.AppCode = appctx.Normalize(target.AppCode)
target.PeriodMonth = strings.TrimSpace(target.PeriodMonth)
if target.AppCode == "" {
return errors.New("app kpi target app code is required")
}
if !databiPeriodMonthPattern.MatchString(target.PeriodMonth) {
return errors.New("app kpi target period month must be formatted as YYYY-MM")
}
if target.TargetUSDMinor < 0 || target.DailyTargetUSDMinor < 0 {
return errors.New("app kpi target amounts must not be negative")
}
key := target.AppCode + ":" + target.PeriodMonth
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if target.TargetUSDMinor == 0 && target.DailyTargetUSDMinor == 0 {
removals = append(removals, target)
continue
}
target.ID = 0
upserts = append(upserts, target)
}
return s.db.Transaction(func(tx *gorm.DB) error {
for _, target := range removals {
if err := tx.Where("app_code = ? AND period_month = ?", target.AppCode, target.PeriodMonth).
Delete(&model.DatabiAppKpiTarget{}).Error; err != nil {
return err
}
}
if len(upserts) == 0 {
return nil
}
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "app_code"}, {Name: "period_month"}},
DoUpdates: clause.AssignmentColumns([]string{
"target_usd_minor",
"daily_target_usd_minor",
"updated_at_ms",
}),
}).Create(&upserts).Error
})
}
func normalizeDatabiKpiTargets(targets []model.DatabiKpiTarget) ([]model.DatabiKpiTarget, []model.DatabiKpiTarget, error) { func normalizeDatabiKpiTargets(targets []model.DatabiKpiTarget) ([]model.DatabiKpiTarget, []model.DatabiKpiTarget, error) {
seen := map[string]struct{}{} seen := map[string]struct{}{}
upserts := make([]model.DatabiKpiTarget, 0, len(targets)) upserts := make([]model.DatabiKpiTarget, 0, len(targets))

View File

@ -81,6 +81,7 @@ func (s *Store) AutoMigrate() error {
&model.DataScope{}, &model.DataScope{},
&model.UserMoneyScope{}, &model.UserMoneyScope{},
&model.DatabiKpiTarget{}, &model.DatabiKpiTarget{},
&model.DatabiAppKpiTarget{},
&model.TemporaryPaymentLinkOwner{}, &model.TemporaryPaymentLinkOwner{},
&model.LegacyGooglePaidDetail{}, &model.LegacyGooglePaidDetail{},
&model.FinanceApplication{}, &model.FinanceApplication{},

View File

@ -0,0 +1,13 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- App 级充值 KPI 月度目标:不分运营的全员共同目标;人级目标仍在 admin_databi_kpi_targetsregion_id=0 表示该运营的整 App 目标)。
CREATE TABLE IF NOT EXISTS admin_databi_app_kpi_targets (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
period_month CHAR(7) NOT NULL COMMENT '目标月份,格式 YYYY-MM',
target_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '当月充值目标USD 分',
daily_target_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '当日充值目标USD 分',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
UNIQUE KEY uk_admin_databi_app_kpi_target (app_code, period_month)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='社交 BI App 级充值 KPI 月度目标(不分运营)';