摸大红支持购物车多选一起结账
下单支持多商品行快照与一次支付;支付后整单复制文案,前台提供购物车加购与结账。
This commit is contained in:
@@ -66,6 +66,7 @@ type MohongOrder struct {
|
||||
AmountCent int64 `gorm:"column:amount_cent;not null;default:0" json:"-"`
|
||||
Status string `gorm:"size:32;not null;default:'pending_payment';index" json:"status"`
|
||||
ProductSnapshot datatypes.JSON `gorm:"column:product_snapshot" json:"product_snapshot"`
|
||||
ItemsSnapshot datatypes.JSON `gorm:"column:items_snapshot" json:"items_snapshot"`
|
||||
QrcodeURLSnapshot string `gorm:"column:qrcode_url_snapshot;size:512;not null;default:''" json:"qrcode_url_snapshot"`
|
||||
CopyText string `gorm:"type:text;not null" json:"copy_text"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
configKeyDefaultQrcodeURL = "mohong.default_qrcode_url"
|
||||
configKeyOrderCopyTemplate = "mohong.order_copy_template"
|
||||
|
||||
defaultOrderCopyTemplate = "【摸大红订单】\n订单号:{{order_no}}\n下单时间:{{created_at}}\n商品:{{product_title}}\n数量:{{quantity}}\n金额:{{amount}}\n下单人:{{buyer_name}}({{buyer_phone}})"
|
||||
defaultOrderCopyTemplate = "【摸大红订单】\n订单号:{{order_no}}\n下单时间:{{created_at}}\n商品明细:\n{{items}}\n数量合计:{{quantity}}\n金额:{{amount}}\n下单人:{{buyer_name}}({{buyer_phone}})"
|
||||
)
|
||||
|
||||
func (r *Repository) GetConfig(ctx context.Context) (*ConfigDTO, error) {
|
||||
|
||||
@@ -87,36 +87,56 @@ type UpdateProductRequest struct {
|
||||
QrcodeImageURL *string `json:"qrcode_image_url"`
|
||||
}
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
type CreateOrderItemRequest struct {
|
||||
ProductID uint64 `json:"product_id" binding:"required"`
|
||||
Quantity int `json:"quantity" binding:"required,min=1,max=99"`
|
||||
}
|
||||
|
||||
// CreateOrderRequest 支持单商品(product_id+quantity)或多商品(items)。
|
||||
type CreateOrderRequest struct {
|
||||
ProductID uint64 `json:"product_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
Items []CreateOrderItemRequest `json:"items"`
|
||||
}
|
||||
|
||||
type OrderItemDTO struct {
|
||||
ProductID uint64 `json:"product_id"`
|
||||
Title string `json:"title"`
|
||||
CoverURL string `json:"cover_url"`
|
||||
Unit string `json:"unit"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPriceCent int64 `json:"unit_price_cent"`
|
||||
UnitPrice string `json:"unit_price"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
Amount string `json:"amount"`
|
||||
}
|
||||
|
||||
type OrderDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
ProductID uint64 `json:"product_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPriceCent int64 `json:"unit_price_cent"`
|
||||
UnitPrice string `json:"unit_price"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
Amount string `json:"amount"`
|
||||
Status string `json:"status"`
|
||||
ProductTitle string `json:"product_title"`
|
||||
ProductCoverURL string `json:"product_cover_url"`
|
||||
ProductUnit string `json:"product_unit"`
|
||||
QrcodeURLSnapshot string `json:"qrcode_url_snapshot"`
|
||||
CopyText string `json:"copy_text"`
|
||||
BuyerNickname string `json:"buyer_nickname,omitempty"`
|
||||
BuyerPhone string `json:"buyer_phone,omitempty"`
|
||||
AdminRemark string `json:"admin_remark,omitempty"`
|
||||
CancelReason string `json:"cancel_reason,omitempty"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
CancelledAt *time.Time `json:"cancelled_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint64 `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
ProductID uint64 `json:"product_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
UnitPriceCent int64 `json:"unit_price_cent"`
|
||||
UnitPrice string `json:"unit_price"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
Amount string `json:"amount"`
|
||||
Status string `json:"status"`
|
||||
ProductTitle string `json:"product_title"`
|
||||
ProductCoverURL string `json:"product_cover_url"`
|
||||
ProductUnit string `json:"product_unit"`
|
||||
Items []OrderItemDTO `json:"items,omitempty"`
|
||||
QrcodeURLSnapshot string `json:"qrcode_url_snapshot"`
|
||||
CopyText string `json:"copy_text"`
|
||||
BuyerNickname string `json:"buyer_nickname,omitempty"`
|
||||
BuyerPhone string `json:"buyer_phone,omitempty"`
|
||||
AdminRemark string `json:"admin_remark,omitempty"`
|
||||
CancelReason string `json:"cancel_reason,omitempty"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
CancelledAt *time.Time `json:"cancelled_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type OrderListQuery struct {
|
||||
@@ -162,3 +182,16 @@ type productSnapshot struct {
|
||||
Unit string `json:"unit"`
|
||||
QrcodeImageURL string `json:"qrcode_image_url"`
|
||||
}
|
||||
|
||||
// orderItemSnapshot 订单商品行快照(多商品结账)。
|
||||
type orderItemSnapshot struct {
|
||||
ID uint64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
CoverURL string `json:"cover_url"`
|
||||
ImageURLs []string `json:"image_urls"`
|
||||
Description string `json:"description"`
|
||||
PriceCent int64 `json:"price_cent"`
|
||||
Unit string `json:"unit"`
|
||||
QrcodeImageURL string `json:"qrcode_image_url"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
@@ -20,64 +20,88 @@ import (
|
||||
)
|
||||
|
||||
func (r *Repository) CreateOrder(ctx context.Context, userID uint64, req CreateOrderRequest) (*OrderDTO, error) {
|
||||
if userID == 0 || req.ProductID == 0 || req.Quantity < 1 || req.Quantity > 99 {
|
||||
if userID == 0 {
|
||||
return nil, ErrInvalidRequest
|
||||
}
|
||||
reqItems := normalizeCreateOrderItems(req)
|
||||
if len(reqItems) == 0 {
|
||||
return nil, ErrInvalidRequest
|
||||
}
|
||||
var createdID uint64
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var product model.MohongProduct
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, req.ProductID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return ErrProductNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if product.Status != model.MohongProductStatusOnSale {
|
||||
return ErrProductUnavailable
|
||||
}
|
||||
if product.PriceCent <= 0 {
|
||||
return ErrProductUnavailable
|
||||
}
|
||||
if product.Stock >= 0 && product.Stock < req.Quantity {
|
||||
return ErrStockInsufficient
|
||||
}
|
||||
// 下单时预扣库存,取消未支付订单时归还。
|
||||
if product.Stock >= 0 {
|
||||
product.Stock -= req.Quantity
|
||||
if err := tx.Save(&product).Error; err != nil {
|
||||
itemSnaps := make([]orderItemSnapshot, 0, len(reqItems))
|
||||
var totalQty int
|
||||
var totalAmount int64
|
||||
var primary productSnapshot
|
||||
|
||||
for _, line := range reqItems {
|
||||
var product model.MohongProduct
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, line.ProductID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return ErrProductNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if product.Status != model.MohongProductStatusOnSale || product.PriceCent <= 0 {
|
||||
return ErrProductUnavailable
|
||||
}
|
||||
if product.Stock >= 0 && product.Stock < line.Quantity {
|
||||
return ErrStockInsufficient
|
||||
}
|
||||
// 下单时预扣库存,取消未支付订单时归还。
|
||||
if product.Stock >= 0 {
|
||||
product.Stock -= line.Quantity
|
||||
if err := tx.Save(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
images := decodeStringList(product.ImageURLs)
|
||||
item := orderItemSnapshot{
|
||||
ID: product.ID,
|
||||
Title: product.Title,
|
||||
CoverURL: product.CoverURL,
|
||||
ImageURLs: images,
|
||||
Description: product.Description,
|
||||
PriceCent: product.PriceCent,
|
||||
Unit: product.Unit,
|
||||
QrcodeImageURL: product.QrcodeImageURL,
|
||||
Quantity: line.Quantity,
|
||||
}
|
||||
itemSnaps = append(itemSnaps, item)
|
||||
totalQty += line.Quantity
|
||||
totalAmount += product.PriceCent * int64(line.Quantity)
|
||||
if primary.ID == 0 {
|
||||
primary = productSnapshot{
|
||||
ID: product.ID, Title: product.Title, CoverURL: product.CoverURL,
|
||||
ImageURLs: images, Description: product.Description, PriceCent: product.PriceCent,
|
||||
Unit: product.Unit, QrcodeImageURL: product.QrcodeImageURL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orderNo, err := newMohongOrderNo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
images := decodeStringList(product.ImageURLs)
|
||||
snap := productSnapshot{
|
||||
ID: product.ID,
|
||||
Title: product.Title,
|
||||
CoverURL: product.CoverURL,
|
||||
ImageURLs: images,
|
||||
Description: product.Description,
|
||||
PriceCent: product.PriceCent,
|
||||
Unit: product.Unit,
|
||||
QrcodeImageURL: product.QrcodeImageURL,
|
||||
}
|
||||
snapRaw, err := json.Marshal(snap)
|
||||
primaryRaw, err := json.Marshal(primary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
amount := product.PriceCent * int64(req.Quantity)
|
||||
itemsRaw, err := json.Marshal(itemSnaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 展示字段:单商品保持原语义;多商品 quantity 为合计,unit_price 用首项单价。
|
||||
order := model.MohongOrder{
|
||||
OrderNo: orderNo,
|
||||
UserID: userID,
|
||||
ProductID: product.ID,
|
||||
Quantity: req.Quantity,
|
||||
UnitPriceCent: product.PriceCent,
|
||||
AmountCent: amount,
|
||||
ProductID: primary.ID,
|
||||
Quantity: totalQty,
|
||||
UnitPriceCent: primary.PriceCent,
|
||||
AmountCent: totalAmount,
|
||||
Status: model.MohongOrderStatusPendingPayment,
|
||||
ProductSnapshot: datatypes.JSON(snapRaw),
|
||||
ProductSnapshot: datatypes.JSON(primaryRaw),
|
||||
ItemsSnapshot: datatypes.JSON(itemsRaw),
|
||||
CopyText: "",
|
||||
}
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
@@ -103,6 +127,40 @@ func (r *Repository) CreateOrder(ctx context.Context, userID uint64, req CreateO
|
||||
return r.FindOrderForUser(ctx, userID, createdID)
|
||||
}
|
||||
|
||||
func normalizeCreateOrderItems(req CreateOrderRequest) []CreateOrderItemRequest {
|
||||
// 优先 items;否则兼容旧单商品参数。
|
||||
if len(req.Items) > 0 {
|
||||
merged := map[uint64]int{}
|
||||
orderIDs := make([]uint64, 0, len(req.Items))
|
||||
for _, item := range req.Items {
|
||||
if item.ProductID == 0 || item.Quantity < 1 || item.Quantity > 99 {
|
||||
continue
|
||||
}
|
||||
if _, ok := merged[item.ProductID]; !ok {
|
||||
orderIDs = append(orderIDs, item.ProductID)
|
||||
}
|
||||
merged[item.ProductID] += item.Quantity
|
||||
if merged[item.ProductID] > 99 {
|
||||
merged[item.ProductID] = 99
|
||||
}
|
||||
}
|
||||
out := make([]CreateOrderItemRequest, 0, len(orderIDs))
|
||||
for _, id := range orderIDs {
|
||||
if qty := merged[id]; qty > 0 {
|
||||
out = append(out, CreateOrderItemRequest{ProductID: id, Quantity: qty})
|
||||
}
|
||||
}
|
||||
if len(out) > 50 {
|
||||
return out[:50]
|
||||
}
|
||||
return out
|
||||
}
|
||||
if req.ProductID > 0 && req.Quantity >= 1 && req.Quantity <= 99 {
|
||||
return []CreateOrderItemRequest{{ProductID: req.ProductID, Quantity: req.Quantity}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindOrderForUser(ctx context.Context, userID, orderID uint64) (*OrderDTO, error) {
|
||||
var row model.MohongOrder
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ?", orderID, userID).First(&row).Error; err != nil {
|
||||
@@ -272,9 +330,10 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
|
||||
if err := tx.First(&user, order.UserID).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
items := decodeOrderItems(order)
|
||||
snap := decodeProductSnapshot(order.ProductSnapshot)
|
||||
qrcodeURL := r.resolveQrcodeURL(tx, snap.QrcodeImageURL)
|
||||
copyText := buildCopyText(r.loadCopyTemplate(tx), order, snap, user)
|
||||
qrcodeURL := r.resolveQrcodeURL(tx, resolveOrderQrcode(items, snap))
|
||||
copyText := buildCopyText(r.loadCopyTemplate(tx), order, items, snap, user)
|
||||
now := time.Now()
|
||||
|
||||
order.Status = model.MohongOrderStatusPaid
|
||||
@@ -303,15 +362,19 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
|
||||
func (r *Repository) NotifyNewConversation(conversationID uint64) {}
|
||||
|
||||
func (r *Repository) cancelPendingOrderTx(tx *gorm.DB, order *model.MohongOrder, reason string) error {
|
||||
// 归还预扣库存
|
||||
if order.Quantity > 0 {
|
||||
// 归还预扣库存(支持多商品行)
|
||||
for _, item := range decodeOrderItems(*order) {
|
||||
if item.Quantity <= 0 || item.ID == 0 {
|
||||
continue
|
||||
}
|
||||
var product model.MohongProduct
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, order.ProductID).Error; err == nil {
|
||||
if product.Stock >= 0 {
|
||||
product.Stock += order.Quantity
|
||||
if err := tx.Save(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&product, item.ID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
if product.Stock >= 0 {
|
||||
product.Stock += item.Quantity
|
||||
if err := tx.Save(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,17 +387,18 @@ func (r *Repository) cancelPendingOrderTx(tx *gorm.DB, order *model.MohongOrder,
|
||||
|
||||
func (r *Repository) orderDTO(ctx context.Context, row model.MohongOrder, includeAdmin bool) (*OrderDTO, error) {
|
||||
snap := decodeProductSnapshot(row.ProductSnapshot)
|
||||
items := decodeOrderItems(row)
|
||||
var nickname, phone string
|
||||
var user model.User
|
||||
if err := r.db.WithContext(ctx).Select("id", "nickname", "phone").First(&user, row.UserID).Error; err == nil {
|
||||
nickname = user.Nickname
|
||||
phone = user.Phone
|
||||
}
|
||||
dto := toOrderDTO(row, snap, nickname, phone, includeAdmin)
|
||||
dto := toOrderDTO(row, snap, items, nickname, phone, includeAdmin)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func buildCopyText(template string, order model.MohongOrder, snap productSnapshot, user model.User) string {
|
||||
func buildCopyText(template string, order model.MohongOrder, items []orderItemSnapshot, snap productSnapshot, user model.User) string {
|
||||
if strings.TrimSpace(template) == "" {
|
||||
template = defaultOrderCopyTemplate
|
||||
}
|
||||
@@ -342,33 +406,110 @@ func buildCopyText(template string, order model.MohongOrder, snap productSnapsho
|
||||
if buyerName == "" {
|
||||
buyerName = "用户"
|
||||
}
|
||||
createdAt := timeutil.ShanghaiNow().Format("2006-01-02 15:04:05")
|
||||
if !order.CreatedAt.IsZero() {
|
||||
createdAt = order.CreatedAt.In(timeutil.ShanghaiLocation()).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
productTitle := snap.Title
|
||||
if productTitle == "" && len(items) > 0 {
|
||||
productTitle = items[0].Title
|
||||
}
|
||||
if len(items) > 1 {
|
||||
productTitle = fmt.Sprintf("%s 等%d种", productTitle, len(items))
|
||||
}
|
||||
unit := snap.Unit
|
||||
if unit == "" && len(items) > 0 {
|
||||
unit = items[0].Unit
|
||||
}
|
||||
replacer := strings.NewReplacer(
|
||||
"{{order_no}}", order.OrderNo,
|
||||
"{{created_at}}", timeutil.ShanghaiNow().Format("2006-01-02 15:04:05"),
|
||||
"{{product_title}}", snap.Title,
|
||||
"{{created_at}}", createdAt,
|
||||
"{{product_title}}", productTitle,
|
||||
"{{items}}", formatItemsForCopy(items, snap, order.Quantity),
|
||||
"{{quantity}}", fmt.Sprintf("%d", order.Quantity),
|
||||
"{{amount}}", money.FormatWithSymbol(order.AmountCent),
|
||||
"{{buyer_name}}", buyerName,
|
||||
"{{buyer_phone}}", maskPhone(user.Phone),
|
||||
"{{unit}}", snap.Unit,
|
||||
"{{unit}}", unit,
|
||||
)
|
||||
// 下单时间用订单创建时间更准确
|
||||
created := order.CreatedAt
|
||||
if !created.IsZero() {
|
||||
replacer = strings.NewReplacer(
|
||||
"{{order_no}}", order.OrderNo,
|
||||
"{{created_at}}", created.In(timeutil.ShanghaiLocation()).Format("2006-01-02 15:04:05"),
|
||||
"{{product_title}}", snap.Title,
|
||||
"{{quantity}}", fmt.Sprintf("%d", order.Quantity),
|
||||
"{{amount}}", money.FormatWithSymbol(order.AmountCent),
|
||||
"{{buyer_name}}", buyerName,
|
||||
"{{buyer_phone}}", maskPhone(user.Phone),
|
||||
"{{unit}}", snap.Unit,
|
||||
)
|
||||
}
|
||||
return replacer.Replace(template)
|
||||
}
|
||||
|
||||
func formatItemsForCopy(items []orderItemSnapshot, snap productSnapshot, fallbackQty int) string {
|
||||
if len(items) == 0 && snap.ID > 0 {
|
||||
items = []orderItemSnapshot{{
|
||||
ID: snap.ID, Title: snap.Title, PriceCent: snap.PriceCent,
|
||||
Unit: snap.Unit, Quantity: fallbackQty,
|
||||
}}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return "-"
|
||||
}
|
||||
lines := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
lineAmount := item.PriceCent * int64(item.Quantity)
|
||||
unit := strings.TrimSpace(item.Unit)
|
||||
if unit == "" {
|
||||
unit = "件"
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(
|
||||
"- %s ×%d%s 单价%s 小计%s",
|
||||
item.Title,
|
||||
item.Quantity,
|
||||
unit,
|
||||
money.FormatWithSymbol(item.PriceCent),
|
||||
money.FormatWithSymbol(lineAmount),
|
||||
))
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func decodeOrderItems(order model.MohongOrder) []orderItemSnapshot {
|
||||
if len(order.ItemsSnapshot) > 0 {
|
||||
var items []orderItemSnapshot
|
||||
if err := json.Unmarshal(order.ItemsSnapshot, &items); err == nil && len(items) > 0 {
|
||||
return items
|
||||
}
|
||||
}
|
||||
// 兼容旧订单:从 product_snapshot 还原单行
|
||||
snap := decodeProductSnapshot(order.ProductSnapshot)
|
||||
if snap.ID == 0 && order.ProductID == 0 {
|
||||
return nil
|
||||
}
|
||||
qty := order.Quantity
|
||||
if qty < 1 {
|
||||
qty = 1
|
||||
}
|
||||
id := snap.ID
|
||||
if id == 0 {
|
||||
id = order.ProductID
|
||||
}
|
||||
price := snap.PriceCent
|
||||
if price <= 0 {
|
||||
price = order.UnitPriceCent
|
||||
}
|
||||
return []orderItemSnapshot{{
|
||||
ID: id,
|
||||
Title: firstNonEmpty(snap.Title, "商品"),
|
||||
CoverURL: snap.CoverURL,
|
||||
ImageURLs: snap.ImageURLs,
|
||||
Description: snap.Description,
|
||||
PriceCent: price,
|
||||
Unit: snap.Unit,
|
||||
QrcodeImageURL: snap.QrcodeImageURL,
|
||||
Quantity: qty,
|
||||
}}
|
||||
}
|
||||
|
||||
func resolveOrderQrcode(items []orderItemSnapshot, snap productSnapshot) string {
|
||||
for _, item := range items {
|
||||
if strings.TrimSpace(item.QrcodeImageURL) != "" {
|
||||
return item.QrcodeImageURL
|
||||
}
|
||||
}
|
||||
return snap.QrcodeImageURL
|
||||
}
|
||||
|
||||
func newMohongOrderNo() (string, error) {
|
||||
var b [4]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package mohong
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -42,7 +43,40 @@ func toProductDTOWithCategory(row model.MohongProduct, categoryName string, incl
|
||||
return dto
|
||||
}
|
||||
|
||||
func toOrderDTO(row model.MohongOrder, snap productSnapshot, buyerNickname, buyerPhone string, includeAdmin bool) OrderDTO {
|
||||
func toOrderDTO(row model.MohongOrder, snap productSnapshot, items []orderItemSnapshot, buyerNickname, buyerPhone string, includeAdmin bool) OrderDTO {
|
||||
if len(items) == 0 {
|
||||
items = decodeOrderItems(row)
|
||||
}
|
||||
title := snap.Title
|
||||
cover := snap.CoverURL
|
||||
unit := snap.Unit
|
||||
if title == "" && len(items) > 0 {
|
||||
title = items[0].Title
|
||||
}
|
||||
if cover == "" && len(items) > 0 {
|
||||
cover = items[0].CoverURL
|
||||
}
|
||||
if unit == "" && len(items) > 0 {
|
||||
unit = items[0].Unit
|
||||
}
|
||||
if len(items) > 1 && title != "" {
|
||||
title = fmt.Sprintf("%s 等%d种", title, len(items))
|
||||
}
|
||||
itemDTOs := make([]OrderItemDTO, 0, len(items))
|
||||
for _, item := range items {
|
||||
lineAmount := item.PriceCent * int64(item.Quantity)
|
||||
itemDTOs = append(itemDTOs, OrderItemDTO{
|
||||
ProductID: item.ID,
|
||||
Title: item.Title,
|
||||
CoverURL: item.CoverURL,
|
||||
Unit: item.Unit,
|
||||
Quantity: item.Quantity,
|
||||
UnitPriceCent: item.PriceCent,
|
||||
UnitPrice: money.Format(item.PriceCent),
|
||||
AmountCent: lineAmount,
|
||||
Amount: money.Format(lineAmount),
|
||||
})
|
||||
}
|
||||
dto := OrderDTO{
|
||||
ID: row.ID,
|
||||
OrderNo: row.OrderNo,
|
||||
@@ -54,9 +88,10 @@ func toOrderDTO(row model.MohongOrder, snap productSnapshot, buyerNickname, buye
|
||||
AmountCent: row.AmountCent,
|
||||
Amount: money.Format(row.AmountCent),
|
||||
Status: row.Status,
|
||||
ProductTitle: snap.Title,
|
||||
ProductCoverURL: snap.CoverURL,
|
||||
ProductUnit: snap.Unit,
|
||||
ProductTitle: title,
|
||||
ProductCoverURL: cover,
|
||||
ProductUnit: unit,
|
||||
Items: itemDTOs,
|
||||
QrcodeURLSnapshot: row.QrcodeURLSnapshot,
|
||||
CopyText: row.CopyText,
|
||||
CancelReason: row.CancelReason,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 摸大红订单支持多商品:商品行快照(兼容旧单:无 items 时仍读 product_snapshot)
|
||||
ALTER TABLE mohong_orders
|
||||
ADD COLUMN items_snapshot JSON NULL COMMENT '订单商品行快照[{product_id,title,quantity,unit_price_cent,...}]' AFTER product_snapshot;
|
||||
|
||||
UPDATE system_configs
|
||||
SET `value` = '【摸大红订单】\n订单号:{{order_no}}\n下单时间:{{created_at}}\n商品明细:\n{{items}}\n数量合计:{{quantity}}\n金额:{{amount}}\n下单人:{{buyer_name}}({{buyer_phone}})',
|
||||
description = '摸大红订单一键复制文案模板(支持多商品 {{items}})'
|
||||
WHERE `key` = 'mohong.order_copy_template'
|
||||
AND (`value` LIKE '%商品:{{product_title}}%' OR `value` = '');
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE mohong_orders DROP COLUMN items_snapshot;
|
||||
Reference in New Issue
Block a user