修改kpi制定规则
This commit is contained in:
parent
e8d2fd37a2
commit
026a354b04
@ -422,6 +422,21 @@ func (DatabiKpiTarget) TableName() string {
|
||||
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 {
|
||||
AppCode string `gorm:"size:32;primaryKey" json:"appCode"`
|
||||
OrderID string `gorm:"size:96;primaryKey" json:"orderId"`
|
||||
|
||||
@ -110,7 +110,12 @@ func (h *Handler) ListKpiTargets(c *gin.Context) {
|
||||
response.ServerError(c, "获取 KPI 目标失败")
|
||||
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 {
|
||||
@ -122,8 +127,17 @@ type kpiTargetItemRequest struct {
|
||||
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 {
|
||||
Items []kpiTargetItemRequest `json:"items"`
|
||||
// AppItems 是 App 级总目标(不分运营的全员共同目标)。
|
||||
AppItems []kpiAppTargetItemRequest `json:"appItems"`
|
||||
}
|
||||
|
||||
func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
|
||||
@ -132,7 +146,7 @@ func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
|
||||
response.BadRequest(c, "KPI 目标参数不正确")
|
||||
return
|
||||
}
|
||||
if len(request.Items) == 0 {
|
||||
if len(request.Items) == 0 && len(request.AppItems) == 0 {
|
||||
response.BadRequest(c, "KPI 目标不能为空")
|
||||
return
|
||||
}
|
||||
@ -147,17 +161,38 @@ func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
|
||||
DailyTargetUSDMinor: item.DailyTargetUSDMinor,
|
||||
})
|
||||
}
|
||||
if err := h.store.UpsertDatabiKpiTargets(targets); err != nil {
|
||||
response.BadRequest(c, "保存 KPI 目标失败: "+err.Error())
|
||||
return
|
||||
appTargets := make([]model.DatabiAppKpiTarget, 0, len(request.AppItems))
|
||||
for _, item := range request.AppItems {
|
||||
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{}{}
|
||||
userIDs := map[uint]struct{}{}
|
||||
for _, item := range request.Items {
|
||||
months[strings.TrimSpace(item.PeriodMonth)] = struct{}{}
|
||||
userIDs[item.UserID] = struct{}{}
|
||||
}
|
||||
for _, item := range request.AppItems {
|
||||
months[strings.TrimSpace(item.PeriodMonth)] = struct{}{}
|
||||
}
|
||||
monthList := make([]string, 0, len(months))
|
||||
for month := range months {
|
||||
monthList = append(monthList, month)
|
||||
@ -171,7 +206,12 @@ func (h *Handler) ReplaceKpiTargets(c *gin.Context) {
|
||||
response.ServerError(c, "获取 KPI 目标失败")
|
||||
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 或拥有全量数据范围的管理员可以看全员绩效。
|
||||
|
||||
@ -403,6 +403,41 @@ type KpiRow struct {
|
||||
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 {
|
||||
RangeRechargeUSDMinor int64 `json:"range_recharge_usd_minor"`
|
||||
MonthRechargeUSDMinor int64 `json:"month_recharge_usd_minor"`
|
||||
@ -412,12 +447,14 @@ type KpiSummary struct {
|
||||
}
|
||||
|
||||
type KpiResult struct {
|
||||
PeriodMonth string `json:"period_month"`
|
||||
MonthStartMS int64 `json:"month_start_ms"`
|
||||
MonthEndMS int64 `json:"month_end_ms"`
|
||||
Items []KpiRow `json:"items"`
|
||||
Summary KpiSummary `json:"summary"`
|
||||
AppErrors map[string]any `json:"app_errors,omitempty"`
|
||||
PeriodMonth string `json:"period_month"`
|
||||
MonthStartMS int64 `json:"month_start_ms"`
|
||||
MonthEndMS int64 `json:"month_end_ms"`
|
||||
Items []KpiRow `json:"items"`
|
||||
OperatorAppRows []KpiOperatorAppRow `json:"operator_app_rows"`
|
||||
AppRows []KpiAppRow `json:"app_rows"`
|
||||
Summary KpiSummary `json:"summary"`
|
||||
AppErrors map[string]any `json:"app_errors,omitempty"`
|
||||
}
|
||||
|
||||
// Kpi 按运营人员负责范围(App×区域)聚合充值/新增,并对齐月度 KPI 目标计算达成率。
|
||||
@ -487,13 +524,6 @@ func (s *Service) Kpi(ctx context.Context, access repository.MoneyAccess, query
|
||||
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))
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
@ -628,34 +658,248 @@ func (s *Service) Kpi(ctx context.Context, access repository.MoneyAccess, query
|
||||
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{}
|
||||
operatorSeen := map[uint]struct{}{}
|
||||
regionTargetByApp := map[string]int64{}
|
||||
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 {
|
||||
summary.RangeRechargeUSDMinor += *row.RangeRechargeUSDMinor
|
||||
}
|
||||
if row.MonthRechargeUSDMinor != nil {
|
||||
summary.MonthRechargeUSDMinor += *row.MonthRechargeUSDMinor
|
||||
}
|
||||
summary.MonthTargetUSDMinor += row.MonthTargetUSDMinor
|
||||
operatorSeen[row.OperatorUserID] = struct{}{}
|
||||
summary.MonthTargetUSDMinor += pickEffectiveAppTarget(
|
||||
row.MonthTargetUSDMinor,
|
||||
operatorAppTargetByApp[row.AppCode],
|
||||
regionTargetByApp[row.AppCode],
|
||||
)
|
||||
}
|
||||
summary.OperatorCount = len(operatorSeen)
|
||||
if summary.MonthTargetUSDMinor > 0 {
|
||||
rate := safeRatio(summary.MonthRechargeUSDMinor, summary.MonthTargetUSDMinor)
|
||||
summary.MonthAttainmentRate = &rate
|
||||
}
|
||||
result := &KpiResult{
|
||||
PeriodMonth: periodMonth,
|
||||
MonthStartMS: monthStart.UnixMilli(),
|
||||
MonthEndMS: monthEnd.UnixMilli(),
|
||||
Items: rows,
|
||||
Summary: summary,
|
||||
return summary
|
||||
}
|
||||
|
||||
// pickEffectiveAppTarget:一个 App 的"生效月目标"取最高层已配置的那级,避免多级目标重复相加。
|
||||
func pickEffectiveAppTarget(appTarget int64, operatorAppTargets int64, regionTargets int64) int64 {
|
||||
if appTarget > 0 {
|
||||
return appTarget
|
||||
}
|
||||
if len(appErrors) > 0 {
|
||||
result.AppErrors = appErrors
|
||||
if operatorAppTargets > 0 {
|
||||
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) {
|
||||
|
||||
@ -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) {
|
||||
seen := map[string]struct{}{}
|
||||
upserts := make([]model.DatabiKpiTarget, 0, len(targets))
|
||||
|
||||
@ -81,6 +81,7 @@ func (s *Store) AutoMigrate() error {
|
||||
&model.DataScope{},
|
||||
&model.UserMoneyScope{},
|
||||
&model.DatabiKpiTarget{},
|
||||
&model.DatabiAppKpiTarget{},
|
||||
&model.TemporaryPaymentLinkOwner{},
|
||||
&model.LegacyGooglePaidDetail{},
|
||||
&model.FinanceApplication{},
|
||||
|
||||
13
server/admin/migrations/076_databi_app_kpi_targets.sql
Normal file
13
server/admin/migrations/076_databi_app_kpi_targets.sql
Normal file
@ -0,0 +1,13 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- App 级充值 KPI 月度目标:不分运营的全员共同目标;人级目标仍在 admin_databi_kpi_targets(region_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 月度目标(不分运营)';
|
||||
Loading…
x
Reference in New Issue
Block a user