258 lines
7.8 KiB
Go
258 lines
7.8 KiB
Go
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
|
||
}
|