2026-06-11 13:33:44 +08:00

220 lines
8.1 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 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"`
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"`
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(),
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(),
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
}