feat: 去掉订单超时/取消等邮件通知,减少打扰
This commit is contained in:
1 parent
7d6946959a
commit
35c5fece51
18 files changed
+143
-141
No files matched your search
@@ -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
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user