All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
- 新增成对迁移 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
173 lines
7.4 KiB
Go
173 lines
7.4 KiB
Go
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
|
||
}
|