260 lines
9.6 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 dailytask
import (
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
"github.com/gin-gonic/gin"
)
type Handler struct {
activity activityclient.Client
audit shared.OperationLogger
}
func New(activity activityclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{activity: activity, audit: audit}
}
type taskRequest struct {
TaskType string `json:"task_type"`
Category string `json:"category"`
MetricType string `json:"metric_type"`
Title string `json:"title"`
Description string `json:"description"`
AudienceType string `json:"audience_type"`
IconKey string `json:"icon_key"`
IconURL string `json:"icon_url"`
ActionType string `json:"action_type"`
ActionParam string `json:"action_param"`
ActionPayloadJSON string `json:"action_payload_json"`
DimensionFilterJSON string `json:"dimension_filter_json"`
TargetValue int64 `json:"target_value"`
TargetUnit string `json:"target_unit"`
RewardCoinAmount int64 `json:"reward_coin_amount"`
RewardAssetType string `json:"reward_asset_type"`
Status string `json:"status"`
SortOrder int32 `json:"sort_order"`
EffectiveFromMS int64 `json:"effective_from_ms"`
EffectiveToMS int64 `json:"effective_to_ms"`
}
type statusRequest struct {
Status string `json:"status"`
}
type taskDTO struct {
TaskID string `json:"task_id"`
TaskType string `json:"task_type"`
Category string `json:"category"`
MetricType string `json:"metric_type"`
Title string `json:"title"`
Description string `json:"description"`
AudienceType string `json:"audience_type"`
IconKey string `json:"icon_key"`
IconURL string `json:"icon_url"`
ActionType string `json:"action_type"`
ActionParam string `json:"action_param"`
ActionPayloadJSON string `json:"action_payload_json"`
DimensionFilterJSON string `json:"dimension_filter_json"`
TargetValue int64 `json:"target_value"`
TargetUnit string `json:"target_unit"`
RewardCoinAmount int64 `json:"reward_coin_amount"`
RewardAssetType string `json:"reward_asset_type"`
Status string `json:"status"`
SortOrder int32 `json:"sort_order"`
Version int64 `json:"version"`
EffectiveFromMS int64 `json:"effective_from_ms"`
EffectiveToMS int64 `json:"effective_to_ms"`
CreatedByAdminID int64 `json:"created_by_admin_id"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
// ListTaskDefinitions 返回活动管理 > 每日任务页的 daily/exclusive 配置列表。
func (h *Handler) ListTaskDefinitions(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.activity.ListTaskDefinitions(c.Request.Context(), &activityv1.ListTaskDefinitionsRequest{
Meta: h.meta(c),
TaskType: strings.TrimSpace(firstQuery(c, "task_type", "taskType")),
Category: strings.TrimSpace(c.Query("category")),
Status: options.Status,
Keyword: options.Keyword,
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
response.ServerError(c, "获取任务列表失败")
return
}
items := make([]taskDTO, 0, len(resp.GetTasks()))
for _, item := range resp.GetTasks() {
items = append(items, taskFromProto(item))
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
// CreateTaskDefinition 创建任务配置active 任务会立即被 App 查询看到。
func (h *Handler) CreateTaskDefinition(c *gin.Context) {
var req taskRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "任务参数不正确")
return
}
resp, err := h.activity.UpsertTaskDefinition(c.Request.Context(), req.toProto(c, "", h.meta(c)))
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := taskFromProto(resp.GetTask())
h.auditLog(c, "create-daily-task", item.TaskID, item.Title)
response.Created(c, item)
}
// UpdateTaskDefinition 编辑任务配置;关键字段变更由 activity-service 生成新版本。
func (h *Handler) UpdateTaskDefinition(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
response.BadRequest(c, "ID 参数不正确")
return
}
var req taskRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "任务参数不正确")
return
}
resp, err := h.activity.UpsertTaskDefinition(c.Request.Context(), req.toProto(c, taskID, h.meta(c)))
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := taskFromProto(resp.GetTask())
h.auditLog(c, "update-daily-task", item.TaskID, item.Title)
response.OK(c, item)
}
// SetTaskDefinitionStatus 只做 draft/active/paused/archived 状态流转,不物理删除任务。
func (h *Handler) SetTaskDefinitionStatus(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
response.BadRequest(c, "ID 参数不正确")
return
}
var req statusRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "状态参数不正确")
return
}
resp, err := h.activity.SetTaskDefinitionStatus(c.Request.Context(), &activityv1.SetTaskDefinitionStatusRequest{
Meta: h.meta(c),
TaskId: taskID,
Status: strings.TrimSpace(req.Status),
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := taskFromProto(resp.GetTask())
h.auditLog(c, "set-daily-task-status", item.TaskID, item.Status)
response.OK(c, item)
}
func (r taskRequest) toProto(c *gin.Context, taskID string, meta *activityv1.RequestMeta) *activityv1.UpsertTaskDefinitionRequest {
// admin-server 只做字段裁剪和身份注入任务口径校验、JSON 对象校验和版本生成统一放在 activity-service。
// 图标和跳转属于展示字段直接透传给任务定义App 任务页会按 icon_key/icon_url/action_* 渲染。
return &activityv1.UpsertTaskDefinitionRequest{
Meta: meta,
TaskId: taskID,
TaskType: strings.TrimSpace(r.TaskType),
Category: strings.TrimSpace(r.Category),
MetricType: strings.TrimSpace(r.MetricType),
Title: strings.TrimSpace(r.Title),
Description: strings.TrimSpace(r.Description),
AudienceType: strings.TrimSpace(r.AudienceType),
IconKey: strings.TrimSpace(r.IconKey),
IconUrl: strings.TrimSpace(r.IconURL),
ActionType: strings.TrimSpace(r.ActionType),
ActionParam: strings.TrimSpace(r.ActionParam),
ActionPayloadJson: strings.TrimSpace(r.ActionPayloadJSON),
DimensionFilterJson: strings.TrimSpace(r.DimensionFilterJSON),
TargetValue: r.TargetValue,
TargetUnit: strings.TrimSpace(r.TargetUnit),
RewardCoinAmount: r.RewardCoinAmount,
RewardAssetType: strings.TrimSpace(r.RewardAssetType),
Status: strings.TrimSpace(r.Status),
SortOrder: r.SortOrder,
EffectiveFromMs: r.EffectiveFromMS,
EffectiveToMs: r.EffectiveToMS,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
}
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UnixMilli(),
}
}
func (h *Handler) auditLog(c *gin.Context, action string, taskID string, detail string) {
if h.audit == nil {
return
}
shared.OperationLogWithResourceID(c, h.audit, action, "task_definitions", taskID, "success", detail)
}
func taskFromProto(item *activityv1.TaskDefinition) taskDTO {
if item == nil {
return taskDTO{}
}
// 后台列表返回 activity-service 的完整任务定义,保证编辑弹窗能原样带回维度过滤、图标和跳转配置。
return taskDTO{
TaskID: item.GetTaskId(),
TaskType: item.GetTaskType(),
Category: item.GetCategory(),
MetricType: item.GetMetricType(),
Title: item.GetTitle(),
Description: item.GetDescription(),
AudienceType: item.GetAudienceType(),
IconKey: item.GetIconKey(),
IconURL: item.GetIconUrl(),
ActionType: item.GetActionType(),
ActionParam: item.GetActionParam(),
ActionPayloadJSON: item.GetActionPayloadJson(),
DimensionFilterJSON: item.GetDimensionFilterJson(),
TargetValue: item.GetTargetValue(),
TargetUnit: item.GetTargetUnit(),
RewardCoinAmount: item.GetRewardCoinAmount(),
RewardAssetType: item.GetRewardAssetType(),
Status: item.GetStatus(),
SortOrder: item.GetSortOrder(),
Version: item.GetVersion(),
EffectiveFromMS: item.GetEffectiveFromMs(),
EffectiveToMS: item.GetEffectiveToMs(),
CreatedByAdminID: item.GetCreatedByAdminId(),
UpdatedByAdminID: item.GetUpdatedByAdminId(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func firstQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
if value := c.Query(key); value != "" {
return value
}
}
return ""
}