From e93e4b8f84a8b5c55c72bf854d8e2563117e428a Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Mon, 3 Aug 2026 20:47:28 +0800 Subject: [PATCH] feat: audit diff, mock SMS 999888, env photos, proxy pay lock, mini-user UX Co-authored-by: Cursor --- AGENTS.md | 2 +- apps/admin-web/src/pages/LoginPage.tsx | 5 +- apps/admin-web/src/pages/RedeemDebugPage.tsx | 5 +- .../src/pages/StorePackageAuditsPage.tsx | 229 ++++++++++++++++-- apps/admin-web/src/pages/StoresPage.tsx | 8 +- apps/h5-partner/src/lib/storeDraft.ts | 13 +- .../src/pages/ProxyOrderDetailPage.tsx | 69 +++++- apps/h5-partner/src/pages/ProxyOrderPage.tsx | 76 +++++- apps/h5-partner/src/pages/StoreCreatePage.tsx | 16 +- apps/h5-partner/src/pages/StoreDetailPage.tsx | 19 +- apps/mini-hq/src/pages/login/index.tsx | 5 +- apps/mini-user/src/pages/home/index.tsx | 32 ++- .../src/pages/store-detail/index.tsx | 56 +---- packages/shared-types/src/config.ts | 3 + packages/shared-types/src/store-package.ts | 5 + packages/shared-types/src/trade.ts | 2 + .../src/integrations/sms/sms-code.store.ts | 11 +- .../src/integrations/sms/sms.mock.provider.ts | 4 +- .../modules/store/store-package.controller.ts | 5 + .../modules/store/store-package.service.ts | 22 ++ .../src/modules/store/store.service.ts | 5 +- .../src/modules/trade/trade.controller.ts | 5 + .../src/modules/trade/trade.service.ts | 62 ++++- 23 files changed, 524 insertions(+), 135 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1b2ceb7..24c262b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ node scripts/smoke-prev1.mjs pnpm lint && pnpm test ``` -Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 +Mock 验证码:`999888`。测试账号见 [`README.md`](./README.md)。 > **端口冲突**:`h5-partner` 与 `admin-web` 同为 5175,勿同时 `dev:partner` + `dev:admin`。 diff --git a/apps/admin-web/src/pages/LoginPage.tsx b/apps/admin-web/src/pages/LoginPage.tsx index 59461da..7a99bea 100644 --- a/apps/admin-web/src/pages/LoginPage.tsx +++ b/apps/admin-web/src/pages/LoginPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Card, Form, Input, Tabs, message, Typography } from 'antd'; +import { MOCK_SMS_FIXED_CODE } from '@dukang/shared-types'; import { saveAuth, request } from '../lib/api'; type LoginResult = { accessToken: string; refreshToken: string }; @@ -117,14 +118,14 @@ export default function LoginPage() { form={smsForm} layout="vertical" onFinish={onSmsFinish} - initialValues={{ phone: '13600000001', code: '123456' }} + initialValues={{ phone: '13600000001', code: MOCK_SMS_FIXED_CODE }} > 0} onClick={() => void sendCode()}> {codeCooldown > 0 ? `${codeCooldown}s` : '获取验证码'} diff --git a/apps/admin-web/src/pages/RedeemDebugPage.tsx b/apps/admin-web/src/pages/RedeemDebugPage.tsx index d48aaa1..690265a 100644 --- a/apps/admin-web/src/pages/RedeemDebugPage.tsx +++ b/apps/admin-web/src/pages/RedeemDebugPage.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Button, Card, Col, Form, Input, InputNumber, Row, Space, Tabs, Typography, message, } from 'antd'; +import { MOCK_SMS_FIXED_CODE } from '@dukang/shared-types'; import { request } from '../lib/api'; type ApiResult = Record; @@ -255,7 +256,7 @@ export default function RedeemDebugPage() { - + - - - ) : ( - row.rejectReason || '—' - ), + render: (_, row) => ( + + + {row.status === 'PENDING' ? ( + <> + + + + ) : ( + row.rejectReason || null + )} + + ), }, ]; @@ -122,6 +254,57 @@ export default function StorePackageAuditsPage() { onChange: (p) => void reload(p, status), }} /> + + setDetailOpen(false)} + extra={ + detail?.status === 'PENDING' ? ( + + + + + ) : null + } + > + {detailLoading ? ( + 加载中… + ) : detail ? ( + <> + + {STORE_PACKAGE_CHANGE_STATUS_LABELS[detail.status]} + + 提交方:{detail.submitterType === 'PARTNER' ? '合伙人' : '门店'} · {fmtTime(detail.createdAt)} + + + {detail.rejectReason ? ( + 驳回原因:{detail.rejectReason} + ) : null} + + 线上 {detail.livePackages?.length ?? 0} 条 → 申请 {detail.packages?.length ?? 0} 条 + + + + ) : null} + + 环境照片 + + 至少 3 张,可继续添加 + - {(fields) => ( + {(fields, { add }) => (
{fields.map((field, index) => ( ))} +
)}
diff --git a/apps/h5-partner/src/lib/storeDraft.ts b/apps/h5-partner/src/lib/storeDraft.ts index 1679dc9..4786d42 100644 --- a/apps/h5-partner/src/lib/storeDraft.ts +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -82,12 +82,18 @@ function timeToMinutes(hhmm: string): number { return h * 60 + m; } +export const MIN_ENV_PHOTO_COUNT = 3; + export function normalizeStringArray(urls: unknown, minLen: number): string[] { const arr = Array.isArray(urls) ? urls.map((u) => String(u ?? '')) : []; while (arr.length < minLen) arr.push(''); return arr; } +export function addEnvPhotoSlot(urls: string[]): string[] { + return [...urls, '']; +} + export function normalizeStoreDraftForm(raw: Partial | null | undefined): StoreDraftForm { const base = defaultStoreForm(); if (!raw) return base; @@ -104,7 +110,7 @@ export function normalizeStoreDraftForm(raw: Partial | null | un openTime2: String(raw.openTime2 ?? base.openTime2), closeTime2: String(raw.closeTime2 ?? base.closeTime2), avgPrice: String(raw.avgPrice ?? base.avgPrice), - envPhotoUrls: normalizeStringArray(raw.envPhotoUrls, 3), + envPhotoUrls: normalizeStringArray(raw.envPhotoUrls, MIN_ENV_PHOTO_COUNT), packages: Array.isArray(raw.packages) ? raw.packages.map((p, i) => ({ name: String((p as { name?: string }).name ?? ''), @@ -215,13 +221,14 @@ export function validateStoreStep2( ): string | null { if (!form.coverUrl.trim()) return '请上传门头照'; const envCount = form.envPhotoUrls.filter((u) => u.trim()).length; - if (envCount < 3) return '请上传至少 3 张环境照片'; + if (envCount < MIN_ENV_PHOTO_COUNT) return `请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`; if (!form.contractUrl.trim()) return '请上传签约合同'; return null; } export function patchEnvPhotoAt(urls: string[], index: number, url: string): string[] { - const envPhotoUrls = normalizeStringArray(urls, 3); + const envPhotoUrls = [...urls]; + while (envPhotoUrls.length <= index) envPhotoUrls.push(''); envPhotoUrls[index] = url; return envPhotoUrls; } diff --git a/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx b/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx index 49af50c..17aa81e 100644 --- a/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx +++ b/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx @@ -59,6 +59,7 @@ export default function ProxyOrderDetailPage() { const [payMethod, setPayMethod] = useState('NATIVE'); const [codeUrl, setCodeUrl] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(null); + const [cancelling, setCancelling] = useState(false); const pollRef = useRef(null); function stopPoll() { @@ -77,7 +78,12 @@ export default function ProxyOrderDetailPage() { useEffect(() => { if (!id) return; void request('PARTNER_H5', `/partner/proxy-orders/${id}`) - .then(setOrder) + .then((data) => { + setOrder(data); + if (data.proxyPayMethod) { + setPayMethod(data.proxyPayMethod); + } + }) .catch((e) => setError(e instanceof Error ? e.message : '加载失败')); }, [id]); @@ -129,6 +135,15 @@ export default function ProxyOrderDetailPage() { async function startPay(method: ProxyPayMethod) { if (!id) return; + const locked = order?.proxyPayMethod; + if (locked && locked !== method) { + setPayMsg( + locked === 'JSAPI' + ? '该订单已发起微信代付,请先取消支付后重新下单' + : '该订单已生成收款码,请先取消支付后重新下单', + ); + return; + } setPaying(true); setPayMsg(''); setPayMethod(method); @@ -166,12 +181,14 @@ export default function ProxyOrderDetailPage() { if (pay.mode === 'native' && pay.codeUrl) { setCodeUrl(pay.codeUrl); + setOrder((prev) => (prev ? { ...prev, proxyPayMethod: method } : prev)); startPoll(id); return; } if (pay.mode === 'jsapi' && pay.prepay) { setCodeUrl(null); + setOrder((prev) => (prev ? { ...prev, proxyPayMethod: method } : prev)); await invokeWechatPay(pay.prepay, { apiBase: '/api/v1', clientApp: 'PARTNER_H5', @@ -191,14 +208,27 @@ export default function ProxyOrderDetailPage() { } } - const autoPayStartedRef = useRef(false); - useEffect(() => { - if (!id || !unpaid || autoPayStartedRef.current) return; - autoPayStartedRef.current = true; - void startPay('NATIVE'); - // 仅首次进入未支付详情时自动拉收款码 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, unpaid]); + async function cancelPay() { + if (!id) return; + setCancelling(true); + setPayMsg(''); + stopPoll(); + try { + await request('PARTNER_H5', `/partner/proxy-orders/${id}/cancel-pay`, { + method: 'POST', + body: '{}', + }); + toastSuccess('已取消支付,可重新下单并选择其他支付方式'); + navigate('/center/proxy-orders'); + } catch (e) { + setPayMsg(e instanceof Error ? e.message : '取消失败'); + toastError(e instanceof Error ? e.message : '取消失败'); + } finally { + setCancelling(false); + } + } + + const lockedPayMethod = order?.proxyPayMethod ?? null; const img = order?.imageResource?.url || ''; const isOnSite = String(order?.deliveryType || '').toUpperCase() === 'ON_SITE_PICKUP'; @@ -285,6 +315,7 @@ export default function ProxyOrderDetailPage() { + {lockedPayMethod ? ( +

+ 已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付 +

+ ) : ( +

+ 请选择支付方式并点击下方按钮发起支付 +

+ )} {payMethod === 'NATIVE' ? (
@@ -338,6 +379,16 @@ export default function ProxyOrderDetailPage() { {payMsg}

) : null} + + ) : null} diff --git a/apps/h5-partner/src/pages/ProxyOrderPage.tsx b/apps/h5-partner/src/pages/ProxyOrderPage.tsx index 70364f8..4cca0f8 100644 --- a/apps/h5-partner/src/pages/ProxyOrderPage.tsx +++ b/apps/h5-partner/src/pages/ProxyOrderPage.tsx @@ -70,6 +70,8 @@ export default function ProxyOrderPage() { const [codeUrl, setCodeUrl] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(null); const [paying, setPaying] = useState(false); + const [cancelling, setCancelling] = useState(false); + const [lockedPayMethod, setLockedPayMethod] = useState(null); const pollRef = useRef(null); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); @@ -234,8 +236,17 @@ export default function ProxyOrderPage() { } async function startPay(orderId: string, method: ProxyPayMethod) { + if (lockedPayMethod && lockedPayMethod !== method) { + setMsg( + lockedPayMethod === 'JSAPI' + ? '该订单已发起微信代付,请先取消支付后重新下单' + : '该订单已生成收款码,请先取消支付后重新下单', + ); + return; + } setPaying(true); setMsg(''); + setPayMethod(method); try { if (method === 'JSAPI') { if (!isWechatEnv()) { @@ -266,11 +277,14 @@ export default function ProxyOrderPage() { if (pay.mode === 'native' && pay.codeUrl) { setCodeUrl(pay.codeUrl); + setLockedPayMethod(method); startPoll(orderId); return; } if (pay.mode === 'jsapi' && pay.prepay) { + setCodeUrl(null); + setLockedPayMethod(method); await invokeWechatPay(pay.prepay, { apiBase: '/api/v1', clientApp: 'PARTNER_H5', @@ -289,6 +303,40 @@ export default function ProxyOrderPage() { } } + async function cancelPay(orderId: string) { + setCancelling(true); + setMsg(''); + stopPoll(); + try { + await request('PARTNER_H5', `/partner/proxy-orders/${orderId}/cancel-pay`, { + method: 'POST', + body: '{}', + }); + toastSuccess('已取消支付,可重新下单并选择其他支付方式'); + setStep('form'); + setCreated(null); + setCodeUrl(null); + setLockedPayMethod(null); + } catch (e) { + setMsg(e instanceof Error ? e.message : '取消失败'); + } finally { + setCancelling(false); + } + } + + function switchPayMethod(method: ProxyPayMethod) { + if (lockedPayMethod && lockedPayMethod !== method) { + setMsg( + lockedPayMethod === 'JSAPI' + ? '该订单已发起微信代付,请先取消支付后重新下单' + : '该订单已生成收款码,请先取消支付后重新下单', + ); + return; + } + setPayMethod(method); + setMsg(''); + } + async function submit() { setMsg(''); const err = validateForm(); @@ -322,6 +370,7 @@ export default function ProxyOrderPage() { setCreated(order); setStep('pay'); setCodeUrl(null); + setLockedPayMethod(null); await startPay(order.id, payMethod); } catch (e) { setMsg(e instanceof Error ? e.message : '下单失败'); @@ -359,24 +408,25 @@ export default function ProxyOrderPage() {
+ {lockedPayMethod ? ( +

+ 已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付 +

+ ) : null} {payMethod === 'NATIVE' ? ( @@ -419,6 +469,16 @@ export default function ProxyOrderPage() { {msg}

) : null} + + ) : loadingOptions ? (

加载商品…

diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index 14ebd80..1e57293 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -40,7 +40,8 @@ import { validateStoreStep3, patchEnvPhotoAt, - + addEnvPhotoSlot, + MIN_ENV_PHOTO_COUNT, } from '../lib/storeDraft'; import StorePackagesForm from '../components/StorePackagesForm'; import { normalizePackageFormItems, validatePackageFormItems } from '../lib/storePackages'; @@ -956,7 +957,7 @@ export default function StoreCreatePage() {

环境照片 *

-

至少 3 张,展示店内整洁环境

+

至少 {MIN_ENV_PHOTO_COUNT} 张,展示店内整洁环境

@@ -982,6 +983,17 @@ export default function StoreCreatePage() {
+ + diff --git a/apps/h5-partner/src/pages/StoreDetailPage.tsx b/apps/h5-partner/src/pages/StoreDetailPage.tsx index 9dacc7d..94ef10c 100644 --- a/apps/h5-partner/src/pages/StoreDetailPage.tsx +++ b/apps/h5-partner/src/pages/StoreDetailPage.tsx @@ -5,7 +5,7 @@ import { request } from '../lib/api'; import { toastError, toastSuccess } from '../lib/toast'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { canManagePartnerStore } from '../lib/partnerAccess'; -import { normalizeStringArray, patchEnvPhotoAt } from '../lib/storeDraft'; +import { MIN_ENV_PHOTO_COUNT, normalizeStringArray, patchEnvPhotoAt, addEnvPhotoSlot } from '../lib/storeDraft'; import OssUploadField from '../components/OssUploadField'; import TencentLocPickerOverlay from '../components/TencentLocPickerOverlay'; import { @@ -18,7 +18,6 @@ import { } from '../lib/storeStatus'; const STATUS_OPTIONS: StoreStatusValue[] = ['OPEN', 'PAUSED', 'CLOSED']; -const ENV_SLOT_COUNT = 3; function uniqueEnvUrls(urls: string[]): string[] { const seen = new Set(); @@ -81,7 +80,7 @@ export default function StoreDetailPage() { .map((m) => String(m.url || '')), ) : []; - setEnvPhotoUrls(normalizeStringArray(envFromMedia, ENV_SLOT_COUNT)); + setEnvPhotoUrls(normalizeStringArray(envFromMedia, MIN_ENV_PHOTO_COUNT)); } useEffect(() => { @@ -196,8 +195,8 @@ export default function StoreDetailPage() { setActionError('请上传门头照'); return; } - if (nextEnv.length < ENV_SLOT_COUNT) { - setActionError(`请上传至少 ${ENV_SLOT_COUNT} 张环境照片`); + if (nextEnv.length < MIN_ENV_PHOTO_COUNT) { + setActionError(`请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`); return; } setMediaSaving(true); @@ -400,7 +399,7 @@ export default function StoreDetailPage() {

店内环境

{canMutate && !readOnly - ? `需 ${ENV_SLOT_COUNT} 张 · 已选 ${uniqueEnvUrls(envPhotoUrls).length} 张` + ? `至少 ${MIN_ENV_PHOTO_COUNT} 张 · 已选 ${uniqueEnvUrls(envPhotoUrls).length} 张` : envPhotos.length ? `已上传 ${envPhotos.length} 张` : '暂无照片'} @@ -420,6 +419,14 @@ export default function StoreDetailPage() { /> ))} +