Compare commits
3 Commits
cd206d6baa
...
047ffd7b16
| Author | SHA1 | Date | |
|---|---|---|---|
| 047ffd7b16 | |||
| bca1d8afea | |||
| 0418370c31 |
@@ -121,10 +121,10 @@ export default function PromoCodesPage() {
|
|||||||
[filters],
|
[filters],
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function loadScenes() {
|
async function loadScenes() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ export default function PromoCodesPage() {
|
|||||||
<Space size="small" wrap>
|
<Space size="small" wrap>
|
||||||
|
|
||||||
<Button type="link" size="small" onClick={() => navigate(`/promo-codes/${row.id}`)}>
|
<Button type="link" size="small" onClick={() => navigate(`/promo-codes/${row.id}`)}>
|
||||||
|
|
||||||
详情
|
详情
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -74,25 +74,28 @@ export default function PromoCodeDetailPage() {
|
|||||||
<>
|
<>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Card title="推广二维码" size="small">
|
<Card title="小程序码" size="small">
|
||||||
{detail.qrcodeUrl ? (
|
{detail.qrcodeUrl ? (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<img
|
<img
|
||||||
src={detail.qrcodeUrl}
|
src={detail.qrcodeUrl}
|
||||||
alt="推广二维码"
|
alt="推广小程序码"
|
||||||
style={{ width: 200, height: 200, marginBottom: 12 }}
|
style={{ width: 200, height: 200, marginBottom: 12 }}
|
||||||
/>
|
/>
|
||||||
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 12, fontSize: 12 }}>
|
||||||
|
微信扫码进入小程序(scene=活动 ID {detail.id})
|
||||||
|
</Typography.Paragraph>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
onClick={() => void downloadQrcode(detail.qrcodeUrl!, `${detail.code}-qrcode.png`)}
|
onClick={() => void downloadQrcode(detail.qrcodeUrl!, `${detail.code}-wxacode.png`)}
|
||||||
>
|
>
|
||||||
下载二维码
|
下载小程序码
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text type="secondary">暂无二维码</Typography.Text>
|
<Typography.Text type="secondary">暂无小程序码</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -144,13 +147,18 @@ export default function PromoCodeDetailPage() {
|
|||||||
<Descriptions.Item label="状态">
|
<Descriptions.Item label="状态">
|
||||||
{PROMO_CODE_STATUS_LABELS[detail.status] || detail.status}
|
{PROMO_CODE_STATUS_LABELS[detail.status] || detail.status}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="活动 ID" span={2}>
|
||||||
|
<Typography.Text copyable={{ text: String(detail.id) }}>
|
||||||
|
{detail.id}(小程序码 scene)
|
||||||
|
</Typography.Text>
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="二维码 ID" span={2}>
|
<Descriptions.Item label="二维码 ID" span={2}>
|
||||||
<Typography.Text copyable={{ text: detail.qrcodeId }}>{detail.qrcodeId}</Typography.Text>
|
<Typography.Text copyable={{ text: detail.qrcodeId }}>{detail.qrcodeId}</Typography.Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="落地链接" span={2}>
|
<Descriptions.Item label="H5 落地链接" span={2}>
|
||||||
<Typography.Text copyable={{ text: detail.landingUrl }}>{detail.landingUrl}</Typography.Text>
|
<Typography.Text copyable={{ text: detail.landingUrl }}>{detail.landingUrl}</Typography.Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="OSS 地址" span={2}>
|
<Descriptions.Item label="小程序码 OSS" span={2}>
|
||||||
{detail.qrcodeUrl ? (
|
{detail.qrcodeUrl ? (
|
||||||
<Typography.Text copyable={{ text: detail.qrcodeUrl }} ellipsis>
|
<Typography.Text copyable={{ text: detail.qrcodeUrl }} ellipsis>
|
||||||
{detail.qrcodeUrl}
|
{detail.qrcodeUrl}
|
||||||
|
|||||||
@@ -47,13 +47,25 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
PENDING_SHIP: '待发货',
|
PENDING_SHIP: '待发货',
|
||||||
OUT_WAREHOUSE: '出库中',
|
OUT_WAREHOUSE: '出库中',
|
||||||
SHIPPING: '配送中',
|
SHIPPING: '配送中',
|
||||||
|
SHIPPED: '配送中',
|
||||||
PENDING_RECEIVE: '待签收',
|
PENDING_RECEIVE: '待签收',
|
||||||
|
DELIVERED: '待签收',
|
||||||
COMPLETED: '已完成',
|
COMPLETED: '已完成',
|
||||||
CANCELLED: '已取消',
|
CANCELLED: '已取消',
|
||||||
REFUNDING: '退款中',
|
REFUNDING: '退款中',
|
||||||
REFUNDED: '已退款',
|
REFUNDED: '已退款',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 已付款未完成:可选现场取货并确认收货 */
|
||||||
|
const ON_SITE_PICKUP_STATUSES = new Set([
|
||||||
|
'PENDING_SHIP',
|
||||||
|
'OUT_WAREHOUSE',
|
||||||
|
'SHIPPING',
|
||||||
|
'SHIPPED',
|
||||||
|
'PENDING_RECEIVE',
|
||||||
|
'DELIVERED',
|
||||||
|
]);
|
||||||
|
|
||||||
function fullReceiverAddress(order: OrderDetail) {
|
function fullReceiverAddress(order: OrderDetail) {
|
||||||
const detail = (order.receiverAddress || '').trim();
|
const detail = (order.receiverAddress || '').trim();
|
||||||
const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
|
const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
|
||||||
@@ -68,6 +80,8 @@ export default function OrderDetailPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const orderId = router.params.id ?? '';
|
const orderId = router.params.id ?? '';
|
||||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||||
|
const [onSitePickup, setOnSitePickup] = useState(false);
|
||||||
|
const [confirming, setConfirming] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orderId) return;
|
if (!orderId) return;
|
||||||
@@ -76,7 +90,16 @@ export default function OrderDetailPage() {
|
|||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, [orderId]);
|
}, [orderId]);
|
||||||
|
|
||||||
const canPay = !!order && order.status === 'PENDING_PAY' && !order.originOrderId;
|
const isReship = !!order?.originOrderId;
|
||||||
|
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
|
||||||
|
const canOnSitePickup =
|
||||||
|
!!order && !isReship && ON_SITE_PICKUP_STATUSES.has(order.status || '');
|
||||||
|
const canConfirmReceive =
|
||||||
|
!!order &&
|
||||||
|
!isReship &&
|
||||||
|
(['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '') ||
|
||||||
|
(onSitePickup && canOnSitePickup));
|
||||||
|
|
||||||
const item = order?.items?.[0];
|
const item = order?.items?.[0];
|
||||||
const productName = item?.productName || order?.productName || '杜康商品';
|
const productName = item?.productName || order?.productName || '杜康商品';
|
||||||
const quantity = item?.quantity ?? order?.quantity ?? order?.qty ?? 1;
|
const quantity = item?.quantity ?? order?.quantity ?? order?.qty ?? 1;
|
||||||
@@ -111,10 +134,41 @@ export default function OrderDetailPage() {
|
|||||||
Taro.navigateTo({ url: '/pages/customer-service/index' });
|
Taro.navigateTo({ url: '/pages/customer-service/index' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmReceive() {
|
||||||
|
if (!order || !canConfirmReceive || confirming) return;
|
||||||
|
|
||||||
|
const useOnSite =
|
||||||
|
onSitePickup || !['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
||||||
|
const { confirm } = await Taro.showModal({
|
||||||
|
title: useOnSite ? '确认现场取货?' : '确认收货?',
|
||||||
|
content: useOnSite
|
||||||
|
? '请确认您已在现场拿到商品。确认后订单将完成,好客权益即时可用,无法再安排配送。若尚未取到酒,请勿确认。'
|
||||||
|
: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
|
||||||
|
confirmText: '确认收货',
|
||||||
|
cancelText: '再想想',
|
||||||
|
});
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
setConfirming(true);
|
||||||
|
try {
|
||||||
|
const updated = await request<OrderDetail>(`/trade/orders/${order.id}/confirm-receive`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: { onSitePickup: useOnSite },
|
||||||
|
});
|
||||||
|
setOrder(updated);
|
||||||
|
setOnSitePickup(false);
|
||||||
|
toast(useOnSite ? '现场取货已确认,订单完成' : '已确认收货');
|
||||||
|
} catch (e) {
|
||||||
|
toast(e instanceof Error ? e.message : '确认收货失败');
|
||||||
|
} finally {
|
||||||
|
setConfirming(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pageClass = [
|
const pageClass = [
|
||||||
'order-detail-page',
|
'order-detail-page',
|
||||||
order ? 'order-detail-page--with-actions' : '',
|
order ? 'order-detail-page--with-actions' : '',
|
||||||
canPay ? 'order-detail-page--with-pay' : '',
|
canPay || canConfirmReceive ? 'order-detail-page--with-pay' : '',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
@@ -171,6 +225,27 @@ export default function OrderDetailPage() {
|
|||||||
<Text className="u-muted">地址信息待完善</Text>
|
<Text className="u-muted">地址信息待完善</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
{canOnSitePickup ? (
|
||||||
|
<View className="order-card">
|
||||||
|
<Text className="order-card-title">取货方式</Text>
|
||||||
|
<View
|
||||||
|
className="order-pickup-option"
|
||||||
|
onClick={() => setOnSitePickup((v) => !v)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className={`order-pickup-check${onSitePickup ? ' order-pickup-check--on' : ''}`}
|
||||||
|
>
|
||||||
|
{onSitePickup ? <Text className="order-pickup-check-mark">✓</Text> : null}
|
||||||
|
</View>
|
||||||
|
<View className="order-pickup-copy">
|
||||||
|
<Text className="order-pickup-title">现场取货</Text>
|
||||||
|
<Text className="order-pickup-desc">
|
||||||
|
已在活动现场或门店拿到商品时勾选,确认后订单直接完成
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<View className="order-card">
|
<View className="order-card">
|
||||||
<Text className="order-card-title">订单信息</Text>
|
<Text className="order-card-title">订单信息</Text>
|
||||||
<View className="order-row">
|
<View className="order-row">
|
||||||
@@ -189,7 +264,11 @@ export default function OrderDetailPage() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{order ? (
|
{order ? (
|
||||||
<View className={`order-detail-actionbar${canPay ? ' order-detail-actionbar--with-pay' : ''}`}>
|
<View
|
||||||
|
className={`order-detail-actionbar${
|
||||||
|
canPay || canConfirmReceive ? ' order-detail-actionbar--with-pay' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{isWeapp ? (
|
{isWeapp ? (
|
||||||
<ContactCsButton
|
<ContactCsButton
|
||||||
className="order-detail-cs-btn"
|
className="order-detail-cs-btn"
|
||||||
@@ -219,6 +298,14 @@ export default function OrderDetailPage() {
|
|||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canConfirmReceive ? (
|
||||||
|
<View
|
||||||
|
className={`order-confirm-submit${confirming ? ' order-confirm-submit--disabled' : ''}`}
|
||||||
|
onClick={confirming ? undefined : () => void confirmReceive()}
|
||||||
|
>
|
||||||
|
{confirming ? '提交中…' : onSitePickup ? '确认现场取货' : '确认收货'}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</PageShell>
|
</PageShell>
|
||||||
|
|||||||
@@ -17,17 +17,29 @@ function formatMoney(amount: number) {
|
|||||||
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 核销金额输入:最多两位小数,禁止非法字符 */
|
/** 核销金额输入:最多两位小数;去掉前导 0;禁止非法字符 */
|
||||||
function sanitizeRedeemAmountInput(raw: string): string {
|
function sanitizeRedeemAmountInput(raw: string): string {
|
||||||
let next = raw.replace(/[^\d.]/g, '');
|
let next = String(raw ?? '').replace(/[^\d.]/g, '');
|
||||||
|
if (!next) return '';
|
||||||
|
|
||||||
const firstDot = next.indexOf('.');
|
const firstDot = next.indexOf('.');
|
||||||
if (firstDot >= 0) {
|
if (firstDot >= 0) {
|
||||||
next =
|
const intRaw = next.slice(0, firstDot).replace(/\D/g, '');
|
||||||
next.slice(0, firstDot + 1) + next.slice(firstDot + 1).replace(/\./g, '');
|
const decRaw = next
|
||||||
const [intPart, decPart = ''] = next.split('.');
|
.slice(firstDot + 1)
|
||||||
next = `${intPart}.${decPart.slice(0, 2)}`;
|
.replace(/\D/g, '')
|
||||||
|
.replace(/\./g, '')
|
||||||
|
.slice(0, 2);
|
||||||
|
const intPart = intRaw.replace(/^0+(?=\d)/, '') || '0';
|
||||||
|
// 正在输入小数点或小数位时保留点
|
||||||
|
if (decRaw.length > 0 || next.endsWith('.')) {
|
||||||
|
return `${intPart}.${decRaw}`;
|
||||||
|
}
|
||||||
|
return intPart;
|
||||||
}
|
}
|
||||||
if (next.startsWith('.')) next = `0${next}`;
|
|
||||||
|
// 纯整数:忽略前导 0(保留单个 0)
|
||||||
|
next = next.replace(/^0+(?=\d)/, '');
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +49,7 @@ export default function RedeemPage() {
|
|||||||
const initialAmount = router.params.amount ?? '';
|
const initialAmount = router.params.amount ?? '';
|
||||||
const [balance, setBalance] = useState(0);
|
const [balance, setBalance] = useState(0);
|
||||||
const [couponBalance, setCouponBalance] = useState<number | null>(null);
|
const [couponBalance, setCouponBalance] = useState<number | null>(null);
|
||||||
const [amount, setAmount] = useState(initialAmount);
|
const [amount, setAmount] = useState(() => sanitizeRedeemAmountInput(initialAmount));
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const redeemableMax = couponId ? (couponBalance ?? 0) : balance;
|
const redeemableMax = couponId ? (couponBalance ?? 0) : balance;
|
||||||
@@ -67,7 +79,11 @@ export default function RedeemPage() {
|
|||||||
|
|
||||||
function fillMaxAmount() {
|
function fillMaxAmount() {
|
||||||
if (redeemableMax < MIN_REDEEM_AMOUNT) return;
|
if (redeemableMax < MIN_REDEEM_AMOUNT) return;
|
||||||
setAmount(String(redeemableMax));
|
setAmount(sanitizeRedeemAmountInput(redeemableMax.toFixed(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAmountChange(raw: string) {
|
||||||
|
setAmount(sanitizeRedeemAmountInput(raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -119,7 +135,8 @@ export default function RedeemPage() {
|
|||||||
placeholder="输入核销金额"
|
placeholder="输入核销金额"
|
||||||
placeholderClass="redeem-input-placeholder"
|
placeholderClass="redeem-input-placeholder"
|
||||||
value={amount}
|
value={amount}
|
||||||
onInput={(e) => setAmount(sanitizeRedeemAmountInput(e.detail.value))}
|
onInput={(e) => onAmountChange(e.detail.value)}
|
||||||
|
onBlur={(e) => onAmountChange(e.detail.value)}
|
||||||
style={{ textAlign: 'center' }}
|
style={{ textAlign: 'center' }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -68,6 +68,63 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-confirm-submit--disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-check {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1.5px solid var(--color-outline, #c8c4be);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-check--on {
|
||||||
|
border-color: var(--color-primary, #8b1a1a);
|
||||||
|
background: var(--color-primary, #8b1a1a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-check-mark {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-copy {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-pickup-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-on-surface-variant, #78716c);
|
||||||
|
}
|
||||||
|
|
||||||
.order-card {
|
.order-card {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ WX_APP_SECRET=
|
|||||||
# 未配置时回退 WX_APP_ID,若与小程序 appid 不同会导致 invalid code
|
# 未配置时回退 WX_APP_ID,若与小程序 appid 不同会导致 invalid code
|
||||||
WX_MINI_APP_ID=
|
WX_MINI_APP_ID=
|
||||||
WX_MINI_APP_SECRET=
|
WX_MINI_APP_SECRET=
|
||||||
|
# 推广码小程序码落地页(getwxacodeunlimit 的 page;勿前导 /)
|
||||||
|
WX_MINI_PROMO_PAGE=pages/home/index
|
||||||
|
# 小程序码打开版本:release | trial | develop(默认 release)
|
||||||
|
# WX_MINI_ENV_VERSION=release
|
||||||
WX_MCH_ID=
|
WX_MCH_ID=
|
||||||
WX_MCH_SERIAL_NO=
|
WX_MCH_SERIAL_NO=
|
||||||
WX_MCH_PRIVATE_KEY=
|
WX_MCH_PRIVATE_KEY=
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ USER_H5_URL=https://user.runxian.top/user
|
|||||||
MOCK_WECHAT=false
|
MOCK_WECHAT=false
|
||||||
WX_APP_ID=
|
WX_APP_ID=
|
||||||
WX_APP_SECRET=
|
WX_APP_SECRET=
|
||||||
|
WX_MINI_APP_ID=
|
||||||
|
WX_MINI_APP_SECRET=
|
||||||
|
WX_MINI_PROMO_PAGE=pages/home/index
|
||||||
|
# WX_MINI_ENV_VERSION=release
|
||||||
WX_MCH_ID=
|
WX_MCH_ID=
|
||||||
WX_MCH_SERIAL_NO=
|
WX_MCH_SERIAL_NO=
|
||||||
WX_MCH_PRIVATE_KEY=
|
WX_MCH_PRIVATE_KEY=
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { BadRequestException, Injectable, InternalServerErrorException, Logger }
|
|||||||
import { loadAppConfig } from '@dukang/shared-types';
|
import { loadAppConfig } from '@dukang/shared-types';
|
||||||
import { RedisService } from '../../common/redis/redis.service';
|
import { RedisService } from '../../common/redis/redis.service';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
import type { IWechatProvider, WechatCodeSession, WechatOAuthSession } from './wechat.interface';
|
import type {
|
||||||
|
IWechatProvider,
|
||||||
|
WechatCodeSession,
|
||||||
|
WechatOAuthSession,
|
||||||
|
WechatWxaCodeUnlimitedInput,
|
||||||
|
} from './wechat.interface';
|
||||||
import { logWechatAuth, type WechatActorRef } from './wechat-log.util';
|
import { logWechatAuth, type WechatActorRef } from './wechat-log.util';
|
||||||
import {
|
import {
|
||||||
decryptPayResource,
|
decryptPayResource,
|
||||||
@@ -252,6 +257,52 @@ export class WechatApiProvider implements IWechatProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||||
|
const scene = (input.scene ?? '').trim();
|
||||||
|
if (!scene || scene.length > 32) {
|
||||||
|
throw new BadRequestException('小程序码 scene 须为 1~32 个可见字符');
|
||||||
|
}
|
||||||
|
const accessToken = await this.getMiniAccessToken();
|
||||||
|
const page = (input.page ?? process.env.WX_MINI_PROMO_PAGE ?? 'pages/home/index').replace(
|
||||||
|
/^\//,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
const envFromCfg = process.env.WX_MINI_ENV_VERSION;
|
||||||
|
const envVersion: 'release' | 'trial' | 'develop' =
|
||||||
|
input.envVersion ??
|
||||||
|
(envFromCfg === 'trial' || envFromCfg === 'develop' || envFromCfg === 'release'
|
||||||
|
? envFromCfg
|
||||||
|
: 'release');
|
||||||
|
const body = {
|
||||||
|
scene,
|
||||||
|
page,
|
||||||
|
width: input.width ?? 430,
|
||||||
|
check_path: input.checkPath ?? false,
|
||||||
|
env_version: envVersion,
|
||||||
|
is_hyaline: input.isHyaline ?? false,
|
||||||
|
};
|
||||||
|
const apiUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`;
|
||||||
|
const res = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
// 失败时微信返回 JSON(以 { 开头),成功为 PNG 二进制
|
||||||
|
if (buf.length >= 1 && buf[0] === 0x7b /* '{' */) {
|
||||||
|
let errMsg = '生成小程序码失败';
|
||||||
|
try {
|
||||||
|
const err = JSON.parse(buf.toString('utf8')) as { errcode?: number; errmsg?: string };
|
||||||
|
errMsg = err.errmsg || errMsg;
|
||||||
|
this.logger.error(`getwxacodeunlimit failed: ${err.errcode} ${err.errmsg}`);
|
||||||
|
} catch {
|
||||||
|
this.logger.error(`getwxacodeunlimit non-image response: ${buf.toString('utf8').slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException(errMsg);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
async getPhoneNumberByCode(code: string, platform: 'mini' | 'h5', actorRef?: WechatActorRef): Promise<string> {
|
async getPhoneNumberByCode(code: string, platform: 'mini' | 'h5', actorRef?: WechatActorRef): Promise<string> {
|
||||||
if (platform === 'h5') {
|
if (platform === 'h5') {
|
||||||
throw new InternalServerErrorException('H5 请使用短信绑定手机号');
|
throw new InternalServerErrorException('H5 请使用短信绑定手机号');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, NotImplementedException } from '@nestjs/common';
|
import { Injectable, NotImplementedException } from '@nestjs/common';
|
||||||
import type { IWechatProvider } from './wechat.interface';
|
import type { IWechatProvider, WechatWxaCodeUnlimitedInput } from './wechat.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WechatDisabledProvider implements IWechatProvider {
|
export class WechatDisabledProvider implements IWechatProvider {
|
||||||
@@ -54,4 +54,8 @@ export class WechatDisabledProvider implements IWechatProvider {
|
|||||||
parsePayNotification() {
|
parsePayNotification() {
|
||||||
return this.disabled();
|
return this.disabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWxaCodeUnlimited(_input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||||
|
return this.disabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ export type WechatPayNotifyResult = {
|
|||||||
amountFen: number;
|
amountFen: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WechatWxaCodeUnlimitedInput = {
|
||||||
|
/** 最大 32 可见字符,扫码后小程序 onLaunch.options.scene */
|
||||||
|
scene: string;
|
||||||
|
/** 小程序页面路径,如 pages/home/index(不要前导 /) */
|
||||||
|
page?: string;
|
||||||
|
width?: number;
|
||||||
|
checkPath?: boolean;
|
||||||
|
envVersion?: 'release' | 'trial' | 'develop';
|
||||||
|
isHyaline?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IWechatProvider {
|
export interface IWechatProvider {
|
||||||
isEnabled(): boolean;
|
isEnabled(): boolean;
|
||||||
|
|
||||||
@@ -81,4 +92,7 @@ export interface IWechatProvider {
|
|||||||
headers: Record<string, string | string[] | undefined>,
|
headers: Record<string, string | string[] | undefined>,
|
||||||
rawBody: string,
|
rawBody: string,
|
||||||
): Promise<WechatPayNotifyResult>;
|
): Promise<WechatPayNotifyResult>;
|
||||||
|
|
||||||
|
/** 获取不限制的小程序码(PNG Buffer),须服务端调用 */
|
||||||
|
getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { Injectable, NotImplementedException } from '@nestjs/common';
|
import { Injectable, NotImplementedException } from '@nestjs/common';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
import type {
|
import type {
|
||||||
IWechatProvider,
|
IWechatProvider,
|
||||||
WechatCodeSession,
|
WechatCodeSession,
|
||||||
WechatOAuthSession,
|
WechatOAuthSession,
|
||||||
|
WechatWxaCodeUnlimitedInput,
|
||||||
} from './wechat.interface';
|
} from './wechat.interface';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,4 +83,15 @@ export class WechatMockProvider implements IWechatProvider {
|
|||||||
parsePayNotification(): never {
|
parsePayNotification(): never {
|
||||||
throw new NotImplementedException('FEATURE_DISABLED');
|
throw new NotImplementedException('FEATURE_DISABLED');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mock:用普通二维码 PNG 占位,内容含 scene,便于本地联调上传 OSS */
|
||||||
|
async getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||||
|
const scene = (input.scene ?? '').trim() || 'mock';
|
||||||
|
return QRCode.toBuffer(`mock-wxa://promo?scene=${encodeURIComponent(scene)}`, {
|
||||||
|
width: input.width ?? 430,
|
||||||
|
margin: 1,
|
||||||
|
type: 'png',
|
||||||
|
color: { dark: '#1f1a17', light: '#ffffff' },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,4 +86,8 @@ export class WechatRouterProvider implements IWechatProvider {
|
|||||||
) {
|
) {
|
||||||
return this.resolve().parsePayNotification(headers, rawBody);
|
return this.resolve().parsePayNotification(headers, rawBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWxaCodeUnlimited(input: Parameters<IWechatProvider['getWxaCodeUnlimited']>[0]) {
|
||||||
|
return this.resolve().getWxaCodeUnlimited(input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class PromoController {
|
|||||||
touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
||||||
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
||||||
return this.promoCodeService.touch(
|
return this.promoCodeService.touch(
|
||||||
{ promoCode: dto.promoCode, qrcodeId: dto.qrcodeId },
|
{ promoCode: dto.promoCode, qrcodeId: dto.qrcodeId, promoId: dto.promoId },
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class PromoTouchDto {
|
export class PromoTouchDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -8,4 +8,9 @@ export class PromoTouchDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
qrcodeId?: string;
|
qrcodeId?: string;
|
||||||
|
|
||||||
|
/** 小程序码 scene 中的推广活动 ID */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
promoId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import * as QRCode from 'qrcode';
|
|
||||||
import {
|
import {
|
||||||
PROMO_CODE_SCENE_LABELS,
|
PROMO_CODE_SCENE_LABELS,
|
||||||
PromoCodeScene,
|
PromoCodeScene,
|
||||||
@@ -14,8 +13,9 @@ import {
|
|||||||
} from '@dukang/shared-types';
|
} from '@dukang/shared-types';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
import { OSS_PROVIDER } from '../../integrations/integrations.constants';
|
import { OSS_PROVIDER, WECHAT_PROVIDER } from '../../integrations/integrations.constants';
|
||||||
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
import type { IOssProvider } from '../../integrations/oss/oss.interface';
|
||||||
|
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||||||
import type {
|
import type {
|
||||||
CreatePromoCodeDto,
|
CreatePromoCodeDto,
|
||||||
PromoCodeListQueryDto,
|
PromoCodeListQueryDto,
|
||||||
@@ -70,6 +70,7 @@ export class PromoCodeService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
@Inject(OSS_PROVIDER) private readonly oss: IOssProvider,
|
||||||
|
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
listScenes() {
|
listScenes() {
|
||||||
@@ -231,18 +232,25 @@ export class PromoCodeService {
|
|||||||
throw new BadRequestException('生成二维码 ID 失败,请重试');
|
throw new BadRequestException('生成二维码 ID 失败,请重试');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createQrcodeResource(promoId: bigint, code: string, qrcodeId: string) {
|
/**
|
||||||
const landingUrl = buildLandingUrl(code, qrcodeId);
|
* 调用微信 getwxacodeunlimit,scene=推广活动 ID,PNG 上传 OSS uploads/qrcode/
|
||||||
const pngBuffer = await QRCode.toBuffer(landingUrl, {
|
*/
|
||||||
width: 512,
|
private async createQrcodeResource(promoId: bigint, code: string) {
|
||||||
margin: 1,
|
const scene = promoId.toString();
|
||||||
type: 'png',
|
if (scene.length > 32) {
|
||||||
color: { dark: '#1f1a17', light: '#ffffff' },
|
throw new BadRequestException('推广活动 ID 过长,无法写入小程序码 scene');
|
||||||
|
}
|
||||||
|
const pngBuffer = await this.wechat.getWxaCodeUnlimited({
|
||||||
|
scene,
|
||||||
|
page: process.env.WX_MINI_PROMO_PAGE || 'pages/home/index',
|
||||||
|
width: 430,
|
||||||
|
checkPath: false,
|
||||||
});
|
});
|
||||||
|
const fileName = `promo-${code}-${scene}.png`;
|
||||||
const uploaded = await this.oss.putObject({
|
const uploaded = await this.oss.putObject({
|
||||||
bizType: 'QRCODE',
|
bizType: 'QRCODE',
|
||||||
mediaType: 'IMAGE',
|
mediaType: 'IMAGE',
|
||||||
fileName: `promo-${code}.png`,
|
fileName,
|
||||||
buffer: pngBuffer,
|
buffer: pngBuffer,
|
||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
});
|
});
|
||||||
@@ -255,7 +263,7 @@ export class PromoCodeService {
|
|||||||
ossBucket: uploaded.bucket,
|
ossBucket: uploaded.bucket,
|
||||||
ossKey: uploaded.ossKey,
|
ossKey: uploaded.ossKey,
|
||||||
url: uploaded.url,
|
url: uploaded.url,
|
||||||
fileName: `promo-${code}.png`,
|
fileName,
|
||||||
fileSize: BigInt(pngBuffer.length),
|
fileSize: BigInt(pngBuffer.length),
|
||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
},
|
},
|
||||||
@@ -281,13 +289,18 @@ export class PromoCodeService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resource = await this.createQrcodeResource(row.id, code, qrcodeId);
|
try {
|
||||||
const updated = await this.prisma.commonPromoCode.update({
|
const resource = await this.createQrcodeResource(row.id, code);
|
||||||
where: { id: row.id },
|
const updated = await this.prisma.commonPromoCode.update({
|
||||||
data: { qrcodeResourceId: resource.id },
|
where: { id: row.id },
|
||||||
include: this.includeRelations,
|
data: { qrcodeResourceId: resource.id },
|
||||||
});
|
include: this.includeRelations,
|
||||||
return this.mapRow(updated);
|
});
|
||||||
|
return this.mapRow(updated);
|
||||||
|
} catch (err) {
|
||||||
|
await this.prisma.commonPromoCode.delete({ where: { id: row.id } }).catch(() => undefined);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: bigint, dto: UpdatePromoCodeDto) {
|
async update(id: bigint, dto: UpdatePromoCodeDto) {
|
||||||
@@ -349,9 +362,13 @@ export class PromoCodeService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByCodeOrQrcodeId(input: { code?: string; qrcodeId?: string }) {
|
async findByCodeOrQrcodeId(input: { code?: string; qrcodeId?: string; promoId?: string }) {
|
||||||
const code = input.code?.trim().toUpperCase();
|
const code = input.code?.trim().toUpperCase();
|
||||||
const qrcodeId = input.qrcodeId?.trim();
|
const qrcodeId = input.qrcodeId?.trim();
|
||||||
|
const promoId = input.promoId?.trim();
|
||||||
|
if (promoId && /^\d+$/.test(promoId)) {
|
||||||
|
return this.prisma.commonPromoCode.findUnique({ where: { id: BigInt(promoId) } });
|
||||||
|
}
|
||||||
if (code) {
|
if (code) {
|
||||||
return this.prisma.commonPromoCode.findUnique({ where: { code } });
|
return this.prisma.commonPromoCode.findUnique({ where: { code } });
|
||||||
}
|
}
|
||||||
@@ -362,14 +379,18 @@ export class PromoCodeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** C 端扫码/带参进入:累加 scan_count、归因、标记用户来源 */
|
/** C 端扫码/带参进入:累加 scan_count、归因、标记用户来源 */
|
||||||
async touch(input: { promoCode?: string; qrcodeId?: string }, userId?: bigint) {
|
async touch(
|
||||||
|
input: { promoCode?: string; qrcodeId?: string; promoId?: string },
|
||||||
|
userId?: bigint,
|
||||||
|
) {
|
||||||
const promoCode = input.promoCode?.trim().toUpperCase();
|
const promoCode = input.promoCode?.trim().toUpperCase();
|
||||||
const qrcodeId = input.qrcodeId?.trim();
|
const qrcodeId = input.qrcodeId?.trim();
|
||||||
if (!promoCode && !qrcodeId) {
|
const promoId = input.promoId?.trim();
|
||||||
throw new BadRequestException('请提供 promoCode 或 qrcodeId');
|
if (!promoCode && !qrcodeId && !promoId) {
|
||||||
|
throw new BadRequestException('请提供 promoId、promoCode 或 qrcodeId');
|
||||||
}
|
}
|
||||||
|
|
||||||
const promo = await this.findByCodeOrQrcodeId({ code: promoCode, qrcodeId });
|
const promo = await this.findByCodeOrQrcodeId({ code: promoCode, qrcodeId, promoId });
|
||||||
if (!promo || promo.status !== 'ACTIVE') {
|
if (!promo || promo.status !== 'ACTIVE') {
|
||||||
throw new NotFoundException('推广码无效或已停用');
|
throw new NotFoundException('推广码无效或已停用');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,14 @@ export class TradeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/confirm-receive')
|
@Post(':id/confirm-receive')
|
||||||
confirmReceive(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
confirmReceive(
|
||||||
return this.tradeService.confirmReceive(user.actorId, BigInt(id));
|
@CurrentUser() user: AuthUser,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body?: { onSitePickup?: boolean },
|
||||||
|
) {
|
||||||
|
return this.tradeService.confirmReceive(user.actorId, BigInt(id), {
|
||||||
|
onSitePickup: !!body?.onSitePickup,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/refund-requests')
|
@Post(':id/refund-requests')
|
||||||
|
|||||||
@@ -467,13 +467,38 @@ export class TradeService {
|
|||||||
return serializeBigInt(updated);
|
return serializeBigInt(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmReceive(userId: bigint, orderId: bigint) {
|
async confirmReceive(
|
||||||
|
userId: bigint,
|
||||||
|
orderId: bigint,
|
||||||
|
opts?: { onSitePickup?: boolean },
|
||||||
|
) {
|
||||||
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
|
||||||
if (!order) throw new NotFoundException('订单不存在');
|
if (!order) throw new NotFoundException('订单不存在');
|
||||||
if (order.status !== 'PENDING_RECEIVE') {
|
|
||||||
|
const onSitePickup = !!opts?.onSitePickup;
|
||||||
|
const onSiteEligible = [
|
||||||
|
'PENDING_SHIP',
|
||||||
|
'OUT_WAREHOUSE',
|
||||||
|
'SHIPPING',
|
||||||
|
'SHIPPED',
|
||||||
|
'PENDING_RECEIVE',
|
||||||
|
'DELIVERED',
|
||||||
|
];
|
||||||
|
if (onSitePickup) {
|
||||||
|
if (!onSiteEligible.includes(order.status)) {
|
||||||
|
throw new BadRequestException('当前状态不可现场取货');
|
||||||
|
}
|
||||||
|
} else if (!['PENDING_RECEIVE', 'DELIVERED'].includes(order.status)) {
|
||||||
throw new BadRequestException('当前状态不可确认收货');
|
throw new BadRequestException('当前状态不可确认收货');
|
||||||
}
|
}
|
||||||
await this.applyStatusTransition(order.id, order.status, 'COMPLETED', 'USER');
|
|
||||||
|
await this.applyStatusTransition(
|
||||||
|
order.id,
|
||||||
|
order.status,
|
||||||
|
'COMPLETED',
|
||||||
|
onSitePickup ? 'USER_ON_SITE' : 'USER',
|
||||||
|
onSitePickup ? '用户现场取货确认收货' : undefined,
|
||||||
|
);
|
||||||
return this.getOrder(userId, orderId);
|
return this.getOrder(userId, orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,6 +917,7 @@ export class TradeService {
|
|||||||
fromStatus: string,
|
fromStatus: string,
|
||||||
targetStatus: string,
|
targetStatus: string,
|
||||||
operator = 'MOCK',
|
operator = 'MOCK',
|
||||||
|
remark?: string,
|
||||||
) {
|
) {
|
||||||
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
|
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
|
||||||
if (!order) return;
|
if (!order) return;
|
||||||
@@ -913,7 +939,7 @@ export class TradeService {
|
|||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
await tx.order.update({ where: { id: orderId }, data: data as never });
|
await tx.order.update({ where: { id: orderId }, data: data as never });
|
||||||
if (Object.keys(deliveryData).length) {
|
if (Object.keys(deliveryData).length) {
|
||||||
await tx.orderDelivery.update({ where: { orderId }, data: deliveryData as never });
|
await tx.orderDelivery.updateMany({ where: { orderId }, data: deliveryData as never });
|
||||||
}
|
}
|
||||||
await tx.commonEvent.create({
|
await tx.commonEvent.create({
|
||||||
data: buildOrderStatusEvent({
|
data: buildOrderStatusEvent({
|
||||||
@@ -921,6 +947,7 @@ export class TradeService {
|
|||||||
fromStatus: currentStatus,
|
fromStatus: currentStatus,
|
||||||
toStatus: targetStatus,
|
toStatus: targetStatus,
|
||||||
operator,
|
operator,
|
||||||
|
remark,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user