修改kpi制定规则
This commit is contained in:
parent
e8d2fd37a2
commit
026a354b04
@ -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"`
|
||||||
|
|||||||
@ -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 或拥有全量数据范围的管理员可以看全员绩效。
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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{},
|
||||||
|
|||||||
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