260 lines
9.6 KiB
Go
260 lines
9.6 KiB
Go
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 ""
|
||
}
|