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 "" }