package webui import ( "context" "errors" "cmroubao/backend-api/internal/domain" "cmroubao/backend-api/internal/transport/authcommon" "cmroubao/backend-api/internal/usecase" ) const localAdminSubject = "local-admin" type UsecaseAdapter struct { tasks *usecase.TaskService assets *usecase.AssetService } func NewUsecaseAdapter( tasks *usecase.TaskService, assets *usecase.AssetService, ) (*UsecaseAdapter, error) { if tasks == nil || assets == nil { return nil, errors.New("admin web use cases are required") } return &UsecaseAdapter{ tasks: tasks, assets: assets, }, nil } func (adapter *UsecaseAdapter) ListTasks( ctx context.Context, input ListTasksInput, ) (TaskList, error) { var status *string if input.Status != "" { value := input.Status status = &value } page, err := adapter.tasks.List(ctx, usecase.ListTasksQuery{ CreatorSubject: localAdminSubject, Status: status, Query: input.Query, Limit: input.Limit, Cursor: input.Cursor, }) if err != nil { return TaskList{}, mapUsecaseError(err) } items := make([]TaskSummary, 0, len(page.Items)) for _, item := range page.Items { items = append(items, taskSummaryFrom(item)) } return TaskList{ Items: items, NextCursor: page.NextCursor, }, nil } func (adapter *UsecaseAdapter) GetTask( ctx context.Context, taskID string, ) (Task, error) { detail, err := adapter.tasks.Get(ctx, localAdminSubject, taskID) if err != nil { return Task{}, mapUsecaseError(err) } return taskFromDetail(detail), nil } func (adapter *UsecaseAdapter) UploadReference( ctx context.Context, input UploadReferenceInput, ) (UploadedAsset, error) { result, err := adapter.assets.UploadTaskReference( ctx, usecase.UploadTaskReferenceCommand{ CreatorSubject: localAdminSubject, IdempotencyKey: input.IdempotencyKey, DeclaredMediaType: input.DeclaredType, Content: input.Content, }, ) if err != nil { return UploadedAsset{}, mapUsecaseError(err) } return UploadedAsset{ ID: result.Asset.ID, }, nil } func (adapter *UsecaseAdapter) CreateTask( ctx context.Context, input CreateTaskInput, ) (Task, error) { maxInt := int64(^uint(0) >> 1) if input.Quantity > maxInt { return Task{}, ErrValidation } var budget *string if input.MaxBudget != "" { value := input.MaxBudget budget = &value } result, err := adapter.tasks.Create(ctx, usecase.CreateTaskCommand{ CreatorSubject: localAdminSubject, ActorUserID: actorUserID(ctx), IdempotencyKey: input.IdempotencyKey, Title: input.Title, Description: input.Description, SKU: input.SKU, ImageAssetID: input.ImageAssetID, Quantity: int(input.Quantity), MaxBudget: budget, }) if err != nil { return Task{}, mapUsecaseError(err) } return taskFromPurchase(result.Task), nil } func actorUserID(ctx context.Context) string { principal, ok := authcommon.Principal(ctx) if !ok || principal.Role != domain.UserRoleAdmin { return "" } return principal.UserID } func (adapter *UsecaseAdapter) CancelPending( ctx context.Context, input CancelPendingInput, ) (Task, error) { task, err := adapter.tasks.Cancel(ctx, usecase.CancelTaskCommand{ CreatorSubject: localAdminSubject, ActorUserID: actorUserID(ctx), TaskID: input.TaskID, Reason: "管理员取消", }) if err != nil { return Task{}, mapUsecaseError(err) } return taskFromPurchase(task), nil } func taskSummaryFrom(task domain.PurchaseTask) TaskSummary { return TaskSummary{ ID: task.ID, Title: task.Title, SKU: task.SKU, Status: string(task.Status), UpdatedAt: task.UpdatedAt, } } func taskFromPurchase(task domain.PurchaseTask) Task { budget := domain.FormatOptionalCNY(task.MaxBudgetCents) result := Task{ ID: task.ID, Title: task.Title, SKU: task.SKU, Description: task.Description, Quantity: int64(task.Quantity), Status: string(task.Status), ReferenceAssetID: task.ImageAssetID, CreatedAt: task.CreatedAt, UpdatedAt: task.UpdatedAt, } if budget != nil { result.MaxBudget = *budget } return result } func taskFromDetail(detail domain.TaskDetail) Task { task := taskFromPurchase(detail.Task) task.ReferenceAssetID = detail.Asset.ID return task } func mapUsecaseError(err error) error { var typed *usecase.Error if !errors.As(err, &typed) { return err } var public error switch typed.Kind { case usecase.ErrorKindInvalid: if typed.Code == "TASK_CANCEL_INVALID" { public = ErrNotFound } else if typed.Code == "ASSET_TOO_LARGE" || typed.Code == "ASSET_MEDIA_TYPE_UNSUPPORTED" || typed.Code == "ASSET_IMAGE_INVALID" || typed.Code == "ASSET_FILE_REQUIRED" { public = ErrInvalidFile } else { public = ErrValidation } case usecase.ErrorKindNotFound: public = ErrNotFound case usecase.ErrorKindConflict: public = ErrConflict case usecase.ErrorKindUnavailable: public = ErrUnavailable default: return err } return &adapterError{ public: public, cause: err, } } type adapterError struct { public error cause error } func (err *adapterError) Error() string { return err.public.Error() } func (err *adapterError) Unwrap() []error { return []error{err.public, err.cause} } var _ Service = (*UsecaseAdapter)(nil)