package admin import ( "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" domainreport "github.com/break/junhong_cmp_fiber/internal/domain/operationsreport" "github.com/break/junhong_cmp_fiber/internal/handler/validation" "github.com/break/junhong_cmp_fiber/internal/model/dto" operationsreportquery "github.com/break/junhong_cmp_fiber/internal/query/operationsreport" exportTaskService "github.com/break/junhong_cmp_fiber/internal/service/export_task" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/response" "github.com/break/junhong_cmp_fiber/pkg/utils" ) // OperationsReportHandler 运营报表 Handler。 // 查询与导出只对超级管理员与平台账号开放(路由组已有角色门禁),Handler 不做任何跳过业务校验的分支。 type OperationsReportHandler struct { query *operationsreportquery.Query exportService *exportTaskService.Service validator *validator.Validate } // NewOperationsReportHandler 创建运营报表 Handler。 func NewOperationsReportHandler(query *operationsreportquery.Query, exportService *exportTaskService.Service, validator *validator.Validate) *OperationsReportHandler { return &OperationsReportHandler{query: query, exportService: exportService, validator: validator} } // ActivationSummary 查询设备激活情况汇总。 // GET /api/admin/operations-reports/activation-summary func (h *OperationsReportHandler) ActivationSummary(c *fiber.Ctx) error { var req dto.OperationsActivationSummaryRequest if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数格式不正确") } if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, validation.Message("查询设备激活情况汇总参数不合法", &req, err)) } result, err := h.query.ActivationSummary(c.UserContext(), req) if err != nil { return err } return response.Success(c, result) } // ActivationTrend 查询设备激活情况日/月趋势。 // GET /api/admin/operations-reports/activation-trend func (h *OperationsReportHandler) ActivationTrend(c *fiber.Ctx) error { var req dto.OperationsActivationTrendRequest if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数格式不正确") } if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, validation.Message("查询设备激活情况趋势参数不合法", &req, err)) } result, err := h.query.ActivationTrend(c.UserContext(), req) if err != nil { return err } return response.Success(c, result) } // RenewalSummary 查询套餐续费情况汇总。 // GET /api/admin/operations-reports/package-renewal-summary func (h *OperationsReportHandler) RenewalSummary(c *fiber.Ctx) error { var req dto.OperationsRenewalSummaryRequest if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数格式不正确") } if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, validation.Message("查询套餐续费情况汇总参数不合法", &req, err)) } result, err := h.query.RenewalSummary(c.UserContext(), req) if err != nil { return err } return response.Success(c, result) } // RenewalTrend 查询套餐续费情况日/月趋势。 // GET /api/admin/operations-reports/package-renewal-trend func (h *OperationsReportHandler) RenewalTrend(c *fiber.Ctx) error { var req dto.OperationsRenewalTrendRequest if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数格式不正确") } if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, validation.Message("查询套餐续费情况趋势参数不合法", &req, err)) } result, err := h.query.RenewalTrend(c.UserContext(), req) if err != nil { return err } return response.Success(c, result) } // ExportActivationSummary 创建设备激活情况报表异步导出任务。 // POST /api/admin/operations-reports/activation-summary/export // 受控入口:创建时冻结筛选条件、操作者与可见店铺范围,非法时间在创建期拒绝。 func (h *OperationsReportHandler) ExportActivationSummary(c *fiber.Ctx) error { request, err := h.parseExportRequest(c, constants.ExportTaskSceneOperationsActivation, domainreport.IsActivationDimension, "导出设备激活情况参数不合法") if err != nil { return err } return h.createExportTask(c, constants.ExportTaskSceneOperationsActivation, request) } // ExportRenewalSummary 创建套餐续费情况报表异步导出任务。 // POST /api/admin/operations-reports/package-renewal-summary/export // 受控入口:创建时冻结筛选条件、操作者与可见店铺范围,非法时间在创建期拒绝。 func (h *OperationsReportHandler) ExportRenewalSummary(c *fiber.Ctx) error { request, err := h.parseExportRequest(c, constants.ExportTaskSceneOperationsRenewal, domainreport.IsRenewalDimension, "导出套餐续费情况参数不合法") if err != nil { return err } return h.createExportTask(c, constants.ExportTaskSceneOperationsRenewal, request) } // createExportTask 复用既有导出任务创建路径:创建期冻结操作者、可见店铺范围与筛选快照。 func (h *OperationsReportHandler) createExportTask(c *fiber.Ctx, scene string, request dto.ExportOperationsReportRequest) error { createRequest := dto.CreateExportTaskRequest{ Scene: scene, Format: request.Format, Query: map[string]interface{}{"filters": exportOperationsReportFilters(request)}, } result, err := h.exportService.CreateTask(c.UserContext(), &createRequest) if err != nil { return err } return response.Success(c, result) } // parseExportRequest 解析并校验受控导出请求。 // 时间边界在创建期用统一严格解析器校验(格式非法或开始晚于结束一律拒绝), // 分组维度必须属于该报表支持的维度集合。 func (h *OperationsReportHandler) parseExportRequest(c *fiber.Ctx, scene string, isDimension func(string) bool, message string) (dto.ExportOperationsReportRequest, error) { var request dto.ExportOperationsReportRequest if err := c.BodyParser(&request); err != nil { return request, errors.New(errors.CodeInvalidParam, "请求参数格式不正确") } if err := h.validator.Struct(&request); err != nil { return request, errors.New(errors.CodeInvalidParam, validation.Message(message, &request, err)) } if _, _, err := utils.ParseTimeRange(request.StartTime, request.EndTime); err != nil { return request, err } if request.GroupBy != "" && !isDimension(request.GroupBy) { return request, errors.New(errors.CodeInvalidParam, "不支持的分组维度 "+request.GroupBy) } return request, nil } // exportOperationsReportFilters 把导出请求转换为导出任务的筛选快照。 // 时间边界按统一契约冻结为纯字符串值,创建期由导出任务服务规范化为 UTC RFC3339 秒级; // 分组维度恒定冻结(未选择时为空串),执行期不再重新解释请求。 func exportOperationsReportFilters(request dto.ExportOperationsReportRequest) map[string]interface{} { filters := make(map[string]interface{}, 3) if request.StartTime != "" { filters["start_time"] = request.StartTime } if request.EndTime != "" { filters["end_time"] = request.EndTime } filters["group_by"] = request.GroupBy return filters }