package services import ( "context" "errors" "time" "quyun/v2/app/errorx" transaction_dto "quyun/v2/app/http/v1/dto" user_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database/fields" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/google/uuid" "github.com/spf13/cast" "go.ipao.vip/gen/types" "gorm.io/gorm" ) // @provider type order struct{} func (s *order) ListUserOrders(ctx context.Context, status string) ([]user_dto.Order, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return nil, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) tbl, q := models.OrderQuery.QueryContext(ctx) q = q.Where(tbl.UserID.Eq(uid)) if status != "" && status != "all" { q = q.Where(tbl.Status.Eq(consts.OrderStatus(status))) } list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var data []user_dto.Order for _, v := range list { data = append(data, s.toUserOrderDTO(v)) } return data, nil } func (s *order) GetUserOrder(ctx context.Context, id string) (*user_dto.Order, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return nil, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) oid := cast.ToInt64(id) tbl, q := models.OrderQuery.QueryContext(ctx) item, err := q.Where(tbl.ID.Eq(oid), tbl.UserID.Eq(uid)).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } return nil, errorx.ErrDatabaseError.WithCause(err) } dto := s.toUserOrderDTO(item) return &dto, nil } func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateForm) (*transaction_dto.OrderCreateResponse, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return nil, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) cid := cast.ToInt64(form.ContentID) // 1. Fetch Content & Price content, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).First() if err != nil { return nil, errorx.ErrRecordNotFound.WithMsg("内容不存在") } if content.Status != consts.ContentStatusPublished { return nil, errorx.ErrBusinessLogic.WithMsg("内容未发布") } price, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First() if err != nil { return nil, errorx.ErrDataCorrupted.WithCause(err).WithMsg("价格信息缺失") } // 2. Create Order (Status: Created) order := &models.Order{ TenantID: content.TenantID, UserID: uid, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusCreated, 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 Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Populate details } if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } // 3. Create Order Item item := &models.OrderItem{ TenantID: content.TenantID, UserID: uid, OrderID: order.ID, ContentID: cid, ContentUserID: content.UserID, AmountPaid: order.AmountPaid, } if err := models.OrderItemQuery.WithContext(ctx).Create(item); err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } return &transaction_dto.OrderCreateResponse{ OrderID: cast.ToString(order.ID), }, nil } func (s *order) Pay(ctx context.Context, id string, form *transaction_dto.OrderPayForm) (*transaction_dto.OrderPayResponse, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return nil, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) oid := cast.ToInt64(id) // Fetch Order o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid), models.OrderQuery.UserID.Eq(uid)).First() if err != nil { return nil, errorx.ErrRecordNotFound } if o.Status != consts.OrderStatusCreated { return nil, errorx.ErrStatusConflict.WithMsg("订单状态不可支付") } if form.Method == "balance" { return s.payWithBalance(ctx, o) } // External payment (mock) return &transaction_dto.OrderPayResponse{ PayParams: "mock_pay_params", }, nil } func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transaction_dto.OrderPayResponse, error) { err := models.Q.Transaction(func(tx *models.Query) error { // 1. Deduct User Balance 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 } if info.RowsAffected == 0 { return errorx.ErrQuotaExceeded.WithMsg("余额不足") } // 2. Update Order Status now := time.Now() _, err = tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(o.ID)).Updates(&models.Order{ Status: consts.OrderStatusPaid, PaidAt: now, }) if err != nil { return err } // 3. Grant Content Access items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find() for _, item := range items { access := &models.ContentAccess{ TenantID: item.TenantID, UserID: o.UserID, ContentID: item.ContentID, OrderID: o.ID, Status: consts.ContentAccessStatusActive, } if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil { return err } } // 4. Create Tenant Ledger (Revenue) t, err := tx.Tenant.WithContext(ctx).Where(tx.Tenant.ID.Eq(o.TenantID)).First() if err != nil { return err } ledger := &models.TenantLedger{ TenantID: o.TenantID, UserID: t.UserID, // Owner OrderID: o.ID, Type: consts.TenantLedgerTypeDebitPurchase, // Income from purchase Amount: o.AmountPaid, BalanceBefore: 0, // TODO: Fetch previous balance if tracking tenant balance BalanceAfter: 0, // TODO FrozenBefore: 0, FrozenAfter: 0, IdempotencyKey: uuid.NewString(), Remark: "内容销售收入", OperatorUserID: o.UserID, } if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil { return err } return nil }) if err != nil { if _, ok := err.(*errorx.AppError); ok { return nil, err } return nil, errorx.ErrDatabaseError.WithCause(err) } return &transaction_dto.OrderPayResponse{ PayParams: "balance_paid", }, nil } func (s *order) Status(ctx context.Context, id string) (*transaction_dto.OrderStatusResponse, error) { // ... check status ... return nil, nil } func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order { return user_dto.Order{ ID: cast.ToString(o.ID), Status: string(o.Status), // Need cast for DTO string field if DTO field is string Amount: float64(o.AmountPaid) / 100.0, CreateTime: o.CreatedAt.Format(time.RFC3339), } }