feat: 去掉订单超时/取消等邮件通知,减少打扰

This commit is contained in:
dujiao-next committed 2026-08-05 14:45:57 +08:00
1 parent 7d6946959a
commit 35c5fece51
18 files changed
+143 -141

No files matched your search

-3
View File
@@ -2836,7 +2836,6 @@ const messages = {
paid: '已支付',
delivered: '已交付(无内容)',
delivered_with_content: '已交付(含内容)',
canceled: '已取消',
refunded: '已退款',
partially_refunded: '部分退款',
},
@@ -7192,7 +7191,6 @@ const messages = {
paid: '已支付',
delivered: '已交付(無內容)',
delivered_with_content: '已交付(含內容)',
canceled: '已取消',
refunded: '已退款',
partially_refunded: '部分退款',
},
@@ -11548,7 +11546,6 @@ const messages = {
paid: 'Paid',
delivered: 'Delivered (no content)',
delivered_with_content: 'Delivered (with content)',
canceled: 'Canceled',
refunded: 'Refunded',
partially_refunded: 'Partially Refunded',
},
@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest'
import { orderEmailSceneKeys } from './orderEmailTemplates'
describe('order email scenes', () => {
it('does not define canceled while retaining actionable refund notifications', () => {
expect(orderEmailSceneKeys).not.toContain('canceled')
expect(orderEmailSceneKeys).toContain('refunded')
expect(orderEmailSceneKeys).toContain('partially_refunded')
})
})
@@ -0,0 +1,8 @@
export const orderEmailSceneKeys = [
'default',
'paid',
'delivered',
'delivered_with_content',
'refunded',
'partially_refunded',
] as const
+2 -3
View File
@@ -14,6 +14,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { notifyError, notifySuccess } from '@/utils/notify'
import { applySiteIcon } from '@/utils/favicon'
import { getImageUrl } from '@/utils/image'
import { orderEmailSceneKeys } from '@/utils/orderEmailTemplates'
import MediaPicker from '@/components/admin/MediaPicker.vue'
import SettingsSMTPTab from './components/SettingsSMTPTab.vue'
import SettingsCaptchaTab from './components/SettingsCaptchaTab.vue'
@@ -296,7 +297,6 @@ const orderEmailTemplateData = reactive({
paid: createOrderEmailSceneTemplate(),
delivered: createOrderEmailSceneTemplate(),
delivered_with_content: createOrderEmailSceneTemplate(),
canceled: createOrderEmailSceneTemplate(),
refunded: createOrderEmailSceneTemplate(),
partially_refunded: createOrderEmailSceneTemplate(),
},
@@ -555,8 +555,7 @@ const fetchSettings = async () => {
const tmplData = orderEmailTmplRes.data.data as Record<string, unknown>
const templates = tmplData.templates as Record<string, unknown> | undefined
if (templates) {
const sceneKeys = ['default', 'paid', 'delivered', 'delivered_with_content', 'canceled', 'refunded', 'partially_refunded'] as const
sceneKeys.forEach((key) => {
orderEmailSceneKeys.forEach((key) => {
const scene = templates[key] as Record<string, unknown> | undefined
if (scene) {
supportedLanguages.forEach((lang) => {
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { notifyError, notifySuccess } from '@/utils/notify'
import { orderEmailSceneKeys } from '@/utils/orderEmailTemplates'
const { t } = useI18n()
@@ -22,14 +23,13 @@ interface OrderEmailTemplateData {
paid: OrderEmailSceneTemplate
delivered: OrderEmailSceneTemplate
delivered_with_content: OrderEmailSceneTemplate
canceled: OrderEmailSceneTemplate
refunded: OrderEmailSceneTemplate
partially_refunded: OrderEmailSceneTemplate
}
guest_tip: Record<SupportedLanguage, string>
}
const sceneKeys = ['default', 'paid', 'delivered', 'delivered_with_content', 'canceled', 'refunded', 'partially_refunded'] as const
const sceneKeys = orderEmailSceneKeys
type SceneKey = (typeof sceneKeys)[number]
const props = defineProps<{
@@ -67,7 +67,6 @@ const form = reactive({
paid: createSceneTemplate(),
delivered: createSceneTemplate(),
delivered_with_content: createSceneTemplate(),
canceled: createSceneTemplate(),
refunded: createSceneTemplate(),
partially_refunded: createSceneTemplate(),
},
@@ -10,6 +10,7 @@ import (
"github.com/dujiao-next/internal/app/container"
"github.com/dujiao-next/internal/config"
"github.com/dujiao-next/internal/constants"
notificationcontract "github.com/dujiao-next/internal/modules/notification/contract"
notificationsmtp "github.com/dujiao-next/internal/modules/notification/infrastructure/smtp"
"github.com/dujiao-next/internal/queue"
@@ -310,6 +311,41 @@ func TestHandleOrderStatusEmailSkipsNonRetryableEmailErrors(t *testing.T) {
}
}
func TestHandleOrderStatusEmailSkipsCanceledWithoutDependencies(t *testing.T) {
task, err := queue.NewOrderStatusEmailTask(queue.OrderStatusEmailPayload{
OrderID: 106,
Status: constants.OrderStatusCanceled,
})
if err != nil {
t.Fatalf("new order status email task failed: %v", err)
}
consumer := &Consumer{}
if err := consumer.handleOrderStatusEmail(context.Background(), task); err != nil {
t.Fatalf("expected canceled email task to be dropped, got %v", err)
}
}
func TestHandleOrderStatusEmailSkipsCanceledRegisteredOrderWhenPayloadStatusEmpty(t *testing.T) {
task, err := queue.NewOrderStatusEmailTask(queue.OrderStatusEmailPayload{OrderID: 107})
if err != nil {
t.Fatalf("new order status email task failed: %v", err)
}
consumer := &Consumer{
Container: &container.Container{},
orderReader: orderStatusEmailWorkerOrderRepoStub{order: &orderdomain.Order{
ID: 107,
OrderNo: "DJ-ORDER-107",
UserID: 42,
Status: constants.OrderStatusCanceled,
}},
}
if err := consumer.handleOrderStatusEmail(context.Background(), task); err != nil {
t.Fatalf("expected canceled registered-order task to be dropped, got %v", err)
}
}
func TestBuildOrderFulfillmentEmailPayloadFromChildren(t *testing.T) {
order := &orderdomain.Order{
Children: []orderdomain.Order{
+17 -18
View File
@@ -43,6 +43,13 @@ func (c *Consumer) handleOrderStatusEmail(ctx context.Context, task *asynq.Task)
logger.Debugw("worker_order_status_email_skip_invalid_payload", "order_id", payload.OrderID)
return nil
}
payloadStatus := strings.TrimSpace(payload.Status)
// 兼容部署前已进入 Redis 的旧取消邮件任务。新代码已不再创建此类任务;
// 这里保留墓碑分支,避免旧任务在取消模板删除后误用默认模板发送。
if payloadStatus == constants.OrderStatusCanceled {
logger.Debugw("worker_order_status_email_skip_canceled", "order_id", payload.OrderID)
return nil
}
// 消费侧二次校验开关:避免 SMTP/订单通知关闭后,旧任务(含 retry/scheduled 中的任务)继续触发发送。
if c.SettingService != nil && c.Config != nil {
smtpSetting, smtpErr := c.SettingService.GetSMTPSetting(c.Config.Email)
@@ -72,20 +79,16 @@ func (c *Consumer) handleOrderStatusEmail(ctx context.Context, task *asynq.Task)
logger.Debugw("worker_order_status_email_skip_order_not_found", "order_id", payload.OrderID)
return nil
}
// 游客订单的"取消"通知价值极低(用户大概率已离开),但极易被滥用为退信轰炸载体:
// 攻击者可用伪造邮箱批量下单等过期取消触发发送。统一不发,保留登录用户与其它状态。
if order.UserID == 0 {
emailStatus := strings.TrimSpace(payload.Status)
if emailStatus == "" {
emailStatus = order.Status
}
if emailStatus == constants.OrderStatusCanceled {
logger.Debugw("worker_order_status_email_skip_guest_canceled",
"order_id", order.ID,
"order_no", order.OrderNo,
)
return nil
}
status := payloadStatus
if status == "" {
status = strings.TrimSpace(order.Status)
}
if status == constants.OrderStatusCanceled {
logger.Debugw("worker_order_status_email_skip_canceled",
"order_id", order.ID,
"order_no", order.OrderNo,
)
return nil
}
var receiverEmail string
var locale string
@@ -124,10 +127,6 @@ func (c *Consumer) handleOrderStatusEmail(ctx context.Context, task *asynq.Task)
tmplSetting = &setting
}
}
status := strings.TrimSpace(payload.Status)
if status == "" {
status = order.Status
}
payloadText := buildOrderFulfillmentEmailPayload(order)
emailBrand, brandErr := c.resolveOrderEmailBrand(ctx, order)
if brandErr != nil {
-3
View File
@@ -320,7 +320,6 @@ var messages = map[string]map[string]string{
"email.order_status.subject": "订单状态更新:%s",
"email.order_status.body": "订单号:%s\n状态:%s\n金额:%s %s\n\n感谢您的购买。\n\n%s 的网址:%s",
"email.order_status.body_paid": "订单号:%s\n状态:%s\n金额:%s %s\n\n我们已收到您的付款,将尽快完成交付。\n\n%s 的网址:%s",
"email.order_status.body_canceled": "订单号:%s\n状态:%s\n金额:%s %s\n\n订单已取消,如有疑问请联系管理员。\n\n%s 的网址:%s",
"email.order_status.body_refunded": "订单号:%s\n状态:%s\n退款金额:%s %s\n退款原因:%s\n\n订单已退款,如有疑问请联系管理员。\n\n%s 的网址:%s",
"email.order_status.body_partially_refunded": "订单号:%s\n状态:%s\n退款金额:%s %s\n退款原因:%s\n\n订单已部分退款,如有疑问请联系管理员。\n\n%s 的网址:%s",
"email.order_status.body_delivered_simple": "订单号:%s\n状态:%s\n金额:%s %s\n\n交付已完成,感谢您的购买。\n\n%s 的网址:%s",
@@ -673,7 +672,6 @@ var messages = map[string]map[string]string{
"email.order_status.subject": "訂單狀態更新:%s",
"email.order_status.body": "訂單號:%s\n狀態:%s\n金額:%s %s\n\n感謝您的購買。\n\n%s 的網址:%s",
"email.order_status.body_paid": "訂單號:%s\n狀態:%s\n金額:%s %s\n\n已收到付款,將盡快完成交付。\n\n%s 的網址:%s",
"email.order_status.body_canceled": "訂單號:%s\n狀態:%s\n金額:%s %s\n\n訂單已取消,如有疑問請聯絡管理員。\n\n%s 的網址:%s",
"email.order_status.body_refunded": "訂單號:%s\n狀態:%s\n退款金額:%s %s\n退款原因:%s\n\n訂單已退款,如有疑問請聯絡管理員。\n\n%s 的網址:%s",
"email.order_status.body_partially_refunded": "訂單號:%s\n狀態:%s\n退款金額:%s %s\n退款原因:%s\n\n訂單已部分退款,如有疑問請聯絡管理員。\n\n%s 的網址:%s",
"email.order_status.body_delivered_simple": "訂單號:%s\n狀態:%s\n金額:%s %s\n\n交付已完成,感謝您的購買。\n\n%s 的網址:%s",
@@ -1026,7 +1024,6 @@ var messages = map[string]map[string]string{
"email.order_status.subject": "Order status updated: %s",
"email.order_status.body": "Order No: %s\nStatus: %s\nAmount: %s %s\n\nThank you for your purchase.\n\n%s's Site URL: %s",
"email.order_status.body_paid": "Order No: %s\nStatus: %s\nAmount: %s %s\n\nWe have received your payment and will deliver soon.\n\n%s's Site URL: %s",
"email.order_status.body_canceled": "Order No: %s\nStatus: %s\nAmount: %s %s\n\nThe order has been canceled. Please contact admin if needed.\n\n%s's Site URL: %s",
"email.order_status.body_refunded": "Order No: %s\nStatus: %s\nRefund Amount: %s %s\nReason for refund: %s\n\nThe order has been refunded. Please contact admin if needed.\n\n%s's Site URL: %s",
"email.order_status.body_partially_refunded": "Order No: %s\nStatus: %s\nRefund Amount: %s %s\nReason for refund: %s\n\nThe order has been partially refunded. Please contact admin if needed.\n\n%s's Site URL: %s",
"email.order_status.body_delivered_simple": "Order No: %s\nStatus: %s\nAmount: %s %s\n\nDelivery completed. Thank you for your purchase.\n\n%s's Site URL: %s",
@@ -172,13 +172,15 @@ func (s *Service) CreateManual(input CreateManualInput) (*fulfillmentdomain.Fulf
if status == "" {
status = constants.OrderStatusDelivered
}
if _, err := orderapp.EnqueueStatusEmailTaskIfEligible(s.orderStore, s.orderQueue, s.settingService, s.defaultEmailConfig, *order.ParentID, status); err != nil {
logger.Warnw("fulfillment_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", *order.ParentID,
"status", status,
"error", err,
)
if status != constants.OrderStatusCanceled {
if _, err := orderapp.EnqueueStatusEmailTaskIfEligible(s.orderStore, s.orderQueue, s.settingService, s.defaultEmailConfig, *order.ParentID, status); err != nil {
logger.Warnw("fulfillment_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", *order.ParentID,
"status", status,
"error", err,
)
}
}
}
} else {
@@ -348,13 +350,15 @@ func (s *Service) CreateAuto(orderID uint) (*fulfillmentdomain.Fulfillment, erro
if status == "" {
status = constants.OrderStatusCompleted
}
if _, err := orderapp.EnqueueStatusEmailTaskIfEligible(s.orderStore, s.orderQueue, s.settingService, s.defaultEmailConfig, *order.ParentID, status); err != nil {
logger.Warnw("fulfillment_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", *order.ParentID,
"status", status,
"error", err,
)
if status != constants.OrderStatusCanceled {
if _, err := orderapp.EnqueueStatusEmailTaskIfEligible(s.orderStore, s.orderQueue, s.settingService, s.defaultEmailConfig, *order.ParentID, status); err != nil {
logger.Warnw("fulfillment_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", *order.ParentID,
"status", status,
"error", err,
)
}
}
}
} else {
@@ -109,8 +109,6 @@ func buildOrderStatusContentFromTemplate(input notificationcontract.OrderStatusE
} else {
sceneTmpl = tmplSetting.Templates.Delivered
}
case constants.OrderStatusCanceled:
sceneTmpl = tmplSetting.Templates.Canceled
case constants.OrderStatusRefunded:
sceneTmpl = tmplSetting.Templates.Refunded
case constants.OrderStatusPartiallyRefunded:
@@ -409,9 +407,6 @@ func buildOrderStatusContent(input notificationcontract.OrderStatusEmailInput, l
case constants.OrderStatusPaid:
body := i18n.Sprintf(normalized, "email.order_status.body_paid", input.OrderNo, statusLabel, amount, currency, siteName, siteURL)
return subject, appendGuestTip(normalized, input, appendFulfillmentAttachmentTip(normalized, input, body))
case constants.OrderStatusCanceled:
body := i18n.Sprintf(normalized, "email.order_status.body_canceled", input.OrderNo, statusLabel, amount, currency, siteName, siteURL)
return subject, appendGuestTip(normalized, input, appendFulfillmentAttachmentTip(normalized, input, body))
case constants.OrderStatusRefunded:
body := i18n.Sprintf(normalized, "email.order_status.body_refunded", input.OrderNo, statusLabel, refundAmount, currency, refundReason, siteName, siteURL)
return subject, appendGuestTip(normalized, input, appendFulfillmentAttachmentTip(normalized, input, body))
@@ -43,19 +43,6 @@ func TestBuildOrderStatusContent(t *testing.T) {
"订单号:DJ-PAID",
},
},
{
name: "canceled_en",
locale: i18n.LocaleEN,
status: "canceled",
wantSubjectContains: []string{
"Order status updated",
"Canceled",
},
wantBodyContains: []string{
"The order has been canceled",
"Order No: DJ-CANCEL",
},
},
{
name: "delivered_with_payload_tw",
locale: i18n.LocaleTW,
@@ -166,8 +153,6 @@ func pickOrderNo(status string) string {
switch status {
case "paid":
return "DJ-PAID"
case "canceled":
return "DJ-CANCEL"
case "refunded":
return "DJ-REFUND"
case "partially_refunded":
@@ -136,16 +136,6 @@ func (s *OrderService) CancelOrder(orderID uint, userID uint) (*orderdomain.Orde
)
}
}
if s.queueClient != nil {
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, order.ID, constants.OrderStatusCanceled); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", order.ID,
"status", constants.OrderStatusCanceled,
"error", err,
)
}
}
FillOrderItemsFromChildren(order)
return order, nil
}
@@ -187,16 +177,6 @@ func (s *OrderService) UpdateOrderStatus(orderID uint, targetStatus string) (*or
)
}
}
if s.queueClient != nil {
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, order.ID, constants.OrderStatusCanceled); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", order.ID,
"status", constants.OrderStatusCanceled,
"error", err,
)
}
}
return order, nil
case constants.OrderStatusCompleted:
if !canCompleteParentOrder(order) {
@@ -330,16 +310,18 @@ func (s *OrderService) UpdateOrderStatus(orderID uint, targetStatus string) (*or
if status == "" {
status = target
}
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, *order.ParentID, status); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", *order.ParentID,
"status", status,
"error", err,
)
if status != constants.OrderStatusCanceled {
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, *order.ParentID, status); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", *order.ParentID,
"status", status,
"error", err,
)
}
}
}
} else if s.queueClient != nil {
} else if s.queueClient != nil && target != constants.OrderStatusCanceled {
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, order.ID, target); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
@@ -445,16 +427,6 @@ func (s *OrderService) CancelExpiredOrder(orderID uint) (*orderdomain.Order, err
)
}
}
if s.queueClient != nil {
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, order.ID, constants.OrderStatusCanceled); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", order.ID,
"status", constants.OrderStatusCanceled,
"error", err,
)
}
}
FillOrderItemsFromChildren(order)
return order, nil
}
@@ -8,7 +8,6 @@ import (
orderdomain "github.com/dujiao-next/internal/modules/order/domain"
"github.com/dujiao-next/internal/constants"
"github.com/dujiao-next/internal/logger"
ordercontract "github.com/dujiao-next/internal/modules/order/contract"
resellercontract "github.com/dujiao-next/internal/modules/reseller/contract"
"github.com/dujiao-next/internal/shared/jsonmap"
@@ -91,16 +90,6 @@ func (s *OrderService) ensureOrderCanceledIfExpired(order *orderdomain.Order) er
if err := s.cancelOrderWithChildren(order, true); err != nil {
return err
}
if s.queueClient != nil {
if _, err := EnqueueStatusEmailTaskIfEligible(s.orderStore, s.queueClient, s.settingService, s.defaultEmailConfig, order.ID, constants.OrderStatusCanceled); err != nil {
logger.Warnw("order_enqueue_status_email_failed",
"order_id", order.ID,
"target_order_id", order.ID,
"status", constants.OrderStatusCanceled,
"error", err,
)
}
}
return nil
}
@@ -47,7 +47,8 @@ import (
)
type fakeOrderTimeoutQueue struct {
enqueued int
enqueued int
statusEmails []string
}
func (q *fakeOrderTimeoutQueue) Enabled() bool {
@@ -59,7 +60,8 @@ func (q *fakeOrderTimeoutQueue) EnqueueTimeoutCancel(_ uint, _ time.Duration) er
return nil
}
func (q *fakeOrderTimeoutQueue) EnqueueStatusEmail(_ uint, _ string) error {
func (q *fakeOrderTimeoutQueue) EnqueueStatusEmail(_ uint, status string) error {
q.statusEmails = append(q.statusEmails, status)
return nil
}
@@ -249,6 +249,7 @@ func TestUpdateOrderStatusAdminCancelExpiresPendingPaymentsSingleOrder(t *testin
now := time.Now()
order := newPendingOrderForCancel("ADMIN-CANCEL-SINGLE-001", 0, nil, now)
order.GuestEmail = "buyer@example.com"
if err := db.Create(order).Error; err != nil {
t.Fatalf("create order failed: %v", err)
}
@@ -258,12 +259,14 @@ func TestUpdateOrderStatusAdminCancelExpiresPendingPaymentsSingleOrder(t *testin
t.Fatalf("create payment failed: %v", err)
}
emailQueue := &fakeOrderTimeoutQueue{}
svc := NewOrderService(OrderServiceOptions{
OrderStore: ordergormstore.New(db, "test-guest-credential-secret-with-32-bytes"),
ProductStore: productgormstore.NewProductStore(db),
ProductSKUStore: productgormstore.NewSKUStore(db),
CouponStore: coupongormstore.New(db),
CouponUsageStore: coupongormstore.NewUsageStore(db),
Queue: emailQueue,
})
updated, err := svc.UpdateOrderStatus(order.ID, constants.OrderStatusCanceled)
if err != nil {
@@ -280,6 +283,9 @@ func TestUpdateOrderStatusAdminCancelExpiresPendingPaymentsSingleOrder(t *testin
if reloaded.Status != constants.PaymentStatusExpired || reloaded.ExpiredAt == nil {
t.Fatalf("payment should expire, got status=%s expired_at=%v", reloaded.Status, reloaded.ExpiredAt)
}
if len(emailQueue.statusEmails) != 0 {
t.Fatalf("canceling an order must not enqueue status email, got %v", emailQueue.statusEmails)
}
}
func TestCancelExpiredOrderExpiresPaymentsForParentAndChildren(t *testing.T) {
@@ -66,7 +66,9 @@ func (s *Service) HandleUpstreamCallback(procurementOrderID uint, upstreamStatus
if status == "" {
status = constants.OrderStatusDelivered
}
_, _ = s.orderLifecycle.EnqueueStatusEmail(*localOrder.ParentID, status)
if status != constants.OrderStatusCanceled {
_, _ = s.orderLifecycle.EnqueueStatusEmail(*localOrder.ParentID, status)
}
}
} else if localOrder != nil && s.orderLifecycle != nil {
_, _ = s.orderLifecycle.EnqueueStatusEmail(localOrder.ID, constants.OrderStatusDelivered)
@@ -44,7 +44,6 @@ type OrderEmailTemplatesSetting struct {
Paid OrderEmailSceneTemplate `json:"paid"`
Delivered OrderEmailSceneTemplate `json:"delivered"`
DeliveredWithContent OrderEmailSceneTemplate `json:"delivered_with_content"`
Canceled OrderEmailSceneTemplate `json:"canceled"`
Refunded OrderEmailSceneTemplate `json:"refunded"`
PartiallyRefunded OrderEmailSceneTemplate `json:"partially_refunded"`
}
@@ -91,7 +90,6 @@ type OrderEmailTemplatesPatch struct {
Paid *OrderEmailSceneTemplatePatch `json:"paid"`
Delivered *OrderEmailSceneTemplatePatch `json:"delivered"`
DeliveredWithContent *OrderEmailSceneTemplatePatch `json:"delivered_with_content"`
Canceled *OrderEmailSceneTemplatePatch `json:"canceled"`
Refunded *OrderEmailSceneTemplatePatch `json:"refunded"`
PartiallyRefunded *OrderEmailSceneTemplatePatch `json:"partially_refunded"`
}
@@ -165,20 +163,6 @@ func DefaultOrderEmailTemplateSetting() OrderEmailTemplateSetting {
Body: "Order No: {{order_no}}\nStatus: {{status}}\nAmount: {{amount}} {{currency}}\n\nDelivery content:\n{{fulfillment_info}}\n\nUsage instructions:\n{{instructions}}\n\nThank you for your purchase.\n\n{{site_name}}'s Site URL: {{site_url}}",
},
},
Canceled: OrderEmailSceneTemplate{
ZHCN: OrderEmailLocalizedTemplate{
Subject: "订单状态更新:{{status}}",
Body: "订单号:{{order_no}}\n状态:{{status}}\n金额:{{amount}} {{currency}}\n\n订单已取消,如有疑问请联系管理员。\n\n{{site_name}} 的网址:{{site_url}}",
},
ZHTW: OrderEmailLocalizedTemplate{
Subject: "訂單狀態更新:{{status}}",
Body: "訂單號:{{order_no}}\n狀態:{{status}}\n金額:{{amount}} {{currency}}\n\n訂單已取消,如有疑問請聯絡管理員。\n\n{{site_name}} 的網址:{{site_url}}",
},
ENUS: OrderEmailLocalizedTemplate{
Subject: "Order status updated: {{status}}",
Body: "Order No: {{order_no}}\nStatus: {{status}}\nAmount: {{amount}} {{currency}}\n\nThe order has been canceled. Please contact admin if needed.\n\n{{site_name}}'s Site URL: {{site_url}}",
},
},
Refunded: OrderEmailSceneTemplate{
ZHCN: OrderEmailLocalizedTemplate{
Subject: "订单状态更新:{{status}}",
@@ -229,7 +213,6 @@ func NormalizeOrderEmailTemplateSetting(setting OrderEmailTemplateSetting) Order
setting.Templates.Paid = normalizeOrderEmailSceneTemplate(setting.Templates.Paid)
setting.Templates.Delivered = normalizeOrderEmailSceneTemplate(setting.Templates.Delivered)
setting.Templates.DeliveredWithContent = normalizeOrderEmailSceneTemplate(setting.Templates.DeliveredWithContent)
setting.Templates.Canceled = normalizeOrderEmailSceneTemplate(setting.Templates.Canceled)
setting.Templates.Refunded = normalizeOrderEmailSceneTemplate(setting.Templates.Refunded)
setting.Templates.PartiallyRefunded = normalizeOrderEmailSceneTemplate(setting.Templates.PartiallyRefunded)
setting.GuestTip.ZHCN = strings.TrimSpace(setting.GuestTip.ZHCN)
@@ -258,7 +241,6 @@ func ValidateOrderEmailTemplateSetting(setting OrderEmailTemplateSetting) error
setting.Templates.Paid,
setting.Templates.Delivered,
setting.Templates.DeliveredWithContent,
setting.Templates.Canceled,
setting.Templates.Refunded,
setting.Templates.PartiallyRefunded,
}
@@ -284,7 +266,6 @@ func EncodeOrderEmailTemplateSetting(setting OrderEmailTemplateSetting) jsonmap.
"paid": orderEmailSceneTemplateToMap(normalized.Templates.Paid),
"delivered": orderEmailSceneTemplateToMap(normalized.Templates.Delivered),
"delivered_with_content": orderEmailSceneTemplateToMap(normalized.Templates.DeliveredWithContent),
"canceled": orderEmailSceneTemplateToMap(normalized.Templates.Canceled),
"refunded": orderEmailSceneTemplateToMap(normalized.Templates.Refunded),
"partially_refunded": orderEmailSceneTemplateToMap(normalized.Templates.PartiallyRefunded),
},
@@ -339,9 +320,6 @@ func ApplyOrderEmailTemplateSettingPatch(current OrderEmailTemplateSetting, patc
if patch.Templates.DeliveredWithContent != nil {
applyOrderEmailSceneTemplatePatch(&next.Templates.DeliveredWithContent, patch.Templates.DeliveredWithContent)
}
if patch.Templates.Canceled != nil {
applyOrderEmailSceneTemplatePatch(&next.Templates.Canceled, patch.Templates.Canceled)
}
if patch.Templates.Refunded != nil {
applyOrderEmailSceneTemplatePatch(&next.Templates.Refunded, patch.Templates.Refunded)
}
@@ -437,9 +415,6 @@ func DecodeOrderEmailTemplateSetting(raw jsonmap.JSON, fallback OrderEmailTempla
if sceneMap := settingsvalue.ToStringAnyMap(templatesMap["delivered_with_content"]); sceneMap != nil {
next.Templates.DeliveredWithContent = orderEmailSceneTemplateFromMap(sceneMap, next.Templates.DeliveredWithContent)
}
if sceneMap := settingsvalue.ToStringAnyMap(templatesMap["canceled"]); sceneMap != nil {
next.Templates.Canceled = orderEmailSceneTemplateFromMap(sceneMap, next.Templates.Canceled)
}
if sceneMap := settingsvalue.ToStringAnyMap(templatesMap["refunded"]); sceneMap != nil {
next.Templates.Refunded = orderEmailSceneTemplateFromMap(sceneMap, next.Templates.Refunded)
}
@@ -0,0 +1,27 @@
package settingsmessaging
import "testing"
func TestOrderEmailTemplateSettingDoesNotExposeCanceledScene(t *testing.T) {
encoded := EncodeOrderEmailTemplateSetting(DefaultOrderEmailTemplateSetting())
templates, ok := encoded["templates"].(map[string]interface{})
if !ok {
t.Fatalf("expected encoded templates map, got %T", encoded["templates"])
}
if _, exists := templates["canceled"]; exists {
t.Fatalf("canceled email scene must not be exposed")
}
// 旧版本保存的 canceled 字段应被安全忽略,并在下一次序列化时自然清除。
templates["canceled"] = map[string]interface{}{
"zh-CN": map[string]interface{}{"subject": "legacy", "body": "legacy"},
}
roundTrip := EncodeOrderEmailTemplateSetting(DecodeOrderEmailTemplateSetting(encoded, DefaultOrderEmailTemplateSetting()))
roundTripTemplates, ok := roundTrip["templates"].(map[string]interface{})
if !ok {
t.Fatalf("expected round-trip templates map, got %T", roundTrip["templates"])
}
if _, exists := roundTripTemplates["canceled"]; exists {
t.Fatalf("legacy canceled email scene must be discarded")
}
}