Files
junhong_cmp_fiber/internal/handler/admin/operations_report.go
break 5e78809b93
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
feat(运营报表): AUG26-015 设备激活与套餐续费日报快照、查询趋势与受控导出
- 新增成对迁移 000231 与三张快照表:日级头行、设备粒度激活行、到期事件粒度续费行,以快照日期为唯一键
- 新增每日 03:30(Asia/Shanghai)日报快照生成任务与幂等整日替换,失败重试沿用同一目标日
- 新增六条受控入口:两张报表的汇总、日/月趋势与受控导出,配套查询层只读快照事实
- 新增 operations_activation 与 operations_renewal 两个导出场景,创建期冻结筛选与可见店铺范围、派发期冻结表头、执行期只按冻结值复核资格
- 采购数量口径按系统内未删除设备数实施并在 PRD 标注,附实测差额依据
- 同步证据链 requirement-evidence.json 与入口能力矩阵、ARCHITECTURE 与验证记录
- 归档 change add-operations-reports 并新建主 Spec openspec/specs/operations-report/spec.md
2026-09-18 09:42:28 +08:00

173 lines
7.4 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 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
}