feat: migrate workbench services

This commit is contained in:
richjohnrambo
2026-10-10 23:34:06 +08:00
parent c13fb72849
commit 81e3cf1d03
88 changed files with 7358 additions and 15333 deletions
+231
View File
@@ -0,0 +1,231 @@
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)
}
}