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 } response.OK(c, gin.H{"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 kpiTargetReplaceRequest struct { Items []kpiTargetItemRequest `json:"items"` } 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 { 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, }) } if err := h.store.UpsertDatabiKpiTargets(targets); err != nil { response.BadRequest(c, "保存 KPI 目标失败: "+err.Error()) return } shared.OperationLogWithResourceID(c, h.audit, "replace-databi-kpi-targets", "admin_databi_kpi_targets", "", "success", fmt.Sprintf("items=%d", len(request.Items))) months := map[string]struct{}{} userIDs := map[uint]struct{}{} for _, item := range request.Items { months[strings.TrimSpace(item.PeriodMonth)] = struct{}{} userIDs[item.UserID] = 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 } response.OK(c, gin.H{"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 }