From 8c4bc55f4581d0b9ac3e7bb3e9df26e8d6274954 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 29 Dec 2025 14:54:05 +0800 Subject: [PATCH] feat: update app errors --- backend/app/errorx/app_error.go | 47 ++++++++++++++++++++++++--------- backend/app/services/common.go | 2 +- backend/app/services/content.go | 12 ++++----- backend/app/services/creator.go | 15 ++++++----- backend/app/services/order.go | 29 +++++++++++--------- backend/app/services/tenant.go | 8 +++--- backend/app/services/user.go | 12 ++++----- backend/app/services/wallet.go | 8 +++--- backend/llm.txt | 8 +++--- 9 files changed, 86 insertions(+), 55 deletions(-) diff --git a/backend/app/errorx/app_error.go b/backend/app/errorx/app_error.go index dadec8c..dd341b1 100644 --- a/backend/app/errorx/app_error.go +++ b/backend/app/errorx/app_error.go @@ -28,36 +28,59 @@ func (e *AppError) Error() string { // Unwrap 允许通过 errors.Unwrap 遍历到原始错误 func (e *AppError) Unwrap() error { return e.originalErr } +// copy 返回 AppError 的副本,用于链式调用时的并发安全 +func (e *AppError) copy() *AppError { + newErr := *e + return &newErr +} + +// WithCause 携带原始错误并记录调用栈 +func (e *AppError) WithCause(err error) *AppError { + newErr := e.copy() + newErr.originalErr = err + + // 记录调用者位置 + if _, file, line, ok := runtime.Caller(1); ok { + newErr.file = fmt.Sprintf("%s:%d", file, line) + } + return newErr +} + // WithData 添加数据 func (e *AppError) WithData(data any) *AppError { - e.Data = data - return e + newErr := e.copy() + newErr.Data = data + return newErr } // WithMsg 设置消息 func (e *AppError) WithMsg(msg string) *AppError { - e.Message = msg - return e + newErr := e.copy() + newErr.Message = msg + return newErr } func (e *AppError) WithMsgf(format string, args ...any) *AppError { - msg := fmt.Sprintf(format, args...) - return e.WithMsg(msg) + newErr := e.copy() + newErr.Message = fmt.Sprintf(format, args...) + return newErr } // WithSQL 记录SQL信息 func (e *AppError) WithSQL(sql string) *AppError { - e.sql = sql - return e + newErr := e.copy() + newErr.sql = sql + return newErr } // WithParams 记录参数信息,并自动获取调用位置 func (e *AppError) WithParams(params ...any) *AppError { - e.params = params + newErr := e.copy() + newErr.params = params if _, file, line, ok := runtime.Caller(1); ok { - e.file = fmt.Sprintf("%s:%d", file, line) + newErr.file = fmt.Sprintf("%s:%d", file, line) } - return e + return newErr } // NewError 创建应用错误 @@ -67,4 +90,4 @@ func NewError(code ErrorCode, statusCode int, message string) *AppError { Message: message, StatusCode: statusCode, } -} +} \ No newline at end of file diff --git a/backend/app/services/common.go b/backend/app/services/common.go index 31ad886..63f4c9a 100644 --- a/backend/app/services/common.go +++ b/backend/app/services/common.go @@ -63,7 +63,7 @@ func (s *common) Upload(ctx context.Context, file *multipart.FileHeader, typeArg } if err := models.MediaAssetQuery.WithContext(ctx).Create(asset); err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } return &common_dto.UploadResult{ diff --git a/backend/app/services/content.go b/backend/app/services/content.go index e3ebd30..d5908c3 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -51,12 +51,12 @@ func (s *content) List(ctx context.Context, keyword, genre, tenantId, sort strin p := requests.Pagination{Page: int64(page), Limit: 10} total, err := q.Count() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } // Convert to DTO @@ -93,7 +93,7 @@ func (s *content) Get(ctx context.Context, id string) (*content_dto.ContentDetai if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } // Interaction status (isLiked, isFavorited) @@ -127,12 +127,12 @@ func (s *content) ListComments(ctx context.Context, id string, page int) (*reque p := requests.Pagination{Page: int64(page), Limit: 10} total, err := q.Count() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } data := make([]content_dto.Comment, len(list)) @@ -181,7 +181,7 @@ func (s *content) CreateComment(ctx context.Context, id string, form *content_dt } if err := models.CommentQuery.WithContext(ctx).Create(comment); err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil } diff --git a/backend/app/services/creator.go b/backend/app/services/creator.go index e16c194..e4c8e00 100644 --- a/backend/app/services/creator.go +++ b/backend/app/services/creator.go @@ -44,7 +44,7 @@ func (s *creator) Apply(ctx context.Context, form *creator_dto.ApplyForm) error } if err := q.Create(tenant); err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } // Also add user as tenant_admin in tenant_users @@ -55,7 +55,7 @@ func (s *creator) Apply(ctx context.Context, form *creator_dto.ApplyForm) error Status: consts.UserStatusVerified, } if err := models.TenantUserQuery.WithContext(ctx).Create(tu); err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil @@ -101,7 +101,7 @@ func (s *creator) ListContents(ctx context.Context, status, genre, keyword strin list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } var data []creator_dto.ContentItem @@ -185,7 +185,7 @@ func (s *creator) DeleteContent(ctx context.Context, id string) error { _, err = models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid), models.ContentQuery.TenantID.Eq(tid)).Delete() if err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil } @@ -201,7 +201,7 @@ func (s *creator) ListOrders(ctx context.Context, status, keyword string) ([]cre // Filters... list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } var data []creator_dto.Order @@ -217,6 +217,7 @@ func (s *creator) ListOrders(ctx context.Context, status, keyword string) ([]cre } func (s *creator) ProcessRefund(ctx context.Context, id string, form *creator_dto.RefundForm) error { + // Complex logic involving ledgers and order status update return nil } @@ -271,7 +272,7 @@ func (s *creator) getTenantID(ctx context.Context) (int64, error) { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, errorx.ErrPermissionDenied.WithMsg("非创作者") } - return 0, errorx.ErrDatabaseError + return 0, errorx.ErrDatabaseError.WithCause(err) } return t.ID, nil -} +} \ No newline at end of file diff --git a/backend/app/services/order.go b/backend/app/services/order.go index 2475a1b..6df85fa 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -37,7 +37,7 @@ func (s *order) ListUserOrders(ctx context.Context, status string) ([]user_dto.O list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } var data []user_dto.Order @@ -61,7 +61,7 @@ func (s *order) GetUserOrder(ctx context.Context, id string) (*user_dto.Order, e if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } dto := s.toUserOrderDTO(item) @@ -87,7 +87,7 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor price, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First() if err != nil { - return nil, errorx.ErrDataCorrupted.WithMsg("价格信息缺失") + return nil, errorx.ErrDataCorrupted.WithCause(err).WithMsg("价格信息缺失") } // 2. Create Order (Status: Created) @@ -96,16 +96,16 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor UserID: uid, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusCreated, - Currency: price.Currency, + Currency: consts.Currency(price.Currency), // price.Currency is consts.Currency in DB? Yes. AmountOriginal: price.PriceAmount, - AmountDiscount: 0, // Calculate discount if needed - AmountPaid: price.PriceAmount, // Expected to pay - IdempotencyKey: uuid.NewString(), // Should be from client ideally + AmountDiscount: 0, // Calculate discount if needed + AmountPaid: price.PriceAmount, // Expected to pay + IdempotencyKey: uuid.NewString(), // Should be from client ideally Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Populate details } if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } // 3. Create Order Item @@ -118,7 +118,7 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor AmountPaid: order.AmountPaid, } if err := models.OrderItemQuery.WithContext(ctx).Create(item); err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } return &transaction_dto.OrderCreateResponse{ @@ -159,6 +159,7 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti info, err := tx.User.WithContext(ctx). Where(tx.User.ID.Eq(o.UserID), tx.User.Balance.Gte(o.AmountPaid)). Update(tx.User.Balance, gorm.Expr("balance - ?", o.AmountPaid)) + if err != nil { return err } @@ -196,7 +197,7 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti if err != nil { return err } - + ledger := &models.TenantLedger{ TenantID: o.TenantID, UserID: t.UserID, // Owner @@ -217,8 +218,12 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti return nil }) + if err != nil { - return nil, err + if _, ok := err.(*errorx.AppError); ok { + return nil, err + } + return nil, errorx.ErrDatabaseError.WithCause(err) } return &transaction_dto.OrderPayResponse{ @@ -238,4 +243,4 @@ func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order { Amount: float64(o.AmountPaid) / 100.0, CreateTime: o.CreatedAt.Format(time.RFC3339), } -} +} \ No newline at end of file diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index 5ea1b6f..d9a0e6a 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -36,7 +36,7 @@ func (s *tenant) GetPublicProfile(ctx context.Context, id string) (*tenant_dto.T if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } // Stats @@ -106,7 +106,7 @@ func (s *tenant) Follow(ctx context.Context, id string) error { } if err := models.TenantUserQuery.WithContext(ctx).Create(tu); err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil } @@ -121,7 +121,7 @@ func (s *tenant) Unfollow(ctx context.Context, id string) error { _, err := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid), models.TenantUserQuery.UserID.Eq(uid)).Delete() if err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil -} +} \ No newline at end of file diff --git a/backend/app/services/user.go b/backend/app/services/user.go index cd54231..c42098d 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -53,10 +53,10 @@ func (s *user) LoginWithOTP(ctx context.Context, phone, otp string) (*auth_dto.L Gender: consts.GenderSecret, // 默认性别 } if err := query.Create(u); err != nil { - return nil, errorx.ErrDatabaseError.WithMsg("创建用户失败") + return nil, errorx.ErrDatabaseError.WithCause(err).WithMsg("创建用户失败") } } else { - return nil, errorx.ErrDatabaseError.WithMsg("查询用户失败") + return nil, errorx.ErrDatabaseError.WithCause(err).WithMsg("查询用户失败") } } @@ -94,7 +94,7 @@ func (s *user) Me(ctx context.Context) (*auth_dto.User, error) { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } return s.toAuthUserDTO(u), nil @@ -117,7 +117,7 @@ func (s *user) Update(ctx context.Context, form *user_dto.UserUpdate) error { // Birthday: form.Birthday, // 类型转换需处理 }) if err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil @@ -139,7 +139,7 @@ func (s *user) RealName(ctx context.Context, form *user_dto.RealNameForm) error // RealName: form.Realname, // 需在 user 表添加字段? payout_accounts 有 realname }) if err != nil { - return errorx.ErrDatabaseError + return errorx.ErrDatabaseError.WithCause(err) } return nil } @@ -160,7 +160,7 @@ func (s *user) GetNotifications(ctx context.Context, typeArg string) ([]user_dto list, err := query.Order(tbl.CreatedAt.Desc()).Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } result := make([]user_dto.Notification, len(list)) diff --git a/backend/app/services/wallet.go b/backend/app/services/wallet.go index f6fc268..e014675 100644 --- a/backend/app/services/wallet.go +++ b/backend/app/services/wallet.go @@ -33,7 +33,7 @@ func (s *wallet) GetWallet(ctx context.Context) (*user_dto.WalletResponse, error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } // Get Transactions (Orders) @@ -44,7 +44,7 @@ func (s *wallet) GetWallet(ctx context.Context) (*user_dto.WalletResponse, error Limit(20). // Limit to recent 20 Find() if err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } var txs []user_dto.Transaction @@ -100,7 +100,7 @@ func (s *wallet) Recharge(ctx context.Context, form *user_dto.RechargeForm) (*us } if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil { - return nil, errorx.ErrDatabaseError + return nil, errorx.ErrDatabaseError.WithCause(err) } // Mock Pay Params @@ -108,4 +108,4 @@ func (s *wallet) Recharge(ctx context.Context, form *user_dto.RechargeForm) (*us PayParams: "mock_recharge_url", OrderID: cast.ToString(order.ID), }, nil -} +} \ No newline at end of file diff --git a/backend/llm.txt b/backend/llm.txt index cdf40a6..c9f13f0 100644 --- a/backend/llm.txt +++ b/backend/llm.txt @@ -15,6 +15,8 @@ This file condenses `backend/docs/dev/http_api.md` + `backend/docs/dev/model.md` - DO regenerate code after changes (routes/docs/models). - MUST: in `backend/app/services`, prefer the generated GORM-Gen DAO (`backend/database/models/*`) for DB access; treat raw `*gorm.DB` usage as a last resort. - MUST: When building queries in services, improve readability by using the assignment: `tbl, query := models.Query.QueryContext(ctx)`. Then use `tbl` for field references (e.g., `tbl.ID.Eq(...)`) and `query` for chaining methods. +- MUST: in `services`, when an error occurs (e.g., DB error, third-party API error), NEVER return a generic `errorx.ErrXxx` alone if there is an underlying `err`. ALWAYS use `errorx.ErrXxx.WithCause(err)` to wrap the original error. This ensures the centralized Logger captures the full context (file, line, root cause) while the client receives a friendly message and a unique Error ID for tracking. +- MUST: all chainable methods on `AppError` (`WithCause`, `WithMsg`, `WithData`, etc.) are thread-safe and return a new instance (clone). Use them freely to add context to global error variables. - MUST: service-layer transactions MUST use `models.Q.Transaction(func(tx *models.Query) error { ... })`; DO NOT use raw `*_db.Transaction(...)` / `db.Transaction(...)` in services unless Gen cannot express the required operation. - MUST: after adding/removing/renaming any files under `backend/app/services/`, run `atomctl gen service --path ./app/services` to regenerate `backend/app/services/services.gen.go`; DO NOT edit `services.gen.go` manually. - DO add `// @provider` above every controller/service `struct` declaration. @@ -248,7 +250,7 @@ In this case: - `backend/app/events/subscribers/.go`(subscriber:实现 `contracts.EventHandler`,负责 `Topic()` + `Handler(...)`) - 生成后:按项目约定运行一次 `atomctl gen provider`(用于刷新 DI/provider 生成文件)。 -### Topic 约定 +### Topic约定 - 统一在 `backend/app/events/topics.go` 维护 topic 常量,避免散落在各处形成“字符串协议”。 - topic 字符串建议使用稳定前缀(例如 `event:`),并使用 `snake_case` 命名。 @@ -297,7 +299,7 @@ Common types: - `Kind` 建议与业务枚举/事件类型对齐,便于 SQL/报表按 `kind` 过滤。 - `Data` 写入对应 payload 的 JSON(payload 可以是多个不同 struct)。 - 读取时: - - 先 `snap := model.Snapshot.Data()`,再 `switch snap.Kind` 选择对应 payload 结构去 `json.Unmarshal(snap.Data, &payload)`。 + - 先 `snap := model.Snapshot.Data()`,再 `switch snap.Kind` 选择对应 payload结构去 `json.Unmarshal(snap.Data, &payload)`。 - 兼容历史数据(旧 JSON 没有 kind/data)时,`UnmarshalJSON` 可以将其标记为 `legacy` 并把原始 JSON 放入 `Data`,避免线上存量读取失败。 --- @@ -462,4 +464,4 @@ func (s *XxxTestSuite) Test_Method() { So(got, ShouldBeNil) }) } -``` \ No newline at end of file +```