- Introduced TenantLedger model with fields for managing tenant transactions, including ID, TenantID, UserID, OrderID, transaction Type, Amount, and balance details. - Implemented CRUD operations for TenantLedger with methods for Create, Update, Delete, and Reload. - Generated query methods for TenantLedger to facilitate database interactions, including filtering, pagination, and aggregation functions. - Established relationships with Order model for foreign key references.
613 lines
18 KiB
Go
613 lines
18 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"time"
|
||
|
||
"quyun/v2/app/errorx"
|
||
"quyun/v2/app/http/tenant/dto"
|
||
"quyun/v2/app/requests"
|
||
"quyun/v2/database/models"
|
||
"quyun/v2/pkg/consts"
|
||
|
||
pkgerrors "github.com/pkg/errors"
|
||
"github.com/samber/lo"
|
||
"github.com/sirupsen/logrus"
|
||
"go.ipao.vip/gen"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
|
||
"go.ipao.vip/gen/types"
|
||
)
|
||
|
||
// PurchaseContentParams defines parameters for purchasing a content within a tenant using tenant balance.
|
||
type PurchaseContentParams struct {
|
||
// TenantID is the tenant scope.
|
||
TenantID int64
|
||
// UserID is the buyer user id.
|
||
UserID int64
|
||
// ContentID is the target content id.
|
||
ContentID int64
|
||
// IdempotencyKey is used to ensure a purchase request is processed at most once.
|
||
IdempotencyKey string
|
||
// Now is the logical time used for created_at/paid_at and ledger snapshots (optional).
|
||
Now time.Time
|
||
}
|
||
|
||
// PurchaseContentResult is returned after purchase attempt (idempotent hit returns existing order/access state).
|
||
type PurchaseContentResult struct {
|
||
// Order is the created or existing order record (may be nil when already purchased without order context).
|
||
Order *models.Order
|
||
// OrderItem is the related order item record (single-item purchase).
|
||
OrderItem *models.OrderItem
|
||
// Access is the content access record after purchase grant.
|
||
Access *models.ContentAccess
|
||
// AmountPaid is the final paid amount in cents (CNY 分).
|
||
AmountPaid int64
|
||
}
|
||
|
||
// order provides order domain operations.
|
||
//
|
||
// @provider
|
||
type order struct {
|
||
db *gorm.DB
|
||
ledger *ledger
|
||
}
|
||
|
||
// MyOrderPage lists orders for current user within a tenant.
|
||
func (s *order) MyOrderPage(ctx context.Context, tenantID, userID int64, filter *dto.MyOrderListFilter) (*requests.Pager, error) {
|
||
if tenantID <= 0 || userID <= 0 {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/user_id must be > 0")
|
||
}
|
||
if filter == nil {
|
||
filter = &dto.MyOrderListFilter{}
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"user_id": userID,
|
||
"status": lo.FromPtr(filter.Status),
|
||
}).Info("services.order.me.page")
|
||
|
||
filter.Pagination.Format()
|
||
|
||
tbl, query := models.OrderQuery.QueryContext(ctx)
|
||
query = query.Preload(tbl.Items)
|
||
|
||
conds := []gen.Condition{
|
||
tbl.TenantID.Eq(tenantID),
|
||
tbl.UserID.Eq(userID),
|
||
}
|
||
if filter.Status != nil {
|
||
conds = append(conds, tbl.Status.Eq(*filter.Status))
|
||
}
|
||
|
||
items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &requests.Pager{
|
||
Pagination: filter.Pagination,
|
||
Total: total,
|
||
Items: items,
|
||
}, nil
|
||
}
|
||
|
||
// MyOrderDetail returns order detail for current user within a tenant.
|
||
func (s *order) MyOrderDetail(ctx context.Context, tenantID, userID, orderID int64) (*models.Order, error) {
|
||
if tenantID <= 0 || userID <= 0 || orderID <= 0 {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/user_id/order_id must be > 0")
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"user_id": userID,
|
||
"order_id": orderID,
|
||
}).Info("services.order.me.detail")
|
||
|
||
tbl, query := models.OrderQuery.QueryContext(ctx)
|
||
m, err := query.Preload(tbl.Items).Where(
|
||
tbl.TenantID.Eq(tenantID),
|
||
tbl.UserID.Eq(userID),
|
||
tbl.ID.Eq(orderID),
|
||
).First()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// AdminOrderPage lists orders within a tenant for tenant-admin.
|
||
func (s *order) AdminOrderPage(ctx context.Context, tenantID int64, filter *dto.AdminOrderListFilter) (*requests.Pager, error) {
|
||
if tenantID <= 0 {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id must be > 0")
|
||
}
|
||
if filter == nil {
|
||
filter = &dto.AdminOrderListFilter{}
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"user_id": lo.FromPtr(filter.UserID),
|
||
"status": lo.FromPtr(filter.Status),
|
||
}).Info("services.order.admin.page")
|
||
|
||
filter.Pagination.Format()
|
||
|
||
tbl, query := models.OrderQuery.QueryContext(ctx)
|
||
query = query.Preload(tbl.Items)
|
||
|
||
conds := []gen.Condition{tbl.TenantID.Eq(tenantID)}
|
||
if filter.UserID != nil {
|
||
conds = append(conds, tbl.UserID.Eq(*filter.UserID))
|
||
}
|
||
if filter.Status != nil {
|
||
conds = append(conds, tbl.Status.Eq(*filter.Status))
|
||
}
|
||
|
||
items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &requests.Pager{
|
||
Pagination: filter.Pagination,
|
||
Total: total,
|
||
Items: items,
|
||
}, nil
|
||
}
|
||
|
||
// AdminOrderDetail returns an order detail within a tenant for tenant-admin.
|
||
func (s *order) AdminOrderDetail(ctx context.Context, tenantID, orderID int64) (*models.Order, error) {
|
||
if tenantID <= 0 || orderID <= 0 {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/order_id must be > 0")
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"order_id": orderID,
|
||
}).Info("services.order.admin.detail")
|
||
|
||
tbl, query := models.OrderQuery.QueryContext(ctx)
|
||
m, err := query.Preload(tbl.Items).Where(tbl.TenantID.Eq(tenantID), tbl.ID.Eq(orderID)).First()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// AdminRefundOrder refunds a paid order (supports forced refund) and revokes granted content access.
|
||
func (s *order) AdminRefundOrder(ctx context.Context, tenantID, operatorUserID, orderID int64, force bool, reason, idempotencyKey string, now time.Time) (*models.Order, error) {
|
||
if tenantID <= 0 || operatorUserID <= 0 || orderID <= 0 {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/operator_user_id/order_id must be > 0")
|
||
}
|
||
if now.IsZero() {
|
||
now = time.Now()
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"operator_user_id": operatorUserID,
|
||
"order_id": orderID,
|
||
"force": force,
|
||
"idempotency_key": idempotencyKey,
|
||
}).Info("services.order.admin.refund")
|
||
|
||
var out *models.Order
|
||
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
var orderModel models.Order
|
||
if err := tx.
|
||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Preload("Items").
|
||
Where("tenant_id = ? AND id = ?", tenantID, orderID).
|
||
First(&orderModel).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
if orderModel.Status == consts.OrderStatusRefunded {
|
||
out = &orderModel
|
||
return nil
|
||
}
|
||
if orderModel.Status != consts.OrderStatusPaid {
|
||
return errorx.ErrStatusConflict.WithMsg("订单非已支付状态,无法退款")
|
||
}
|
||
if orderModel.PaidAt.IsZero() {
|
||
return errorx.ErrPreconditionFailed.WithMsg("订单缺少 paid_at,无法退款")
|
||
}
|
||
|
||
if !force {
|
||
deadline := orderModel.PaidAt.Add(consts.DefaultOrderRefundWindow)
|
||
if now.After(deadline) {
|
||
return errorx.ErrPreconditionFailed.WithMsg("已超过默认退款时间窗")
|
||
}
|
||
}
|
||
|
||
amount := orderModel.AmountPaid
|
||
refundKey := fmt.Sprintf("refund:%d", orderModel.ID)
|
||
|
||
if amount > 0 {
|
||
if _, err := s.ledger.CreditRefundTx(ctx, tx, tenantID, orderModel.UserID, orderModel.ID, amount, refundKey, reason, now); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// revoke content access immediately
|
||
for _, item := range orderModel.Items {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
if err := tx.Table(models.TableNameContentAccess).
|
||
Where("tenant_id = ? AND user_id = ? AND content_id = ?", tenantID, orderModel.UserID, item.ContentID).
|
||
Updates(map[string]any{
|
||
"status": consts.ContentAccessStatusRevoked,
|
||
"revoked_at": now,
|
||
"updated_at": now,
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if err := tx.Table(models.TableNameOrder).
|
||
Where("id = ?", orderModel.ID).
|
||
Updates(map[string]any{
|
||
"status": consts.OrderStatusRefunded,
|
||
"refunded_at": now,
|
||
"refund_forced": force,
|
||
"refund_operator_user_id": operatorUserID,
|
||
"refund_reason": reason,
|
||
"updated_at": now,
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
orderModel.Status = consts.OrderStatusRefunded
|
||
orderModel.RefundedAt = now
|
||
orderModel.RefundForced = force
|
||
orderModel.RefundOperatorUserID = operatorUserID
|
||
orderModel.RefundReason = reason
|
||
orderModel.UpdatedAt = now
|
||
|
||
out = &orderModel
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"operator_user_id": operatorUserID,
|
||
"order_id": orderID,
|
||
"force": force,
|
||
"idempotency_key": idempotencyKey,
|
||
}).WithError(err).Warn("services.order.admin.refund.failed")
|
||
return nil, err
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": tenantID,
|
||
"operator_user_id": operatorUserID,
|
||
"order_id": orderID,
|
||
"status": out.Status,
|
||
"refund_forced": out.RefundForced,
|
||
}).Info("services.order.admin.refund.ok")
|
||
|
||
return out, nil
|
||
}
|
||
|
||
func (s *order) PurchaseContent(ctx context.Context, params *PurchaseContentParams) (*PurchaseContentResult, error) {
|
||
if params == nil {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("params is required")
|
||
}
|
||
if params.TenantID <= 0 || params.UserID <= 0 || params.ContentID <= 0 {
|
||
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id/user_id/content_id must be > 0")
|
||
}
|
||
|
||
now := params.Now
|
||
if now.IsZero() {
|
||
now = time.Now()
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": params.TenantID,
|
||
"user_id": params.UserID,
|
||
"content_id": params.ContentID,
|
||
"idempotency_key": params.IdempotencyKey,
|
||
}).Info("services.order.purchase_content")
|
||
|
||
var out PurchaseContentResult
|
||
|
||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
if params.IdempotencyKey != "" {
|
||
var existing models.Order
|
||
if err := tx.
|
||
Preload("Items").
|
||
Where("tenant_id = ? AND user_id = ? AND idempotency_key = ?", params.TenantID, params.UserID, params.IdempotencyKey).
|
||
First(&existing).Error; err == nil {
|
||
out.Order = &existing
|
||
if len(existing.Items) > 0 {
|
||
out.OrderItem = existing.Items[0]
|
||
}
|
||
if out.OrderItem != nil {
|
||
var access models.ContentAccess
|
||
if err := tx.
|
||
Where("tenant_id = ? AND user_id = ? AND content_id = ?", params.TenantID, params.UserID, out.OrderItem.ContentID).
|
||
First(&access).Error; err == nil {
|
||
out.Access = &access
|
||
}
|
||
}
|
||
out.AmountPaid = existing.AmountPaid
|
||
return nil
|
||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
}
|
||
|
||
var content models.Content
|
||
if err := tx.
|
||
Where("tenant_id = ? AND id = ? AND deleted_at IS NULL", params.TenantID, params.ContentID).
|
||
First(&content).Error; err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return errorx.ErrRecordNotFound.WithMsg("content not found")
|
||
}
|
||
return err
|
||
}
|
||
if content.Status != consts.ContentStatusPublished {
|
||
return errorx.ErrPreconditionFailed.WithMsg("content not published")
|
||
}
|
||
|
||
if content.UserID == params.UserID {
|
||
out.AmountPaid = 0
|
||
if err := s.grantAccess(ctx, tx, params.TenantID, params.UserID, params.ContentID, 0, now); err != nil {
|
||
return err
|
||
}
|
||
var access models.ContentAccess
|
||
if err := tx.Where("tenant_id = ? AND user_id = ? AND content_id = ?", params.TenantID, params.UserID, params.ContentID).First(&access).Error; err != nil {
|
||
return err
|
||
}
|
||
out.Access = &access
|
||
return nil
|
||
}
|
||
|
||
var accessExisting models.ContentAccess
|
||
if err := tx.
|
||
Where("tenant_id = ? AND user_id = ? AND content_id = ?", params.TenantID, params.UserID, params.ContentID).
|
||
First(&accessExisting).Error; err == nil {
|
||
if accessExisting.Status == consts.ContentAccessStatusActive {
|
||
out.Access = &accessExisting
|
||
return nil
|
||
}
|
||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
|
||
var price models.ContentPrice
|
||
priceAmount := int64(0)
|
||
if err := tx.Where("tenant_id = ? AND content_id = ?", params.TenantID, params.ContentID).First(&price).Error; err == nil {
|
||
priceAmount = price.PriceAmount
|
||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
|
||
amountPaid := s.computeFinalPrice(priceAmount, &price, now)
|
||
out.AmountPaid = amountPaid
|
||
|
||
if amountPaid == 0 {
|
||
orderModel := &models.Order{
|
||
TenantID: params.TenantID,
|
||
UserID: params.UserID,
|
||
Type: consts.OrderTypeContentPurchase,
|
||
Status: consts.OrderStatusPaid,
|
||
Currency: consts.CurrencyCNY,
|
||
AmountOriginal: priceAmount,
|
||
AmountDiscount: priceAmount - amountPaid,
|
||
AmountPaid: amountPaid,
|
||
Snapshot: types.JSON([]byte("{}")),
|
||
IdempotencyKey: params.IdempotencyKey,
|
||
PaidAt: now,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if err := tx.Create(orderModel).Error; err != nil {
|
||
return err
|
||
}
|
||
item := &models.OrderItem{
|
||
TenantID: params.TenantID,
|
||
UserID: params.UserID,
|
||
OrderID: orderModel.ID,
|
||
ContentID: params.ContentID,
|
||
ContentUserID: content.UserID,
|
||
AmountPaid: amountPaid,
|
||
Snapshot: types.JSON([]byte("{}")),
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if err := tx.Create(item).Error; err != nil {
|
||
return err
|
||
}
|
||
if err := s.grantAccess(ctx, tx, params.TenantID, params.UserID, params.ContentID, orderModel.ID, now); err != nil {
|
||
return err
|
||
}
|
||
var access models.ContentAccess
|
||
if err := tx.Where("tenant_id = ? AND user_id = ? AND content_id = ?", params.TenantID, params.UserID, params.ContentID).First(&access).Error; err != nil {
|
||
return err
|
||
}
|
||
out.Order = orderModel
|
||
out.OrderItem = item
|
||
out.Access = &access
|
||
return nil
|
||
}
|
||
|
||
orderModel := &models.Order{
|
||
TenantID: params.TenantID,
|
||
UserID: params.UserID,
|
||
Type: consts.OrderTypeContentPurchase,
|
||
Status: consts.OrderStatusCreated,
|
||
Currency: consts.CurrencyCNY,
|
||
AmountOriginal: priceAmount,
|
||
AmountDiscount: priceAmount - amountPaid,
|
||
AmountPaid: amountPaid,
|
||
Snapshot: types.JSON([]byte("{}")),
|
||
IdempotencyKey: params.IdempotencyKey,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
|
||
freezeKey := fmt.Sprintf("%s:freeze", params.IdempotencyKey)
|
||
if params.IdempotencyKey == "" {
|
||
freezeKey = ""
|
||
}
|
||
if _, err := s.ledger.FreezeTx(ctx, tx, params.TenantID, params.UserID, 0, amountPaid, freezeKey, "purchase freeze", now); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := tx.Create(orderModel).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
item := &models.OrderItem{
|
||
TenantID: params.TenantID,
|
||
UserID: params.UserID,
|
||
OrderID: orderModel.ID,
|
||
ContentID: params.ContentID,
|
||
ContentUserID: content.UserID,
|
||
AmountPaid: amountPaid,
|
||
Snapshot: types.JSON([]byte("{}")),
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if err := tx.Create(item).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
debitKey := fmt.Sprintf("%s:debit", params.IdempotencyKey)
|
||
if params.IdempotencyKey == "" {
|
||
debitKey = ""
|
||
}
|
||
if _, err := s.ledger.DebitPurchaseTx(ctx, tx, params.TenantID, params.UserID, orderModel.ID, amountPaid, debitKey, "purchase debit", now); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := tx.Model(&models.Order{}).
|
||
Where("id = ?", orderModel.ID).
|
||
Updates(map[string]any{
|
||
"status": consts.OrderStatusPaid,
|
||
"paid_at": now,
|
||
"updated_at": now,
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := s.grantAccess(ctx, tx, params.TenantID, params.UserID, params.ContentID, orderModel.ID, now); err != nil {
|
||
return err
|
||
}
|
||
|
||
var access models.ContentAccess
|
||
if err := tx.Where("tenant_id = ? AND user_id = ? AND content_id = ?", params.TenantID, params.UserID, params.ContentID).First(&access).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
out.Order = orderModel
|
||
out.OrderItem = item
|
||
out.Access = &access
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": params.TenantID,
|
||
"user_id": params.UserID,
|
||
"content_id": params.ContentID,
|
||
"idempotency_key": params.IdempotencyKey,
|
||
}).WithError(err).Warn("services.order.purchase_content.failed")
|
||
return nil, pkgerrors.Wrap(err, "purchase content failed")
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"tenant_id": params.TenantID,
|
||
"user_id": params.UserID,
|
||
"content_id": params.ContentID,
|
||
"order_id": loID(out.Order),
|
||
"amount_paid": out.AmountPaid,
|
||
"idempotency_key": params.IdempotencyKey,
|
||
}).Info("services.order.purchase_content.ok")
|
||
|
||
return &out, nil
|
||
}
|
||
|
||
func (s *order) computeFinalPrice(priceAmount int64, price *models.ContentPrice, now time.Time) int64 {
|
||
if priceAmount <= 0 || price == nil {
|
||
return 0
|
||
}
|
||
|
||
discountType := price.DiscountType
|
||
if discountType == "" {
|
||
discountType = consts.DiscountTypeNone
|
||
}
|
||
|
||
if !price.DiscountStartAt.IsZero() && now.Before(price.DiscountStartAt) {
|
||
return priceAmount
|
||
}
|
||
if !price.DiscountEndAt.IsZero() && now.After(price.DiscountEndAt) {
|
||
return priceAmount
|
||
}
|
||
|
||
switch discountType {
|
||
case consts.DiscountTypePercent:
|
||
percent := price.DiscountValue
|
||
if percent <= 0 {
|
||
return priceAmount
|
||
}
|
||
if percent >= 100 {
|
||
return 0
|
||
}
|
||
return priceAmount * (100 - percent) / 100
|
||
case consts.DiscountTypeAmount:
|
||
amount := price.DiscountValue
|
||
if amount <= 0 {
|
||
return priceAmount
|
||
}
|
||
if amount >= priceAmount {
|
||
return 0
|
||
}
|
||
return priceAmount - amount
|
||
default:
|
||
return priceAmount
|
||
}
|
||
}
|
||
|
||
func (s *order) grantAccess(ctx context.Context, tx *gorm.DB, tenantID, userID, contentID, orderID int64, now time.Time) error {
|
||
insert := map[string]any{
|
||
"tenant_id": tenantID,
|
||
"user_id": userID,
|
||
"content_id": contentID,
|
||
"order_id": orderID,
|
||
"status": consts.ContentAccessStatusActive,
|
||
"revoked_at": nil,
|
||
"created_at": now,
|
||
"updated_at": now,
|
||
}
|
||
|
||
if err := tx.Table(models.TableNameContentAccess).
|
||
Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "tenant_id"}, {Name: "user_id"}, {Name: "content_id"}},
|
||
DoUpdates: clause.Assignments(map[string]any{
|
||
"order_id": orderID,
|
||
"status": consts.ContentAccessStatusActive,
|
||
"revoked_at": nil,
|
||
"updated_at": now,
|
||
}),
|
||
}).
|
||
Create(insert).Error; err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func loID(m *models.Order) int64 {
|
||
if m == nil {
|
||
return 0
|
||
}
|
||
return m.ID
|
||
}
|