2026-07-03 22:42:48 +08:00

258 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package databi
import (
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/repository"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
)
const (
kpiManagePermission = "databi-kpi:manage"
kpiViewAllPermission = "databi-kpi:view-all"
)
type Handler struct {
service *Service
store *repository.Store
audit shared.OperationLogger
}
func New(service *Service, store *repository.Store, audit shared.OperationLogger) *Handler {
return &Handler{service: service, store: store, audit: audit}
}
func (h *Handler) Master(c *gin.Context) {
access, ok := h.moneyAccess(c)
if !ok {
return
}
master, err := h.service.Master(c.Request.Context(), access, shared.ActorFromContext(c).UserID, h.canViewAllKpi(c, access))
if err != nil {
response.ServerError(c, "获取 BI 主数据失败")
return
}
master.Permissions = map[string]bool{
"kpi_manage": middleware.HasPermission(c, kpiManagePermission),
"kpi_view_all": h.canViewAllKpi(c, access),
}
response.OK(c, master)
}
func (h *Handler) Overview(c *gin.Context) {
access, ok := h.moneyAccess(c)
if !ok {
return
}
result, err := h.service.Overview(c.Request.Context(), access, OverviewQuery{
StatTZ: c.Query("stat_tz"),
StartMS: queryInt64(c, "start_ms"),
EndMS: queryInt64(c, "end_ms"),
AppCodes: splitCSV(c.Query("app_codes")),
RegionID: queryInt64(c, "region_id"),
})
if err != nil {
response.ServerError(c, "获取 BI 统计数据失败")
return
}
response.OK(c, result)
}
func (h *Handler) Kpi(c *gin.Context) {
access, ok := h.moneyAccess(c)
if !ok {
return
}
result, err := h.service.Kpi(c.Request.Context(), access, KpiQuery{
StatTZ: c.Query("stat_tz"),
StartMS: queryInt64(c, "start_ms"),
EndMS: queryInt64(c, "end_ms"),
PeriodMonth: c.Query("period_month"),
AppCodes: splitCSV(c.Query("app_codes")),
OperatorUserID: queryUint(c, "operator_user_id"),
ActorUserID: shared.ActorFromContext(c).UserID,
ViewAll: h.canViewAllKpi(c, access),
})
if err != nil {
if strings.Contains(err.Error(), "period_month") {
response.BadRequest(c, "KPI 月份格式不正确")
return
}
response.ServerError(c, "获取 KPI 数据失败")
return
}
response.OK(c, result)
}
func (h *Handler) ListKpiTargets(c *gin.Context) {
access, ok := h.moneyAccess(c)
if !ok {
return
}
userIDs := []uint{}
if userID := queryUint(c, "user_id"); userID > 0 {
userIDs = append(userIDs, userID)
}
if !middleware.HasPermission(c, kpiManagePermission) && !h.canViewAllKpi(c, access) {
// 没有管理/全员查看权限时只能查询自己的目标,保证"我的 KPI"可用又不泄露他人目标。
userIDs = []uint{shared.ActorFromContext(c).UserID}
}
months := splitCSV(c.Query("period_month"))
targets, err := h.store.ListDatabiKpiTargets(months, userIDs)
if err != nil {
response.ServerError(c, "获取 KPI 目标失败")
return
}
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 {
UserID uint `json:"userId"`
AppCode string `json:"appCode"`
RegionID int64 `json:"regionId"`
PeriodMonth string `json:"periodMonth"`
TargetUSDMinor int64 `json:"targetUsdMinor"`
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) {
var request kpiTargetReplaceRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "KPI 目标参数不正确")
return
}
if len(request.Items) == 0 && len(request.AppItems) == 0 {
response.BadRequest(c, "KPI 目标不能为空")
return
}
targets := make([]model.DatabiKpiTarget, 0, len(request.Items))
for _, item := range request.Items {
targets = append(targets, model.DatabiKpiTarget{
UserID: item.UserID,
AppCode: item.AppCode,
RegionID: item.RegionID,
PeriodMonth: item.PeriodMonth,
TargetUSDMinor: item.TargetUSDMinor,
DailyTargetUSDMinor: item.DailyTargetUSDMinor,
})
}
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,
})
}
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)
}
userList := make([]uint, 0, len(userIDs))
for userID := range userIDs {
userList = append(userList, userID)
}
targetsAfter, err := h.store.ListDatabiKpiTargets(monthList, userList)
if err != nil {
response.ServerError(c, "获取 KPI 目标失败")
return
}
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 或拥有全量数据范围的管理员可以看全员绩效。
func (h *Handler) canViewAllKpi(c *gin.Context, access repository.MoneyAccess) bool {
if middleware.HasPermission(c, kpiViewAllPermission) || middleware.HasPermission(c, kpiManagePermission) {
return true
}
return access.All
}
func (h *Handler) moneyAccess(c *gin.Context) (repository.MoneyAccess, bool) {
if h.store == nil {
return repository.MoneyAccess{All: true}, true
}
access, err := h.store.MoneyAccessForUser(shared.ActorFromContext(c).UserID)
if err != nil {
response.ServerError(c, "获取数据范围失败")
return repository.MoneyAccess{}, false
}
return access, true
}
func queryInt64(c *gin.Context, key string) int64 {
parsed, _ := strconv.ParseInt(strings.TrimSpace(c.Query(key)), 10, 64)
return parsed
}
func queryUint(c *gin.Context, key string) uint {
parsed, _ := strconv.ParseUint(strings.TrimSpace(c.Query(key)), 10, 64)
return uint(parsed)
}
func splitCSV(value string) []string {
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}