package httpapi import ( "errors" "net/http" "strconv" "strings" "git.118im.com/im_team/im_workbench/internal/auth" "git.118im.com/im_team/im_workbench/internal/store" "git.118im.com/im_team/im_workbench/internal/tasks" ) type taskClaimRequest struct { HallID int64 `json:"hall_id"` } func (a *API) taskHalls(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } principal, ok := a.authenticateTaskMember(w, r) if !ok { return } result, err := a.tasks.Halls(r.Context(), principal.TenantID, principal.MemberID) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) taskEligibility(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } principal, ok := a.authenticateTaskMember(w, r) if !ok { return } hallID, err := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("hall_id")), 10, 64) if err != nil { writeTaskError(w, r, tasks.ErrHallIDInvalid) return } result, err := a.tasks.Eligibility(r.Context(), principal.TenantID, principal.MemberID, hallID) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) taskStats(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } principal, ok := a.authenticateTaskMember(w, r) if !ok { return } result, err := a.tasks.Stats(r.Context(), principal.TenantID, principal.MemberID) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) taskClaim(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { methodNotAllowed(w, r, http.MethodPost) return } principal, ok := a.authenticateTaskMember(w, r) if !ok { return } var body taskClaimRequest if !decodeBody(w, r, &body) { return } result, err := a.tasks.Claim(r.Context(), principal.TenantID, principal.MemberID, body.HallID) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) taskOrders(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } principal, ok := a.authenticateTaskMember(w, r) if !ok { return } page, ok := optionalPositiveInt(w, r, "page") if !ok { writeTaskError(w, r, tasks.ErrPageInvalid) return } pageSize, ok := optionalPositiveInt(w, r, "page_size") if !ok { writeTaskError(w, r, tasks.ErrPageSizeInvalid) return } result, err := a.tasks.Orders(r.Context(), principal.TenantID, principal.MemberID, page, pageSize, r.URL.Query().Get("status")) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) taskOrderResource(w http.ResponseWriter, r *http.Request) { path := strings.Trim(strings.TrimPrefix(r.URL.Path, "/v1/task-orders/"), "/") parts := strings.Split(path, "/") if len(parts) < 1 || len(parts) > 2 || parts[0] == "" { writeTaskError(w, r, tasks.ErrOrderIDInvalid) return } orderID, err := strconv.ParseInt(parts[0], 10, 64) if err != nil || orderID <= 0 { writeTaskError(w, r, tasks.ErrOrderIDInvalid) return } principal, ok := a.authenticateTaskMember(w, r) if !ok { return } if len(parts) == 1 { if r.Method != http.MethodGet { methodNotAllowed(w, r, http.MethodGet) return } result, err := a.tasks.Order(r.Context(), principal.TenantID, principal.MemberID, orderID) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) return } if parts[1] != "submissions" { writeTaskError(w, r, store.ErrOrderNotFound) return } if r.Method != http.MethodPost { methodNotAllowed(w, r, http.MethodPost) return } var body tasks.SubmitInput if !decodeBody(w, r, &body) { return } result, err := a.tasks.Submit(r.Context(), principal.TenantID, principal.MemberID, orderID, body) if err != nil { writeTaskError(w, r, err) return } writeSuccess(w, r, result) } func (a *API) authenticateTaskMember(w http.ResponseWriter, r *http.Request) (*auth.Principal, bool) { if a.tasks == nil { writeError(w, r, http.StatusServiceUnavailable, 90001, "SERVICE_UNAVAILABLE", "service unavailable", nil) return nil, false } principal, ok := a.authenticateMember(w, r) if !ok { return nil, false } return principal, true } func optionalPositiveInt(w http.ResponseWriter, r *http.Request, name string) (int, bool) { value := strings.TrimSpace(r.URL.Query().Get(name)) if value == "" { return 0, true } parsed, err := strconv.Atoi(value) return parsed, err == nil && parsed > 0 } func writeTaskError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, tasks.ErrHallIDInvalid): writeError(w, r, http.StatusBadRequest, 30000, "HALL_ID_INVALID", "validation failed", map[string]any{"field": "hall_id"}) case errors.Is(err, tasks.ErrOrderIDInvalid): writeError(w, r, http.StatusBadRequest, 30000, "ORDER_ID_INVALID", "validation failed", map[string]any{"field": "order_id"}) case errors.Is(err, tasks.ErrPageInvalid): writeError(w, r, http.StatusBadRequest, 30000, "PAGE_INVALID", "validation failed", map[string]any{"field": "page"}) case errors.Is(err, tasks.ErrPageSizeInvalid): writeError(w, r, http.StatusBadRequest, 30000, "PAGE_SIZE_INVALID", "validation failed", map[string]any{"field": "page_size"}) case errors.Is(err, tasks.ErrStatusInvalid): writeError(w, r, http.StatusBadRequest, 30000, "ORDER_STATUS_INVALID", "validation failed", map[string]any{"field": "status"}) case errors.Is(err, tasks.ErrEvidenceInvalid), errors.Is(err, store.ErrEvidenceInvalid): writeError(w, r, http.StatusBadRequest, 30000, "EVIDENCE_INVALID", "validation failed", map[string]any{"field": "evidence"}) case errors.Is(err, tasks.ErrTaskDisabled), errors.Is(err, store.ErrTaskDisabled): writeError(w, r, http.StatusUnprocessableEntity, 30022, "TASK_DISABLED", "task feature is disabled", nil) case errors.Is(err, tasks.ErrOutsideBusinessHour), errors.Is(err, store.ErrOutsideBusinessHours): writeError(w, r, http.StatusUnprocessableEntity, 30022, "OUTSIDE_BUSINESS_HOURS", "outside business hours", nil) case errors.Is(err, store.ErrInsufficientBalance): writeError(w, r, http.StatusUnprocessableEntity, 30022, "INSUFFICIENT_BALANCE", "insufficient available balance", nil) case errors.Is(err, store.ErrDailyLimitReached): writeError(w, r, http.StatusUnprocessableEntity, 30022, "DAILY_LIMIT_REACHED", "daily claim limit reached", nil) case errors.Is(err, store.ErrLevelNotEligible), errors.Is(err, store.ErrTaskHallNotFound): writeError(w, r, http.StatusUnprocessableEntity, 30022, "LEVEL_NOT_ELIGIBLE", "member level is not eligible", nil) case errors.Is(err, store.ErrAccountNotFound), errors.Is(err, store.ErrAccountRestricted): writeError(w, r, http.StatusUnprocessableEntity, 30022, "ACCOUNT_RESTRICTED", "account is restricted", nil) case errors.Is(err, store.ErrPendingOrderExists): writeError(w, r, http.StatusConflict, 30040, "PENDING_ORDER_EXISTS", "unfinished order exists", nil) case errors.Is(err, store.ErrNoTaskAvailable): writeError(w, r, http.StatusServiceUnavailable, 30053, "NO_TASK_AVAILABLE", "no task is currently available", nil) case errors.Is(err, store.ErrOrderNotFound): writeError(w, r, http.StatusNotFound, 30044, "ORDER_NOT_FOUND", "task order not found", nil) case errors.Is(err, store.ErrOrderState): writeError(w, r, http.StatusConflict, 30040, "ORDER_STATE_INVALID", "task order state does not allow this operation", nil) case errors.Is(err, store.ErrOrderExpired): writeError(w, r, http.StatusConflict, 30040, "ORDER_EXPIRED", "task order has expired", nil) default: writeError(w, r, http.StatusInternalServerError, 90000, reasonInternal, "internal error", nil) } }