Refactor order and tenant ledger models to use consts for Currency and Type fields; add new UserStatus values; implement comprehensive test cases for content, creator, order, super, and wallet services.
This commit is contained in:
@@ -2,30 +2,240 @@ 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) {
|
||||
return []user_dto.Order{}, nil
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
return &user_dto.Order{}, nil
|
||||
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
|
||||
}
|
||||
|
||||
dto := s.toUserOrderDTO(item)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateForm) (*transaction_dto.OrderCreateResponse, error) {
|
||||
return &transaction_dto.OrderCreateResponse{}, nil
|
||||
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.WithMsg("价格信息缺失")
|
||||
}
|
||||
|
||||
// 2. Create Order (Status: Created)
|
||||
order := &models.Order{
|
||||
TenantID: content.TenantID,
|
||||
UserID: uid,
|
||||
Type: consts.OrderTypeContentPurchase,
|
||||
Status: consts.OrderStatusCreated,
|
||||
Currency: price.Currency,
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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) {
|
||||
return &transaction_dto.OrderPayResponse{}, nil
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &transaction_dto.OrderPayResponse{
|
||||
PayParams: "balance_paid",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *order) Status(ctx context.Context, id string) (*transaction_dto.OrderStatusResponse, error) {
|
||||
return &transaction_dto.OrderStatusResponse{}, nil
|
||||
// ... 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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user