package activityapi import ( "encoding/json" "net/http" "strings" activityv1 "hyapp.local/api/proto/activity/v1" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) type taskSectionData struct { Section string `json:"section"` Items []taskItemData `json:"items"` ServerTimeMS int64 `json:"server_time_ms"` NextRefreshAtMS int64 `json:"next_refresh_at_ms"` } type taskItemData 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"` ActionPayload map[string]any `json:"action_payload"` DimensionFilterJSON string `json:"dimension_filter_json"` TargetValue int64 `json:"target_value"` TargetUnit string `json:"target_unit"` ProgressValue int64 `json:"progress_value"` RewardCoinAmount int64 `json:"reward_coin_amount"` RewardAssetType string `json:"reward_asset_type"` Status string `json:"status"` Claimable bool `json:"claimable"` TaskDay string `json:"task_day"` ServerTimeMS int64 `json:"server_time_ms"` NextRefreshAtMS int64 `json:"next_refresh_at_ms"` SortOrder int32 `json:"sort_order"` Version int64 `json:"version"` } type taskClaimData struct { ClaimID string `json:"claim_id"` TaskID string `json:"task_id"` TaskType string `json:"task_type"` TaskDay string `json:"task_day"` RewardCoinAmount int64 `json:"reward_coin_amount"` RewardAssetType string `json:"reward_asset_type"` Status string `json:"status"` WalletTransactionID string `json:"wallet_transaction_id"` GrantedAtMS int64 `json:"granted_at_ms"` Claimed bool `json:"claimed"` } type taskClaimRequestBody struct { TaskID string `json:"task_id"` TaskIDCamel string `json:"taskId"` TaskType string `json:"task_type"` TaskTypeCamel string `json:"taskType"` TaskDay string `json:"task_day"` TaskDayCamel string `json:"taskDay"` CommandID string `json:"command_id"` CommandIDCamel string `json:"commandId"` } func (b taskClaimRequestBody) normalizedTaskID() string { if value := strings.TrimSpace(b.TaskID); value != "" { return value } return strings.TrimSpace(b.TaskIDCamel) } func (b taskClaimRequestBody) normalizedTaskType() string { if value := strings.TrimSpace(b.TaskType); value != "" { return value } return strings.TrimSpace(b.TaskTypeCamel) } func (b taskClaimRequestBody) normalizedTaskDay() string { if value := strings.TrimSpace(b.TaskDay); value != "" { return value } return strings.TrimSpace(b.TaskDayCamel) } func (b taskClaimRequestBody) normalizedCommandID() string { if value := strings.TrimSpace(b.CommandID); value != "" { return value } return strings.TrimSpace(b.CommandIDCamel) } // listTaskTabs 返回当前用户任务页;进度由 activity-service 事实表决定。 func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request) { if h.taskClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } sections := make([]taskSectionData, 0, len(resp.GetSections())) for _, section := range resp.GetSections() { items := make([]taskItemData, 0, len(section.GetItems())) for _, item := range section.GetItems() { items = append(items, taskItemFromProto(item)) } sections = append(sections, taskSectionData{ Section: section.GetSection(), Items: items, ServerTimeMS: section.GetServerTimeMs(), NextRefreshAtMS: section.GetNextRefreshAtMs(), }) } httpkit.WriteOK(writer, request, map[string]any{ "sections": sections, "server_time_ms": resp.GetServerTimeMs(), "next_refresh_at_ms": resp.GetNextRefreshAtMs(), }) } // claimTaskReward 只透传当前用户身份和幂等命令,完成校验及发奖在 activity-service 内完成。 func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Request) { if h.taskClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } var body taskClaimRequestBody if !httpkit.Decode(writer, request, &body) { return } taskID := body.normalizedTaskID() taskType := body.normalizedTaskType() taskDay := body.normalizedTaskDay() // command_id 是客户端动作的幂等锚点,gateway 只裁剪空白,不替客户端补值。 commandID := strings.TrimSpace(body.normalizedCommandID()) if commandID == "" || taskID == "" || taskType == "" || taskDay == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), TaskId: taskID, TaskType: taskType, TaskDay: taskDay, CommandId: commandID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, taskClaimData{ ClaimID: resp.GetClaimId(), TaskID: resp.GetTaskId(), TaskType: resp.GetTaskType(), TaskDay: resp.GetTaskDay(), RewardCoinAmount: resp.GetRewardCoinAmount(), RewardAssetType: resp.GetRewardAssetType(), Status: resp.GetStatus(), WalletTransactionID: resp.GetWalletTransactionId(), GrantedAtMS: resp.GetGrantedAtMs(), Claimed: resp.GetClaimed(), }) } func taskItemFromProto(item *activityv1.TaskItem) taskItemData { // gateway 不重新计算任务状态,只把 activity-service 的进度、奖励、图标和跳转元数据转换为 App JSON 合约。 return taskItemData{ 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(), ActionPayload: taskJSONPayload(item.GetActionPayloadJson()), DimensionFilterJSON: item.GetDimensionFilterJson(), TargetValue: item.GetTargetValue(), TargetUnit: item.GetTargetUnit(), ProgressValue: item.GetProgressValue(), RewardCoinAmount: item.GetRewardCoinAmount(), RewardAssetType: item.GetRewardAssetType(), Status: item.GetStatus(), Claimable: item.GetClaimable(), TaskDay: item.GetTaskDay(), ServerTimeMS: item.GetServerTimeMs(), NextRefreshAtMS: item.GetNextRefreshAtMs(), SortOrder: item.GetSortOrder(), Version: item.GetVersion(), } } func taskJSONPayload(value string) map[string]any { payload := map[string]any{} if strings.TrimSpace(value) == "" { return payload } // action_payload_json 是给 App 的辅助结构化字段;解析失败时仍保留原始字符串字段,避免一个配置错误打断整个任务页。 _ = json.Unmarshal([]byte(value), &payload) if payload == nil { return map[string]any{} } return payload }