5 Commits

Author SHA1 Message Date
jacy ee493823bf 增加音效逻辑
CI / verify (pull_request) Has been cancelled
2026-08-20 15:04:54 +08:00
jacy 8b5bb6a981 大屏显示优化
CI / verify (pull_request) Has been cancelled
2026-08-20 13:41:50 +08:00
jacy 0a36188b35 调整优化大屏 2026-08-20 12:55:11 +08:00
jacy 6cf66c722c v3.5.2 大屏调整
CI / verify (pull_request) Has been cancelled
2026-08-20 11:58:15 +08:00
jacy 4db21cf1a1 mockpay功能调试
CI / verify (pull_request) Has been cancelled
2026-08-20 00:51:18 +08:00
15 changed files with 351 additions and 559 deletions
+55 -22
View File
@@ -291,26 +291,21 @@ body:has(.big-screen-page),
z-index: 1;
flex: 1;
min-height: 0;
width: 100%;
display: flex;
flex-direction: column;
padding: 0 8px;
padding: 0;
overflow: hidden;
}
.big-screen-list-head,
.big-screen-row {
display: grid;
grid-template-columns: 180px 1fr 160px 200px;
grid-template-columns: minmax(180px, 18%) minmax(0, 1fr) minmax(140px, 16%) minmax(160px, 18%);
align-items: center;
column-gap: 16px;
}
.big-screen-list-head {
flex: 0 0 auto;
padding: 0 28px 10px 40px;
font-size: 15px;
color: rgba(170, 200, 230, 0.55);
letter-spacing: 2px;
width: 100%;
box-sizing: border-box;
}
.big-screen-list-body {
@@ -434,6 +429,13 @@ body:has(.big-screen-page),
font-weight: 700;
font-variant-numeric: tabular-nums;
white-space: nowrap;
letter-spacing: 0.5px;
min-width: 0;
}
.big-screen-amount--6d {
font-size: 0.86em;
letter-spacing: 0;
}
.big-screen-items {
@@ -450,10 +452,20 @@ body:has(.big-screen-page),
}
.big-screen-empty {
position: absolute;
left: 0;
right: 0;
/* 全屏高度黄金分割点(≈0.382),略偏上 */
top: 38.2%;
transform: translateY(-50%);
z-index: 2;
text-align: center;
color: rgba(255, 255, 255, 0.45);
padding: 48px 20px;
font-size: 18px;
color: rgba(145, 190, 230, 0.42);
padding: 0 40px;
font-size: 60px;
letter-spacing: 6px;
font-weight: 400;
pointer-events: none;
}
.big-screen-fx {
@@ -540,13 +552,14 @@ body:has(.big-screen-page),
.big-screen-fx-card {
position: relative;
z-index: 2;
min-width: 420px;
max-width: 72vw;
min-width: min(420px, 92vw);
max-width: min(920px, 92vw);
padding: 28px 40px 32px;
border-radius: 12px;
text-align: center;
background: rgba(8, 22, 48, 0.82);
backdrop-filter: blur(8px);
box-sizing: border-box;
}
.big-screen-fx-card--t1 {
@@ -562,7 +575,7 @@ body:has(.big-screen-page),
}
.big-screen-fx-card--t3 {
min-width: 520px;
min-width: min(560px, 94vw);
padding: 36px 48px 40px;
border: 3px solid #ffd666;
box-shadow:
@@ -623,12 +636,19 @@ body:has(.big-screen-page),
}
.big-screen-fx-amount {
font-size: 64px;
font-size: clamp(44px, 8.5vw, 64px);
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: 2px;
line-height: 1.1;
color: #e6f7ff;
max-width: 100%;
margin-inline: auto;
}
.big-screen-fx-amount--6d {
font-size: clamp(36px, 7vw, 52px) !important;
letter-spacing: 1px;
}
.big-screen-fx-card--t1 .big-screen-fx-amount {
@@ -643,7 +663,7 @@ body:has(.big-screen-page),
}
.big-screen-fx-card--t3 .big-screen-fx-amount {
font-size: 84px;
font-size: clamp(52px, 10vw, 84px);
color: #ffd666;
text-shadow:
0 0 12px #ffd666,
@@ -651,6 +671,11 @@ body:has(.big-screen-page),
animation: big-screen-amount-flash 0.9s ease-in-out infinite;
}
.big-screen-fx-card--t3 .big-screen-fx-amount--6d {
font-size: clamp(44px, 8.5vw, 68px) !important;
letter-spacing: 1px;
}
@keyframes big-screen-amount-pop {
0% {
transform: scale(0.7);
@@ -693,16 +718,24 @@ body:has(.big-screen-page),
.big-screen-clock {
font-size: 56px;
}
.big-screen-list-head,
.big-screen-row {
grid-template-columns: 140px 1fr 120px 160px;
grid-template-columns: minmax(140px, 20%) minmax(0, 1fr) minmax(110px, 16%) minmax(130px, 18%);
font-size: 16px;
}
.big-screen-amount--6d {
font-size: 0.82em;
}
.big-screen-fx-amount {
font-size: 44px;
font-size: clamp(36px, 8vw, 44px);
}
.big-screen-fx-card--t3 .big-screen-fx-amount {
font-size: 56px;
font-size: clamp(40px, 9vw, 56px);
}
.big-screen-fx-amount--6d {
font-size: clamp(32px, 6.5vw, 40px) !important;
}
.big-screen-fx-card--t3 .big-screen-fx-amount--6d {
font-size: clamp(34px, 7vw, 48px) !important;
}
}
@@ -217,6 +217,12 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
method: 'POST',
body: JSON.stringify({ payMethod: 'NATIVE' }),
});
if (pay.mode === 'mock') {
message.success(`支付成功:${order.orderNo}`);
resetForm();
onSuccess({ id: order.id, orderNo: order.orderNo });
return;
}
setCodeUrl(pay.codeUrl ?? null);
startPoll(order.id);
} catch (e) {
-66
View File
@@ -1,71 +1,5 @@
export const PACKAGE_AUDIT_CHANGED_EVENT = 'admin:package-audit-changed';
export const AUDIT_NOTICE_CHANGED_EVENT = 'dukang:audit-notice-changed';
export const BIG_SCREEN_DEMO_CHANNEL = 'dukang-big-screen';
export const BIG_SCREEN_DEMO_STORAGE_KEY = 'dukang:big-screen-demo';
export type BigScreenDemoOrder = {
payAmount: number;
items: string;
userPhoneMasked: string;
};
export type BigScreenDemoPayload = {
type: 'demo';
id: string;
at: number;
orders: BigScreenDemoOrder[];
};
export function buildBigScreenDemoPayload(): BigScreenDemoPayload {
return {
type: 'demo',
id: `demo-${Date.now()}`,
at: Date.now(),
orders: [
{ payAmount: 1288, items: '杜康君酿 · 浓香型 × 6瓶', userPhoneMasked: '138****8888' },
{ payAmount: 688, items: '杜康君酿 · 浓香型 × 2瓶', userPhoneMasked: '139****6666' },
{ payAmount: 188, items: '杜康君酿 · 浓香型 × 1瓶', userPhoneMasked: '137****1888' },
],
};
}
export function triggerBigScreenDemo() {
const payload = buildBigScreenDemoPayload();
try {
localStorage.setItem(BIG_SCREEN_DEMO_STORAGE_KEY, JSON.stringify(payload));
} catch {
/* ignore quota */
}
try {
const ch = new BroadcastChannel(BIG_SCREEN_DEMO_CHANNEL);
ch.postMessage(payload);
ch.close();
} catch {
/* BroadcastChannel 不可用时靠 localStorage 冷启动 */
}
return payload;
}
export function readPendingBigScreenDemo(maxAgeMs = 8000): BigScreenDemoPayload | null {
try {
const raw = localStorage.getItem(BIG_SCREEN_DEMO_STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as BigScreenDemoPayload;
if (parsed?.type !== 'demo' || !parsed.id || !Array.isArray(parsed.orders)) return null;
if (Date.now() - Number(parsed.at || 0) > maxAgeMs) return null;
return parsed;
} catch {
return null;
}
}
export function clearPendingBigScreenDemo() {
try {
localStorage.removeItem(BIG_SCREEN_DEMO_STORAGE_KEY);
} catch {
/* ignore */
}
}
export function notifyPackageAuditChanged() {
window.dispatchEvent(new Event(PACKAGE_AUDIT_CHANGED_EVENT));
+132 -73
View File
@@ -1,18 +1,17 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { request } from '../lib/api';
import {
BIG_SCREEN_DEMO_CHANNEL,
clearPendingBigScreenDemo,
readPendingBigScreenDemo,
type BigScreenDemoPayload,
} from '../lib/admin-events';
import sfxTier1 from '../assets/big-screen/celebrate-t1.mp3';
import sfxTier2 from '../assets/big-screen/celebrate-t2.mp3';
import sfxTier3 from '../assets/big-screen/celebrate-t3.mp3';
export type BigScreenOrder = {
id: string;
orderNo: string;
payAmount: number;
items: string;
/** 展示用时间(付款成功时间) */
createdAt: string;
paidAt?: string;
userPhoneMasked: string | null;
};
@@ -20,8 +19,61 @@ type AmountTier = 1 | 2 | 3;
const POLL_MS = 3000;
const ROW_MS = 2500;
/** 不足此条数时重复填充,保证滚动连贯 */
const MIN_SCROLL_ROWS = 10;
const FX_DURATION_MS = 10_000;
const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const TIER_DURATION: Record<AmountTier, number> = { 1: 3000, 2: 4500, 3: 6000 };
const CELEBRATE_SFX: Record<AmountTier, string> = {
1: sfxTier1,
2: sfxTier2,
3: sfxTier3,
};
/** 浏览器自动播放策略:需用户手势后才能出声 */
let audioUnlocked = false;
const sharedAudio = typeof Audio !== 'undefined' ? new Audio() : null;
function unlockCelebrateAudio() {
if (audioUnlocked || !sharedAudio) return;
sharedAudio.muted = true;
sharedAudio.src = CELEBRATE_SFX[1];
void sharedAudio
.play()
.then(() => {
sharedAudio.pause();
sharedAudio.currentTime = 0;
sharedAudio.muted = false;
audioUnlocked = true;
})
.catch(() => {
/* 等待下次手势 */
});
}
function playCelebrateSfx(tier: AmountTier): () => void {
if (!sharedAudio) return () => undefined;
try {
sharedAudio.pause();
sharedAudio.currentTime = 0;
sharedAudio.src = CELEBRATE_SFX[tier];
sharedAudio.volume = tier === 3 ? 0.85 : tier === 2 ? 0.75 : 0.65;
sharedAudio.muted = false;
void sharedAudio.play().catch(() => {
/* 未解锁时静默失败 */
});
} catch {
/* ignore */
}
return () => {
try {
sharedAudio.pause();
sharedAudio.currentTime = 0;
} catch {
/* ignore */
}
};
}
function pad(n: number) {
return String(n).padStart(2, '0');
@@ -35,7 +87,7 @@ function formatDateLine(d: Date) {
return `${d.getFullYear()} / ${pad(d.getMonth() + 1)} / ${pad(d.getDate())} ${WEEKDAYS[d.getDay()]}`;
}
function formatHm(iso: string): string {
function formatOrderTime(iso: string): string {
try {
return formatClock(new Date(iso));
} catch {
@@ -43,11 +95,25 @@ function formatHm(iso: string): string {
}
}
// function formatAmount(n: number): string {
// return Number(n || 0).toLocaleString('zh-CN', {
// minimumFractionDigits: 0,
// maximumFractionDigits: 0,
// });
// }
function formatAmount(n: number): string {
return Number(n || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
const num = Number(n || 0);
// 先保留两位小数,再分割处理
const fixed = num.toFixed(2);
const [intStr, decStr] = fixed.split('.');
const intFormatted = Number(intStr).toLocaleString('zh-CN');
return decStr === '00' ? intFormatted : `${intFormatted}.${decStr}`;
}
function amountCssClass(payAmount: number, base: string): string {
const wide = Math.floor(Math.abs(Number(payAmount || 0))) >= 100_000;
return wide ? `${base} ${base}--6d` : base;
}
export function amountTier(payAmount: number): AmountTier {
@@ -83,9 +149,11 @@ function OrderRow({
className={`big-screen-row big-screen-row--t${tier}${latest ? ' is-latest' : ''}`}
>
{latest ? <span className="big-screen-row-mark" /> : null}
<span className="big-screen-amount">¥ {formatAmount(order.payAmount)}</span>
<span className={amountCssClass(order.payAmount, 'big-screen-amount')}>
¥ {formatAmount(order.payAmount)}
</span>
<span className="big-screen-items">{order.items || '—'}</span>
<span className="big-screen-time">{formatHm(order.createdAt)}</span>
<span className="big-screen-time">{formatOrderTime(order.paidAt ?? order.createdAt)}</span>
<span className="big-screen-phone">{order.userPhoneMasked || '—'}</span>
</div>
);
@@ -166,9 +234,13 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
const [displayAmount, setDisplayAmount] = useState(tier === 3 ? 0 : order.payAmount);
useEffect(() => {
const duration = TIER_DURATION[tier];
const timer = window.setTimeout(() => onDoneRef.current(), duration);
const timer = window.setTimeout(() => onDoneRef.current(), FX_DURATION_MS);
return () => window.clearTimeout(timer);
}, [order.id]);
useEffect(() => {
const stop = playCelebrateSfx(tier);
return stop;
}, [order.id, tier]);
useEffect(() => {
@@ -201,7 +273,6 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
let raf = 0;
let last = performance.now();
const started = performance.now();
const duration = TIER_DURATION[tier];
const gravity = tier === 3 ? 0.18 : 0.14;
const loop = (now: number) => {
const dt = Math.min(32, now - last) / 16.6;
@@ -212,8 +283,12 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += p.vr * dt;
p.life -= (tier === 3 ? 0.0032 : 0.005) * dt;
if (p.rain && (p.y > canvas.clientHeight + 30 || p.life <= 0) && now - started < duration - 400) {
p.life -= (tier === 3 ? 0.002 : 0.003) * dt;
if (
p.rain &&
(p.y > canvas.clientHeight + 30 || p.life <= 0) &&
now - started < FX_DURATION_MS - 400
) {
p.x = Math.random() * canvas.clientWidth;
p.y = -16 - Math.random() * 60;
p.vx = (Math.random() - 0.5) * 1.4;
@@ -253,11 +328,13 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
{tier >= 2 ? <div className="big-screen-fx-sweep big-screen-fx-sweep--alt" /> : null}
<div className={`big-screen-fx-card big-screen-fx-card--t${tier}`}>
<div className="big-screen-fx-kicker">{tier === 3 ? '高额成交' : tier === 2 ? '大额成交' : '新成交'}</div>
<div className="big-screen-fx-amount">¥ {formatAmount(displayAmount)}</div>
<div className={amountCssClass(displayAmount || order.payAmount, 'big-screen-fx-amount')}>
¥ {formatAmount(displayAmount)}
</div>
<div className="big-screen-fx-items">{order.items || '—'}</div>
<div className="big-screen-fx-meta">
<span>{order.userPhoneMasked || '—'}</span>
<span>{formatHm(order.createdAt)}</span>
<span>{formatOrderTime(order.paidAt ?? order.createdAt)}</span>
</div>
</div>
</div>
@@ -270,10 +347,11 @@ export default function BigScreenPage() {
const [paused, setPaused] = useState(false);
const [celebrate, setCelebrate] = useState<BigScreenOrder | null>(null);
const seenIdsRef = useRef<Set<string> | null>(null);
const celebratedIdsRef = useRef<Set<string>>(new Set());
const queueRef = useRef<BigScreenOrder[]>([]);
const celebratingRef = useRef(false);
const viewportRef = useRef<HTMLDivElement | null>(null);
const demoSeenRef = useRef<Set<string>>(new Set());
const dayKeyRef = useRef('');
const [viewportH, setViewportH] = useState(0);
const playNext = useCallback(() => {
@@ -285,11 +363,13 @@ export default function BigScreenPage() {
const enqueueNew = useCallback(
(fresh: BigScreenOrder[]) => {
if (!fresh.length) return;
const ranked = [...fresh].sort((a, b) => {
const toCelebrate = fresh.filter((o) => !celebratedIdsRef.current.has(o.id));
if (!toCelebrate.length) return;
for (const o of toCelebrate) celebratedIdsRef.current.add(o.id);
const ranked = [...toCelebrate].sort((a, b) => {
const td = amountTier(b.payAmount) - amountTier(a.payAmount);
if (td !== 0) return td;
return +new Date(b.createdAt) - +new Date(a.createdAt);
return +new Date(b.paidAt ?? b.createdAt) - +new Date(a.paidAt ?? a.createdAt);
});
queueRef.current.push(...ranked);
if (!celebratingRef.current) playNext();
@@ -297,38 +377,28 @@ export default function BigScreenPage() {
[playNext],
);
const playDemo = useCallback(
(payload: BigScreenDemoPayload) => {
if (!payload?.orders?.length || demoSeenRef.current.has(payload.id)) return;
demoSeenRef.current.add(payload.id);
const now = new Date().toISOString();
const fake: BigScreenOrder[] = payload.orders.map((o, i) => ({
id: `${payload.id}-${i}`,
orderNo: `${payload.id}-${i}`,
payAmount: o.payAmount,
items: o.items,
createdAt: now,
userPhoneMasked: o.userPhoneMasked,
}));
setItems((prev) => [...fake, ...prev.filter((row) => !row.id.startsWith('demo-'))]);
enqueueNew(fake);
clearPendingBigScreenDemo();
},
[enqueueNew],
);
const fetchData = useCallback(() => {
const now = new Date();
const todayKey = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
if (dayKeyRef.current && dayKeyRef.current !== todayKey) {
seenIdsRef.current = null;
celebratedIdsRef.current = new Set();
queueRef.current = [];
celebratingRef.current = false;
setCelebrate(null);
setPaused(false);
}
dayKeyRef.current = todayKey;
return request<{ items: BigScreenOrder[] }>('/admin/orders/big-screen?limit=2000')
.then((d) => {
const list = d.items ?? [];
setItems((prev) => {
const demos = prev.filter((o) => o.id.startsWith('demo-'));
return demos.length ? [...demos, ...list] : list;
});
setItems(list);
setLoadError('');
const seen = seenIdsRef.current;
if (!seen) {
seenIdsRef.current = new Set(list.map((o) => o.id));
for (const o of list) celebratedIdsRef.current.add(o.id);
return;
}
const fresh = list.filter((o) => !seen.has(o.id));
@@ -347,22 +417,15 @@ export default function BigScreenPage() {
}, [fetchData]);
useEffect(() => {
const pending = readPendingBigScreenDemo();
if (pending) playDemo(pending);
let ch: BroadcastChannel | null = null;
try {
ch = new BroadcastChannel(BIG_SCREEN_DEMO_CHANNEL);
ch.onmessage = (ev: MessageEvent<BigScreenDemoPayload>) => {
if (ev.data?.type === 'demo' && ev.data.orders?.length) playDemo(ev.data);
};
} catch {
ch = null;
}
unlockCelebrateAudio();
const unlock = () => unlockCelebrateAudio();
window.addEventListener('pointerdown', unlock, { once: true });
window.addEventListener('keydown', unlock, { once: true });
return () => {
ch?.close();
window.removeEventListener('pointerdown', unlock);
window.removeEventListener('keydown', unlock);
};
}, [playDemo]);
}, []);
useEffect(() => {
const html = document.documentElement;
@@ -390,7 +453,7 @@ export default function BigScreenPage() {
if (!items.length) return [];
const rowH = 80;
const visible = Math.max(1, Math.ceil((viewportH || 480) / rowH));
const minCount = visible + 1;
const minCount = Math.max(MIN_SCROLL_ROWS, visible + 1);
const unit: BigScreenOrder[] = [];
while (unit.length < minCount) unit.push(...items);
return unit;
@@ -428,16 +491,8 @@ export default function BigScreenPage() {
<LiveClock />
<div className="big-screen-list">
<div className="big-screen-list-head">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div className="big-screen-list-body" ref={viewportRef}>
{items.length === 0 ? (
<div className="big-screen-empty">{loadError || '暂无订单'}</div>
) : (
{items.length === 0 ? null : (
<div
className={`big-screen-track${rolling ? ' is-rolling' : ''}${paused ? ' is-paused' : ''}`}
style={rolling ? ({ ['--marquee-ms']: `${marqueeMs}ms` } as CSSProperties) : undefined}
@@ -454,6 +509,10 @@ export default function BigScreenPage() {
</div>
</div>
{items.length === 0 ? (
<div className="big-screen-empty">{loadError || '当日暂无订单'}</div>
) : null}
{celebrate ? (
<CelebrateFx
key={celebrate.id}
-13
View File
@@ -21,7 +21,6 @@ import {
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api';
import { triggerBigScreenDemo } from '../lib/admin-events';
import {
ADMIN_OPTIONS_PAGE_SIZE,
DELIVERY_PROVIDER_LABELS,
@@ -192,7 +191,6 @@ export default function OrdersPage() {
const [trackOpen, setTrackOpen] = useState(false);
const canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete');
const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders');
const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN';
const selectedOrders = useMemo(
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
@@ -545,17 +543,6 @@ export default function OrdersPage() {
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Space>
<Button onClick={() => window.open('/orders/big-screen', 'dukang-big-screen')}></Button>
{isSuperAdmin ? (
<Button
onClick={() => {
window.open('/orders/big-screen', 'dukang-big-screen');
triggerBigScreenDemo();
message.success('已发送测试成交动效(三级 / 二级 / 一级)');
}}
>
</Button>
) : null}
{canProxyOrder ? (
<Button type="primary" onClick={() => setProxyOpen(true)}>
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.mp3' {
const src: string;
export default src;
}
-323
View File
@@ -168,128 +168,6 @@ importers:
specifier: ^5.4.0
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
apps/h5-user:
dependencies:
'@dukang/client-logging':
specifier: workspace:*
version: link:../../packages/client-logging
'@dukang/shared-types':
specifier: workspace:*
version: link:../../packages/shared-types
'@dukang/shared-ui':
specifier: workspace:*
version: link:../../packages/shared-ui
'@dukang/weixin-sdk':
specifier: workspace:*
version: link:../../packages/weixin-sdk
element-china-area-data:
specifier: ^6.1.0
version: 6.1.0
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
react-router-dom:
specifier: ^6.26.0
version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@types/react':
specifier: ^18.3.3
version: 18.3.31
'@types/react-dom':
specifier: ^18.3.0
version: 18.3.7(@types/react@18.3.31)
'@vitejs/plugin-react':
specifier: ^4.3.1
version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
typescript:
specifier: ^5.4.5
version: 5.9.3
vite:
specifier: ^5.4.0
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
apps/mini-hq:
dependencies:
'@babel/runtime':
specifier: ^7.24.4
version: 7.29.7
'@dukang/shared-types':
specifier: workspace:*
version: link:../../packages/shared-types
'@dukang/weixin-sdk':
specifier: workspace:*
version: link:../../packages/weixin-sdk
'@tarojs/components':
specifier: 4.2.0
version: 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/helper':
specifier: 4.2.0
version: 4.2.0
'@tarojs/plugin-framework-react':
specifier: 4.2.0
version: 4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(postcss@8.5.15))
'@tarojs/plugin-html':
specifier: 4.2.0
version: 4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)
'@tarojs/plugin-platform-h5':
specifier: 4.2.0
version: 4.2.0(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/plugin-platform-weapp':
specifier: 4.2.0
version: 4.2.0(@tarojs/service@4.2.0)(@tarojs/shared@4.2.0)
'@tarojs/react':
specifier: 4.2.0
version: 4.2.0(react@18.3.1)
'@tarojs/router':
specifier: 4.2.0
version: 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))
'@tarojs/runtime':
specifier: 4.2.0
version: 4.2.0
'@tarojs/shared':
specifier: 4.2.0
version: 4.2.0
'@tarojs/taro':
specifier: 4.2.0
version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
qrcode:
specifier: ^1.5.4
version: 1.5.4
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
devDependencies:
'@babel/preset-react':
specifier: ^7.24.1
version: 7.29.7(@babel/core@7.29.7)
'@tarojs/cli':
specifier: 4.2.0
version: 4.2.0
'@tarojs/vite-runner':
specifier: 4.2.0
version: 4.2.0(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
'@types/react':
specifier: ^18.3.3
version: 18.3.31
'@vitejs/plugin-react':
specifier: ^4.3.1
version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
babel-preset-taro:
specifier: 4.2.0
version: 4.2.0(@babel/core@7.29.7)(@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7))(@babel/preset-react@7.29.7(@babel/core@7.29.7))
typescript:
specifier: ^5.4.5
version: 5.9.3
vite:
specifier: ^5.4.0
version: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
apps/mini-user:
dependencies:
'@babel/runtime':
@@ -2534,14 +2412,6 @@ packages:
webpack:
optional: true
'@tarojs/plugin-html@4.2.0':
resolution: {integrity: sha512-q0NJ6WVZTxWwr4MIYU9cj84uSPNOZ1BC+IdI3aTSKPyACHACUQp0yaFSL8/MGUj2+QoOU0op6/PSJUHfDNRV+g==}
engines: {node: '>= 18'}
peerDependencies:
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/plugin-platform-h5@4.2.0':
resolution: {integrity: sha512-HoyGHYaBrxJtaIRT1Y81LK7re0pLfF9DOGQ77bNmQwtuJS9JpiFMI47Q27+2anldwJodQdQP9d3KSPRqGnRLCw==}
@@ -8803,29 +8673,6 @@ snapshots:
- webpack-chain
- webpack-dev-server
'@tarojs/components-react@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15))':
dependencies:
'@babel/runtime': 7.29.7
'@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/shared': 4.2.0
'@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
classnames: 2.5.1
identity-obj-proxy: 3.0.0
react: 18.3.1
solid-js: 1.9.14
swiper: 11.1.15
tslib: 2.8.1
transitivePeerDependencies:
- '@tarojs/helper'
- '@types/react'
- html-webpack-plugin
- postcss
- rollup
- vue
- webpack
- webpack-chain
- webpack-dev-server
'@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15))':
dependencies:
'@stencil/core': 2.22.3
@@ -8849,29 +8696,6 @@ snapshots:
- webpack-chain
- webpack-dev-server
'@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))':
dependencies:
'@stencil/core': 2.22.3
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
classnames: 2.5.1
hammerjs: 2.0.8
hls.js: 1.6.16
resolve-pathname: 3.0.0
swiper: 11.1.15
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.31
transitivePeerDependencies:
- '@tarojs/helper'
- html-webpack-plugin
- postcss
- rollup
- webpack
- webpack-chain
- webpack-dev-server
'@tarojs/helper@4.2.0':
dependencies:
'@babel/core': 7.29.7
@@ -9009,27 +8833,6 @@ snapshots:
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
webpack: 5.97.1(@swc/core@1.3.96)(postcss@8.5.15)
'@tarojs/plugin-framework-react@4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)))(react@18.3.1)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))(webpack@5.97.1(postcss@8.5.15))':
dependencies:
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
acorn: 8.17.0
acorn-walk: 8.3.5
lodash: 4.18.1
tslib: 2.8.1
optionalDependencies:
'@vitejs/plugin-react': 4.7.0(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))
react: 18.3.1
vite: 5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0)
webpack: 5.97.1(postcss@8.5.15)
'@tarojs/plugin-html@4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)':
dependencies:
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/plugin-platform-h5@4.2.0(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15))':
dependencies:
'@babel/core': 7.29.7
@@ -9059,35 +8862,6 @@ snapshots:
- webpack-chain
- webpack-dev-server
'@tarojs/plugin-platform-h5@4.2.0(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15))':
dependencies:
'@babel/core': 7.29.7
'@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/components-react': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(react@18.3.1)(rollup@3.30.0)(solid-js@1.9.14)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/service': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/taro-h5': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))
babel-plugin-transform-taroapi: 4.2.0(@babel/core@7.29.7)
change-case: 4.1.2
lodash-es: 4.17.21
tslib: 2.8.1
transitivePeerDependencies:
- '@swc/helpers'
- '@tarojs/taro'
- '@types/react'
- html-webpack-plugin
- postcss
- react
- rollup
- solid-js
- supports-color
- vue
- webpack
- webpack-chain
- webpack-dev-server
'@tarojs/plugin-platform-weapp@4.2.0(@tarojs/service@4.2.0)(@tarojs/shared@4.2.0)':
dependencies:
'@tarojs/service': 4.2.0
@@ -9112,18 +8886,6 @@ snapshots:
tslib: 2.8.1
universal-router: 9.2.1
'@tarojs/router@4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))':
dependencies:
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
dingtalk-jsapi: 2.15.6
history: 5.3.0
mobile-detect: 1.4.5
query-string: 9.4.1
tslib: 2.8.1
universal-router: 9.2.1
'@tarojs/runner-utils@4.2.0':
dependencies:
'@tarojs/helper': 4.2.0
@@ -9175,26 +8937,6 @@ snapshots:
transitivePeerDependencies:
- '@tarojs/taro'
'@tarojs/taro-h5@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))':
dependencies:
'@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)
'@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/router': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
abortcontroller-polyfill: 1.7.8
base64-js: 1.5.1
ics: 3.12.0
is-mobile: 4.0.0
jsonp-retry: 1.0.3
lodash-es: 4.17.21
platform: 1.3.6
query-string: 9.4.1
tslib: 2.8.1
whatwg-fetch: 3.6.20
transitivePeerDependencies:
- '@tarojs/taro'
'@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(@swc/core@1.3.96)(postcss@8.5.15))':
dependencies:
'@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)
@@ -9209,20 +8951,6 @@ snapshots:
rollup: 3.30.0
webpack: 5.97.1(@swc/core@1.3.96)(postcss@8.5.15)
'@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))':
dependencies:
'@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)
'@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)(webpack@5.97.1(postcss@8.5.15))
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@types/postcss-url': 10.0.4
postcss: 8.5.15
optionalDependencies:
'@types/react': 18.3.31
rollup: 3.30.0
webpack: 5.97.1(postcss@8.5.15)
'@tarojs/vite-runner@4.2.0(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.20.0)(sass@1.101.0)(terser@5.48.0))':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -13385,17 +13113,6 @@ snapshots:
optionalDependencies:
'@swc/core': 1.3.96
terser-webpack-plugin@5.6.1(postcss@8.5.15)(webpack@5.97.1(postcss@8.5.15)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.48.0
webpack: 5.97.1(postcss@8.5.15)
optionalDependencies:
postcss: 8.5.15
optional: true
terser@5.48.0:
dependencies:
'@jridgewell/source-map': 0.3.11
@@ -13832,46 +13549,6 @@ snapshots:
- uglify-js
optional: true
webpack@5.97.1(postcss@8.5.15):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.9
'@webassemblyjs/ast': 1.14.1
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.17.0
browserslist: 4.28.4
chrome-trace-event: 1.0.4
enhanced-resolve: 5.24.1
es-module-lexer: 1.7.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.2
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.3.3
terser-webpack-plugin: 5.6.1(postcss@8.5.15)(webpack@5.97.1(postcss@8.5.15))
watchpack: 2.5.2
webpack-sources: 3.5.0
transitivePeerDependencies:
- '@minify-html/node'
- '@swc/core'
- '@swc/css'
- '@swc/html'
- clean-css
- cssnano
- csso
- esbuild
- html-minifier-terser
- lightningcss
- postcss
- uglify-js
optional: true
whatwg-fetch@3.6.20: {}
whatwg-url@5.0.0:
@@ -0,0 +1,73 @@
import { config } from 'dotenv';
import { resolve } from 'path';
import { PrismaClient } from '@prisma/client';
config({ path: resolve('.env'), override: true });
function mockPayEnabled() {
return process.env.MOCK_PAY !== 'false';
}
function wechatPayEnabled() {
const e = process.env;
const appId = e.WX_MINI_APP_ID?.trim() || e.WX_APP_ID?.trim();
return (
!mockPayEnabled() &&
!!(
appId &&
e.WX_MCH_ID?.trim() &&
e.WX_MCH_SERIAL_NO?.trim() &&
e.WX_MCH_PRIVATE_KEY?.trim() &&
e.WX_API_V3_KEY?.trim()
)
);
}
const prisma = new PrismaClient();
console.log('=== .env only ===');
console.log('MOCK_PAY:', process.env.MOCK_PAY);
console.log('mockPayEnabled:', mockPayEnabled());
const rows = await prisma.systemConfig.findMany({
where: { configKey: { in: ['MOCK_PAY', 'MOCK_SMS', 'MOCK_WECHAT', 'WX_MCH_ID'] } },
});
console.log('\n=== system_config (DB) ===');
for (const r of rows) {
console.log(`${r.configKey} = ${r.configKey.includes('KEY') ? '(redacted)' : r.value}`);
process.env[r.configKey] = r.value;
}
console.log('\n=== After DB overlay (API startup) ===');
console.log('MOCK_PAY:', process.env.MOCK_PAY);
console.log('mockPayEnabled:', mockPayEnabled());
console.log('wechatPayEnabled:', wechatPayEnabled());
const orders = await prisma.order.findMany({
where: { orderType: 'PROXY' },
orderBy: { id: 'desc' },
take: 5,
select: {
id: true,
orderNo: true,
status: true,
payStatus: true,
payExternalNo: true,
channelSource: true,
createdAt: true,
},
});
console.log('\n=== Recent PROXY orders ===');
for (const o of orders) {
console.log({
id: o.id.toString(),
orderNo: o.orderNo,
status: o.status,
payStatus: o.payStatus,
payExternalNo: o.payExternalNo,
channelSource: o.channelSource,
createdAt: o.createdAt.toISOString(),
});
}
await prisma.$disconnect();
@@ -1,30 +1,19 @@
import { Injectable } from '@nestjs/common';
import { loadAppConfig } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import type { IPayProvider, PayMethod, PayOrderResult, RefundOrderResult } from './pay.interface';
@Injectable()
export class PayMockProvider implements IPayProvider {
constructor(private readonly prisma: PrismaService) {}
async payOrder(
orderId: bigint,
_openId?: string,
_platform?: 'h5' | 'mini',
payMethod: PayMethod = 'JSAPI',
_payMethod: PayMethod = 'JSAPI',
): Promise<PayOrderResult> {
if (!loadAppConfig().mockPay) {
throw new Error('Real WeChat pay requires PayWechatProvider');
}
if (payMethod === 'NATIVE') {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const orderNo = order?.orderNo ?? orderId.toString();
return {
mode: 'native',
codeUrl: `mock://wechat-pay/native?orderNo=${encodeURIComponent(orderNo)}`,
externalNo: `MOCK-NATIVE-${Date.now()}`,
};
}
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
}
@@ -21,15 +21,6 @@ export class PayWechatProvider implements IPayProvider {
payMethod: PayMethod = 'JSAPI',
): Promise<PayOrderResult> {
if (loadAppConfig().mockPay) {
if (payMethod === 'NATIVE') {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
const orderNo = order?.orderNo ?? orderId.toString();
return {
mode: 'native',
codeUrl: `mock://wechat-pay/native?orderNo=${encodeURIComponent(orderNo)}`,
externalNo: `MOCK-NATIVE-${Date.now()}`,
};
}
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
}
if (!this.wechat.isPayEnabled()) {
@@ -25,15 +25,17 @@ export class AdminOrdersService {
private readonly adminRedeemService: AdminRedeemService,
) {}
/** v3.5.1 #1:发布会大屏,返回全部订单(按时间倒序,上限 2000) */
/** v3.5.1 #1:发布会大屏,仅当日已付款订单;按付款时间倒序 */
async listBigScreen(limit?: string) {
const take = Math.min(Math.max(Number(limit) || 2000, 1), 2000);
const startOfToday = new Date();
startOfToday.setHours(0, 0, 0, 0);
const orders = await this.prisma.order.findMany({
where: {
isTest: false,
payAmount: { gte: 100 },
payStatus: 'PAID',
paidAt: { gte: startOfToday },
},
orderBy: { createdAt: 'desc' },
orderBy: [{ paidAt: 'desc' }, { createdAt: 'desc' }],
take,
include: { user: { select: { phone: true } } },
});
@@ -41,12 +43,14 @@ export class AdminOrdersService {
items: orders.map((o) => {
const spec = o.productSpec ? ` ${o.productSpec}` : '';
const phone = o.user?.phone || o.receiverPhone;
const paidAt = o.paidAt?.toISOString() ?? o.createdAt.toISOString();
return {
id: o.id.toString(),
orderNo: o.orderNo,
payAmount: Number(o.payAmount),
items: `${o.productName}${spec} × ${o.quantity}`,
createdAt: o.createdAt.toISOString(),
createdAt: paidAt,
paidAt,
userPhoneMasked: phone ? maskContactPhone(phone) : null,
};
}),
@@ -24,7 +24,7 @@ import { AuthService } from '../iam/auth.service';
import { PromoCodeService } from '../promo/promo-code.service';
import { TicketService } from '../common/ticket.service';
import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants';
import { IPayProvider } from '../../integrations/pay/pay.interface';
import { IPayProvider, type PayOrderResult } from '../../integrations/pay/pay.interface';
import { IDeliveryProvider } from '../../integrations/delivery/delivery.interface';
import { IpGeoService } from '../../common/geo/ip-geo.service';
import { buildOrderClientLocationSnapshot } from '../../common/geo/client-location.util';
@@ -2173,6 +2173,9 @@ export class TradeService {
throw new BadRequestException('订单已超时未支付');
}
const mockPaid = await this.tryMockPayProxyOrder(order, 'PARTNER_PROXY_MOCK_PAY');
if (mockPaid) return mockPaid;
const lockedMethod = this.parseProxyPayMethod(order.channelSource);
if (lockedMethod && lockedMethod !== payMethod) {
throw new BadRequestException(
@@ -2217,33 +2220,7 @@ export class TradeService {
throw e;
}
if (payResult.mode === 'native') {
return {
mode: 'native' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
codeUrl: payResult.codeUrl,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
if (payResult.mode === 'jsapi') {
return {
mode: 'jsapi' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
prepay: payResult.prepay,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
await this.markProxyOrderPaid(order.id, payResult.externalNo, 'PARTNER_PROXY_MOCK_PAY');
return {
mode: 'mock' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
return this.finishProxyPayResult(order, payResult, 'PARTNER_PROXY_MOCK_PAY');
}
/** 总部代下单支付:仅 Native 收款码 */
@@ -2262,6 +2239,9 @@ export class TradeService {
throw new BadRequestException('订单已超时未支付');
}
const mockPaid = await this.tryMockPayProxyOrder(order, 'HQ_PROXY_MOCK_PAY');
if (mockPaid) return mockPaid;
this.payRedeemAnomaly.onPayAttempt(Number(order.payAmount), {
orderNo: order.orderNo,
userId: order.userId,
@@ -2277,20 +2257,15 @@ export class TradeService {
});
throw e;
}
if (payResult.mode !== 'native') {
const result = await this.finishProxyPayResult(order, payResult, 'HQ_PROXY_MOCK_PAY');
if (result.mode !== 'native' && result.mode !== 'mock') {
this.payRedeemAnomaly.onPayFail('无法生成收款码', {
orderNo: order.orderNo,
userId: order.userId,
});
throw new BadRequestException('无法生成收款码');
}
return {
mode: 'native' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
codeUrl: payResult.codeUrl,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
return result;
}
/** Mock 环境确认代下单支付 */
@@ -2375,6 +2350,64 @@ export class TradeService {
return this.fulfillmentService.getOrderTrack(orderId);
}
private async tryMockPayProxyOrder(
order: { id: bigint; orderNo: string; payExpireAt: Date | null },
operator: string,
) {
if (!loadAppConfig().mockPay) return null;
await this.markProxyOrderPaid(order.id, `MOCK-${Date.now()}`, operator);
return {
mode: 'mock' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
/** 代下单支付结果:Mock 环境直接落单,真实环境返回收款码/JSAPI */
private async finishProxyPayResult(
order: { id: bigint; orderNo: string; payExpireAt: Date | null },
payResult: PayOrderResult,
operator: string,
) {
const mockPay = loadAppConfig().mockPay;
const isMockNative =
payResult.mode === 'native' &&
(mockPay || payResult.codeUrl.startsWith('mock://'));
if (payResult.mode === 'mock' || isMockNative) {
await this.markProxyOrderPaid(order.id, payResult.externalNo, operator);
return {
mode: 'mock' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
if (payResult.mode === 'native') {
return {
mode: 'native' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
codeUrl: payResult.codeUrl,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
if (payResult.mode === 'jsapi') {
return {
mode: 'jsapi' as const,
orderId: order.id.toString(),
orderNo: order.orderNo,
prepay: payResult.prepay,
payExpireAt: order.payExpireAt?.toISOString() ?? null,
};
}
throw new BadRequestException('支付发起失败');
}
private async markProxyOrderPaid(orderId: bigint, externalNo: string, operator: string) {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
if (!order) throw new NotFoundException('订单不存在');