thread-master/apps/backend/internal/response/response.go

200 lines
8.9 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 response
import (
"context"
"errors"
"net/http"
"strings"
"apps/backend/internal/domain"
appnotifDomain "apps/backend/internal/module/appnotif/domain"
jobDomain "apps/backend/internal/module/job/domain"
memberDomain "apps/backend/internal/module/member/domain"
inspireDomain "apps/backend/internal/module/inspire/domain"
scoutDomain "apps/backend/internal/module/scout/domain"
studioDomain "apps/backend/internal/module/studio/domain"
threadsDomain "apps/backend/internal/module/threads/domain"
tokenDomain "apps/backend/internal/module/token/domain"
usageDomain "apps/backend/internal/module/usage/domain"
"github.com/zeromicro/go-zero/rest/httpx"
)
// cleanBizMessage strips domain error prefixes so UI can show Chinese hints directly.
func cleanBizMessage(msg string) string {
msg = strings.TrimSpace(msg)
for _, p := range []string{
"studio validation: ",
"inspire validation: ",
"scout validation: ",
"validation: ",
} {
if strings.HasPrefix(msg, p) {
return strings.TrimSpace(strings.TrimPrefix(msg, p))
}
// also case where errors.Is wraps as "prefix: detail"
if i := strings.Index(msg, ": "); i > 0 {
head := msg[:i]
if strings.Contains(head, "validation") || strings.Contains(head, "ErrValidation") {
return strings.TrimSpace(msg[i+2:])
}
}
}
return msg
}
// Envelope is the standard JSON body (AGENTS.md).
type Envelope struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Error interface{} `json:"error"`
}
// Write is used by goctl-generated handlers.
func Write(ctx context.Context, w http.ResponseWriter, data interface{}, err error) {
if err != nil {
status, env := mapError(err)
httpx.WriteJsonCtx(ctx, w, status, env)
return
}
httpx.WriteJsonCtx(ctx, w, http.StatusOK, Envelope{
Code: domain.SuccessCode,
Message: domain.SuccessMessage,
Data: data,
Error: nil,
})
}
func WrapRequestError(err error) error {
if err == nil {
return nil
}
return &bizError{http: http.StatusBadRequest, code: 400001, msg: err.Error()}
}
type bizError struct {
http int
code int64
msg string
}
func (e *bizError) Error() string { return e.msg }
func Biz(httpStatus int, code int64, msg string) error {
return &bizError{http: httpStatus, code: code, msg: msg}
}
func mapError(err error) (int, Envelope) {
var be *bizError
if errors.As(err, &be) {
return be.http, Envelope{Code: be.code, Message: be.msg}
}
switch {
case errors.Is(err, memberDomain.ErrBadPassword):
return http.StatusUnauthorized, Envelope{Code: 401010, Message: "invalid email or password"}
case errors.Is(err, memberDomain.ErrSuspended):
return http.StatusForbidden, Envelope{Code: 403001, Message: "account suspended"}
case errors.Is(err, memberDomain.ErrEmailTaken):
return http.StatusConflict, Envelope{Code: 409001, Message: "email already registered"}
case errors.Is(err, memberDomain.ErrWeakPassword):
// 與 domain.PasswordPolicyEN、FE api.err.400003 / password.policy.hint 同一句
return http.StatusBadRequest, Envelope{Code: 400003, Message: memberDomain.PasswordPolicyEN}
case errors.Is(err, memberDomain.ErrInvalidCode):
return http.StatusBadRequest, Envelope{Code: 400004, Message: "invalid or expired code"}
case errors.Is(err, memberDomain.ErrEmailNotRegistered):
return http.StatusNotFound, Envelope{Code: 404002, Message: "email not registered"}
case errors.Is(err, memberDomain.ErrNotFound):
return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
case errors.Is(err, memberDomain.ErrLastAdmin):
return http.StatusBadRequest, Envelope{Code: 400021, Message: err.Error()}
case errors.Is(err, memberDomain.ErrSelfSuspend):
return http.StatusBadRequest, Envelope{Code: 400020, Message: err.Error()}
// 邀請message 為 FE i18n keyinvite.err.*
case errors.Is(err, memberDomain.ErrInviteAlreadyBound),
errors.Is(err, memberDomain.ErrInviteCodeRequired),
errors.Is(err, memberDomain.ErrInviteCodeNotFound),
errors.Is(err, memberDomain.ErrInviteCodeSelf),
errors.Is(err, memberDomain.ErrInviteCycle),
errors.Is(err, memberDomain.ErrInviteSelfParent),
errors.Is(err, memberDomain.ErrInviteParentMiss):
return http.StatusBadRequest, Envelope{Code: 400070, Message: err.Error()}
case errors.Is(err, memberDomain.ErrInviteNeedAdmin):
return http.StatusForbidden, Envelope{Code: 403002, Message: err.Error()}
case errors.Is(err, tokenDomain.ErrInvalidToken):
return http.StatusUnauthorized, Envelope{Code: 401002, Message: "invalid token"}
case errors.Is(err, threadsDomain.ErrNotFound), errors.Is(err, jobDomain.ErrNotFound),
errors.Is(err, appnotifDomain.ErrNotFound):
return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
case errors.Is(err, threadsDomain.ErrForbidden), errors.Is(err, jobDomain.ErrForbidden),
errors.Is(err, appnotifDomain.ErrForbidden):
return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
case errors.Is(err, threadsDomain.ErrOAuthDenied):
return http.StatusBadRequest, Envelope{Code: 400031, Message: "oauth denied"}
case errors.Is(err, threadsDomain.ErrOAuthExchange), errors.Is(err, threadsDomain.ErrInvalidState):
return http.StatusBadRequest, Envelope{Code: 400032, Message: err.Error()}
case errors.Is(err, threadsDomain.ErrRefreshFailed):
return http.StatusBadRequest, Envelope{Code: 400033, Message: "session refresh failed"}
case errors.Is(err, jobDomain.ErrIllegalStatus):
return http.StatusConflict, Envelope{Code: 409010, Message: "illegal job status transition"}
case errors.Is(err, usageDomain.ErrNoKey):
return http.StatusBadRequest, Envelope{Code: 400040, Message: "no api key configured (platform or byok)"}
case errors.Is(err, usageDomain.ErrQuotaExceeded):
return http.StatusPaymentRequired, Envelope{Code: 402001, Message: "platform credits insufficient"}
case errors.Is(err, usageDomain.ErrMeterCapExceeded):
// message = FE i18n key
return http.StatusPaymentRequired, Envelope{Code: 402002, Message: err.Error()}
case errors.Is(err, usageDomain.ErrPlatformCapacity):
// 平台 key 限流/無容量:建議改 BYOKmessage = FE i18n key
return http.StatusServiceUnavailable, Envelope{Code: 503002, Message: err.Error()}
case errors.Is(err, usageDomain.ErrForbidden):
return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
case errors.Is(err, usageDomain.ErrInvalidPlan):
return http.StatusBadRequest, Envelope{Code: 400041, Message: "invalid plan"}
case errors.Is(err, studioDomain.ErrNotFound):
return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
case errors.Is(err, studioDomain.ErrForbidden):
return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
case errors.Is(err, studioDomain.ErrValidation), errors.Is(err, studioDomain.ErrBadURL):
return http.StatusBadRequest, Envelope{Code: 400050, Message: cleanBizMessage(err.Error())}
case errors.Is(err, studioDomain.ErrNoAccount):
return http.StatusBadRequest, Envelope{Code: 400051, Message: "no usable threads account"}
case errors.Is(err, studioDomain.ErrIllegalStatus), errors.Is(err, studioDomain.ErrRootBlocked):
return http.StatusConflict, Envelope{Code: 409020, Message: err.Error()}
case errors.Is(err, studioDomain.ErrFormulaRemove):
return http.StatusGone, Envelope{Code: 410001, Message: "generateFromFormula removed"}
case errors.Is(err, inspireDomain.ErrNotFound), errors.Is(err, scoutDomain.ErrNotFound):
return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
case errors.Is(err, inspireDomain.ErrForbidden), errors.Is(err, scoutDomain.ErrForbidden):
return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
case errors.Is(err, inspireDomain.ErrValidation), errors.Is(err, scoutDomain.ErrValidation):
return http.StatusBadRequest, Envelope{Code: 400060, Message: cleanBizMessage(err.Error())}
case errors.Is(err, inspireDomain.ErrRemoved), errors.Is(err, scoutDomain.ErrTopicRemoved):
return http.StatusGone, Envelope{Code: 410002, Message: err.Error()}
case errors.Is(err, scoutDomain.ErrNoCrawlerSession):
return http.StatusBadRequest, Envelope{Code: 400061, Message: "crawler session required when dev_mode enabled"}
case errors.Is(err, scoutDomain.ErrHasProducts):
return http.StatusConflict, Envelope{Code: 409030, Message: "brand has products; remove products first"}
default:
return http.StatusInternalServerError, Envelope{Code: 500000, Message: err.Error()}
}
}
func OK(w http.ResponseWriter, data interface{}) {
Write(context.Background(), w, data, nil)
}
func Fail(w http.ResponseWriter, httpStatus int, code int64, message string, errDetail interface{}) {
if code == domain.SuccessCode {
code = 400000
}
httpx.WriteJson(w, httpStatus, Envelope{Code: code, Message: message, Data: nil, Error: errDetail})
}
func NotImplemented(w http.ResponseWriter, feature string) {
Fail(w, http.StatusNotImplemented, 501000, "not implemented: "+feature, map[string]string{"feature": feature})
}
func IsSuccessCode(code int64) bool { return code == domain.SuccessCode }