Files
quyun/backend_v1/app/services/orders.go

196 lines
5.7 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 services
import (
"context"
"time"
"quyun/v2/app/requests"
"quyun/v2/database/models"
"quyun/v2/pkg/fields"
"github.com/pkg/errors"
"github.com/samber/lo"
"go.ipao.vip/gen"
"go.ipao.vip/gen/types"
)
// OrderListItem 用于订单列表展示,补充作品标题与用户名等冗余信息,避免前端二次查询。
type OrderListItem struct {
*models.Order
PostTitle string `json:"post_title"`
Username string `json:"username"`
}
// @provider
type orders struct{}
// List 订单列表(支持按订单号模糊查询、按用户过滤)。
func (m *orders) List(
ctx context.Context,
pagination *requests.Pagination,
conds ...gen.Condition,
) (*requests.Pager, error) {
pagination.Format()
tbl, query := models.OrderQuery.QueryContext(ctx)
orders, cnt, err := query.
Where(conds...).
Order(tbl.ID.Desc()).
FindByPage(int(pagination.Offset()), int(pagination.Limit))
if err != nil {
return nil, errors.Wrap(err, "failed to list orders")
}
// 这里刻意使用“先查订单,再批量查关联”的方式,避免在分页时 JOIN 造成重复行/分页不稳定。
postIDs := lo.Uniq(lo.Map(orders, func(o *models.Order, _ int) int64 { return o.PostID }))
userIDs := lo.Uniq(lo.Map(orders, func(o *models.Order, _ int) int64 { return o.UserID }))
posts, err := models.PostQuery.WithContext(ctx).GetByIDs(postIDs...)
if err != nil {
return nil, errors.Wrap(err, "failed to get posts by ids")
}
users, err := models.UserQuery.WithContext(ctx).GetByIDs(userIDs...)
if err != nil {
return nil, errors.Wrap(err, "failed to get users by ids")
}
postMap := lo.SliceToMap(posts, func(p *models.Post) (int64, *models.Post) { return p.ID, p })
userMap := lo.SliceToMap(users, func(u *models.User) (int64, *models.User) { return u.ID, u })
items := lo.Map(orders, func(o *models.Order, _ int) *OrderListItem {
item := &OrderListItem{Order: o}
if post, ok := postMap[o.PostID]; ok {
item.PostTitle = post.Title
}
if user, ok := userMap[o.UserID]; ok {
item.Username = user.Username
}
return item
})
return &requests.Pager{
Items: items,
Total: cnt,
Pagination: *pagination,
}, nil
}
// Refund 订单退款(余额支付走本地退款;微信支付走微信退款并标记为退款处理中)。
func (m *orders) Refund(ctx context.Context, order *models.Order) error {
// 余额支付:这里强调“状态一致性”,必须在一个事务中完成:余额退回 + 撤销购买权益 + 更新订单状态。
return models.Q.Transaction(func(tx *models.Query) error {
// 已移除微信退款wepay能力仅支持余额支付订单退款。
if order.PaymentMethod != "balance" {
return errors.New("暂不支持该支付方式退款")
}
// 退回余额(使用原子自增,避免并发覆盖)。
costBalance := order.Meta.Data().CostBalance
if costBalance > 0 {
if _, err := tx.User.
WithContext(ctx).
Where(tx.User.ID.Eq(order.UserID)).
Inc(tx.User.Balance, costBalance); err != nil {
return errors.Wrap(err, "failed to refund balance")
}
}
// 撤销已购买的作品权限(删除 user_posts 记录)。
if _, err := tx.UserPost.
WithContext(ctx).
Where(
tx.UserPost.UserID.Eq(order.UserID),
tx.UserPost.PostID.Eq(order.PostID),
).
Delete(); err != nil {
return errors.Wrap(err, "failed to revoke user post")
}
// 更新订单状态为“退款成功”。
if _, err := tx.Order.
WithContext(ctx).
Where(tx.Order.ID.Eq(order.ID)).
Update(tx.Order.Status, fields.OrderStatusRefundSuccess); err != nil {
return errors.Wrap(err, "failed to update order status")
}
return nil
})
}
// GetByOrderNO
func (m *orders) GetByOrderNO(ctx context.Context, orderNo string) (*models.Order, error) {
return models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.OrderNo.Eq(orderNo)).First()
}
func (o *orders) CreateFromUserPostID(ctx context.Context, userId, postId int64) (*models.Order, error) {
post, err := Posts.FindByID(ctx, postId)
if err != nil {
return nil, errors.Wrap(err, "failed to get post")
}
m := &models.Order{}
m.Status = fields.OrderStatusPending
m.OrderNo = time.Now().Format("20060102150405")
m.SubOrderNo = m.OrderNo
m.UserID = userId
m.PostID = postId
m.Meta = types.NewJSONType(fields.OrderMeta{})
m.Price = post.Price
m.Discount = post.Discount
if err := m.Create(ctx); err != nil {
return m, err
}
return m, nil
}
// FindByID
func (m *orders) FindByID(ctx context.Context, orderID int64) (*models.Order, error) {
return models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(orderID)).First()
}
func (m *orders) SetMeta(ctx context.Context, orderID int64, metaFunc func(fields.OrderMeta) fields.OrderMeta) error {
order, err := m.FindByID(ctx, orderID)
if err != nil {
return err
}
order.Meta = types.NewJSONType(metaFunc(order.Meta.Data()))
_, err = order.Update(ctx)
return err
}
// SetStatus
func (m *orders) SetStatus(ctx context.Context, orderID int64, status fields.OrderStatus) error {
tbl, query := models.OrderQuery.QueryContext(ctx)
_, err := query.Where(tbl.ID.Eq(orderID)).Update(tbl.Status, status)
return err
}
// SumAmount
func (m *orders) SumAmount(ctx context.Context) (int64, error) {
tbl, query := models.OrderQuery.QueryContext(ctx)
var calc struct {
Amount int64 `json:"amount"`
}
err := query.Select(tbl.Price.Sum().As("amount")).Scan(&calc)
if err != nil {
return 0, errors.Wrap(err, "failed to sum amount")
}
return calc.Amount, nil
}
// Count
func (m *orders) Count(ctx context.Context, conds ...gen.Condition) (int64, error) {
_, query := models.OrderQuery.QueryContext(ctx)
if len(conds) > 0 {
query = query.Where(conds...)
}
return query.Count()
}