From 0f5f0f4c2bb17afd0f302297a4dc79dde4ed60cb Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Wed, 15 Jul 2026 19:57:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AB=AF=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E7=94=A8=E6=88=B7=E8=BE=93=E5=85=A5=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/dukang-coding/reference-backend.md | 2 +- .../skills/dukang-v3/reference-req-index.md | 2 +- .../src/components/PhoneVerifySheet.tsx | 7 ++- apps/h5-user/src/pages/LoginPage.tsx | 12 ++-- apps/h5-user/src/pages/OrderConfirmPage.tsx | 52 ++++++++++++++-- apps/h5-user/src/styles.css | 12 ++++ .../src/components/WechatShareBootstrap.tsx | 15 ++--- apps/mini-user/src/lib/pay-ready.ts | 9 +-- apps/mini-user/src/pages/login/index.tsx | 30 ++++++--- .../src/pages/order-confirm/index.tsx | 29 ++++++++- .../src/modules/iam/auth.service.ts | 61 +++++++++---------- .../src/modules/trade/trade.controller.ts | 3 - 杜康好客-v3-PRD.md | 2 +- 杜康好客-v3-现状对照.md | 2 +- 14 files changed, 167 insertions(+), 71 deletions(-) diff --git a/.cursor/skills/dukang-coding/reference-backend.md b/.cursor/skills/dukang-coding/reference-backend.md index 58e3836..2bf3cc1 100644 --- a/.cursor/skills/dukang-coding/reference-backend.md +++ b/.cursor/skills/dukang-coding/reference-backend.md @@ -23,7 +23,7 @@ | Guard | 用途 | |-------|------| | JwtAuthGuard | 需登录 | -| PhoneVerifiedGuard | C 端需绑定手机(下单/支付) | +| PhoneVerifiedGuard | (已废弃强制)C 端手机号改为下单页可选绑定 | | OptionalJwtAuthGuard | 可选登录(bootstrap) | Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。 diff --git a/.cursor/skills/dukang-v3/reference-req-index.md b/.cursor/skills/dukang-v3/reference-req-index.md index fe44cc0..6e9287c 100644 --- a/.cursor/skills/dukang-v3/reference-req-index.md +++ b/.cursor/skills/dukang-v3/reference-req-index.md @@ -7,7 +7,7 @@ | ID | 模块 | 结论摘要 | |----|------|----------| | REQ-U-001 | 导航 | 首页/门店/好客权益/个人中心 | -| REQ-U-002 | 登录 | 无感登录;下单强制手机号;微信/短信;7 天免登 | +| REQ-U-002 | 登录 | 无感登录;下单提示绑定手机号(可选);微信/短信;7 天免登 | | REQ-U-003 | 支付 | 仅微信支付 | | REQ-U-004 | 定位 | 右上角市区;默认 IP;可授权精确定位 | | REQ-U-005 | 首页商品 | 4 款酒祖杜康;大图、标题加粗、价格标红、权益金额 | diff --git a/apps/h5-user/src/components/PhoneVerifySheet.tsx b/apps/h5-user/src/components/PhoneVerifySheet.tsx index 6db9910..ed6ad19 100644 --- a/apps/h5-user/src/components/PhoneVerifySheet.tsx +++ b/apps/h5-user/src/components/PhoneVerifySheet.tsx @@ -105,8 +105,8 @@ export default function PhoneVerifySheet({ const sheetDesc = description ?? (mode === 'wechat_bind_phone' - ? '微信授权成功,请绑定手机号以完成支付' - : '下单前需验证手机号,以便接收订单通知'); + ? '建议绑定手机号,便于订单通知与售后;关闭可跳过继续支付' + : '建议绑定手机号,便于订单通知与售后;关闭可跳过继续下单'); return (
@@ -159,6 +159,9 @@ export default function PhoneVerifySheet({ +
); diff --git a/apps/h5-user/src/pages/LoginPage.tsx b/apps/h5-user/src/pages/LoginPage.tsx index 494310c..b31de3b 100644 --- a/apps/h5-user/src/pages/LoginPage.tsx +++ b/apps/h5-user/src/pages/LoginPage.tsx @@ -51,12 +51,6 @@ export default function LoginPage() { }, [wxAuthorize]); function handleWechatLoginResult(result: WechatLoginResult) { - if (result.needBindPhone && result.wxSessionKey) { - setBindMode(true); - setWxSessionKey(result.wxSessionKey); - setMsg('微信授权成功,请绑定手机号完成登录'); - return; - } if (result.accessToken) { applySession({ accessToken: result.accessToken, @@ -68,6 +62,12 @@ export default function LoginPage() { void finishLogin(navigate, returnTo); return; } + if (result.needBindPhone && result.wxSessionKey) { + setBindMode(true); + setWxSessionKey(result.wxSessionKey); + setMsg('微信授权成功,可绑定手机号(也可跳过,稍后在下单时再绑定)'); + return; + } } function ensureAgreed() { diff --git a/apps/h5-user/src/pages/OrderConfirmPage.tsx b/apps/h5-user/src/pages/OrderConfirmPage.tsx index aa9a650..6d5f58b 100644 --- a/apps/h5-user/src/pages/OrderConfirmPage.tsx +++ b/apps/h5-user/src/pages/OrderConfirmPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import SubPageHeader from '../components/SubPageHeader'; import AppImage from '@dukang/shared-ui/AppImage'; @@ -73,6 +73,7 @@ export default function OrderConfirmPage() { const [previewLoading, setPreviewLoading] = useState(false); const [loading, setLoading] = useState(false); const [msg, setMsg] = useState(''); + const phonePromptSkipped = useRef(false); useEffect(() => { request('USER_H5', '/user/addresses').then((list) => { @@ -195,7 +196,7 @@ export default function OrderConfirmPage() { setMsg('请选择收货地址'); return; } - if (!phoneVerified) { + if (!phoneVerified && !phonePromptSkipped.current) { setPendingSubmit(true); setShowPhoneVerify(true); return; @@ -204,6 +205,7 @@ export default function OrderConfirmPage() { const authResult = await ensureWechatAuthForPay(); if (!authResult.ok) { if ('needBindPhone' in authResult) { + // 兼容旧接口:微信授权后提示可选绑定 setWxSessionKey(authResult.wxSessionKey); setShowWechatBindPhone(true); setPendingSubmit(true); @@ -418,9 +420,35 @@ export default function OrderConfirmPage() { { setShowPhoneVerify(false); - setPendingSubmit(false); + if (pendingSubmit) { + phonePromptSkipped.current = true; + setPendingSubmit(false); + void (async () => { + setLoading(true); + setMsg(''); + try { + const authResult = await ensureWechatAuthForPay(); + if (!authResult.ok) { + if ('needBindPhone' in authResult) { + setWxSessionKey(authResult.wxSessionKey); + setShowWechatBindPhone(true); + setPendingSubmit(true); + return; + } + return; + } + await doSubmit(); + } catch (e) { + setMsg(e instanceof Error ? e.message : '下单失败'); + } finally { + setLoading(false); + } + })(); + } }} onSuccess={handlePhoneVerified} /> @@ -430,10 +458,26 @@ export default function OrderConfirmPage() { mode="wechat_bind_phone" wxSessionKey={wxSessionKey ?? undefined} defaultPhone={selectedAddress?.phone} + title="建议绑定手机号" + description="绑定后便于订单通知与售后联系;关闭即可跳过继续下单。" onClose={() => { setShowWechatBindPhone(false); setWxSessionKey(null); - setPendingSubmit(false); + if (pendingSubmit) { + phonePromptSkipped.current = true; + setPendingSubmit(false); + void (async () => { + setLoading(true); + setMsg(''); + try { + await doSubmit(); + } catch (e) { + setMsg(e instanceof Error ? e.message : '下单失败'); + } finally { + setLoading(false); + } + })(); + } }} onSuccess={handleWechatPhoneBound} /> diff --git a/apps/h5-user/src/styles.css b/apps/h5-user/src/styles.css index 7c68771..89e617c 100644 --- a/apps/h5-user/src/styles.css +++ b/apps/h5-user/src/styles.css @@ -5663,6 +5663,18 @@ line-height: 1.5; } +.phone-verify-skip { + display: block; + width: 100%; + margin-top: 12px; + padding: 10px; + border: none; + background: transparent; + font-size: 14px; + color: var(--color-on-surface-variant, #888); + cursor: pointer; +} + .login-msg--hint { color: var(--color-success, #2d6a4f); } diff --git a/apps/mini-user/src/components/WechatShareBootstrap.tsx b/apps/mini-user/src/components/WechatShareBootstrap.tsx index a101541..4a2070a 100644 --- a/apps/mini-user/src/components/WechatShareBootstrap.tsx +++ b/apps/mini-user/src/components/WechatShareBootstrap.tsx @@ -68,13 +68,6 @@ export default function WechatShareBootstrap() { handleWechatAuthCallback() .then((result) => { if (!result) return; - if (result.needBindPhone && result.wxSessionKey) { - goLogin(returnFromLogin, { - bindMode: '1', - wxSessionKey: result.wxSessionKey, - }); - return; - } if (saveWechatLoginResult(result)) { toast('微信授权成功', 'success'); if ( @@ -83,6 +76,14 @@ export default function WechatShareBootstrap() { ) { finishLoginNavigate(returnFromLogin || params.get('return') || undefined); } + return; + } + // 兼容旧接口:仅 needBindPhone 时引导可选绑定,不阻塞浏览 + if (result.needBindPhone && result.wxSessionKey) { + goLogin(returnFromLogin, { + bindMode: '1', + wxSessionKey: result.wxSessionKey, + }); } }) .catch((e) => { diff --git a/apps/mini-user/src/lib/pay-ready.ts b/apps/mini-user/src/lib/pay-ready.ts index 704dfa7..25c95cb 100644 --- a/apps/mini-user/src/lib/pay-ready.ts +++ b/apps/mini-user/src/lib/pay-ready.ts @@ -5,7 +5,7 @@ import { fetchClientConfig, fetchUserProfile, needsWechatAuthForPay } from './pa /** * 支付前门禁: - * - 未登录 / 未验手机 → 跳转登录页 + * - 未登录 → 跳转登录页(微信授权即可,不强制手机号) * - H5 微信内缺 openId → 尝试 OAuth(可能跳转微信授权页) * - 小程序缺绑定 → 跳转登录页 needWechat */ @@ -17,10 +17,6 @@ export async function ensurePayReady(returnPath: string): Promise { try { const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]); - if (!profile.phoneVerified) { - goLogin(returnPath, { needPhone: '1' }); - return false; - } if (!needsWechatAuthForPay(config, profile)) { return true; } @@ -28,8 +24,9 @@ export async function ensurePayReady(returnPath: string): Promise { if (process.env.TARO_ENV === 'h5') { const auth = await ensureWechatAuthForPay(); if (auth.ok) return true; + // 旧版 needBindPhone 已不再返回;缺 openId 时走登录补微信绑定 if ('needBindPhone' in auth && auth.needBindPhone) { - goLogin(returnPath, { bindMode: '1', wxSessionKey: auth.wxSessionKey }); + goLogin(returnPath, { needWechat: '1' }); return false; } // redirecting:正在跳转微信 OAuth diff --git a/apps/mini-user/src/pages/login/index.tsx b/apps/mini-user/src/pages/login/index.tsx index c5617f7..2a10047 100644 --- a/apps/mini-user/src/pages/login/index.tsx +++ b/apps/mini-user/src/pages/login/index.tsx @@ -121,17 +121,18 @@ export default function LoginPage() { } function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) { + // 微信授权成功即登录;手机号改为下单页可选绑定 + if (result.accessToken) { + applySessionAndLeave(result, undefined, wxInfo); + return; + } if (result.needBindPhone && result.wxSessionKey) { setBindMode(true); setWxSessionKey(result.wxSessionKey); - setMsg('微信授权成功,请绑定手机号完成登录'); + setMsg('微信授权成功,可绑定手机号(也可跳过,稍后在下单时再绑定)'); setSentHint(''); return; } - if (result.accessToken) { - applySessionAndLeave(result, undefined, wxInfo); - return; - } setMsg('微信登录未完成,请重试或使用手机号登录'); } @@ -286,13 +287,17 @@ export default function LoginPage() { {completeMode === 'phone' - ? '完成手机验证' + ? '建议绑定手机号' : completeMode === 'wechat' ? '完成微信授权' : '欢迎来到杜康好客'} - {completeMode ? '完成后将返回继续支付' : '买美酒,享好礼'} + {completeMode === 'phone' + ? '便于订单通知与售后,也可稍后绑定' + : completeMode === 'wechat' + ? '完成后将返回继续支付' + : '买美酒,享好礼'} @@ -365,6 +370,17 @@ export default function LoginPage() { : '登录'} + {completeMode === 'phone' ? ( + finishLoginNavigate(returnTo)} + style={{ marginTop: 12, textAlign: 'center' }} + > + + 暂不绑定,继续下单 + + + ) : null} )} diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx index 946f175..851a501 100644 --- a/apps/mini-user/src/pages/order-confirm/index.tsx +++ b/apps/mini-user/src/pages/order-confirm/index.tsx @@ -1,12 +1,14 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { View, Text, Image } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; +import { goLogin } from '../../lib/auth-nav'; import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav'; import { tryGetClientGpsLocation } from '../../lib/client-location'; import { maskPhone } from '../../lib/phone'; import { ensurePayReady } from '../../lib/pay-ready'; +import { fetchUserProfile } from '../../lib/pay-wechat'; import { request } from '../../lib/api'; import { getProductMainImage } from '../../lib/product-images'; @@ -58,6 +60,7 @@ export default function OrderConfirmPage() { const [previewLoading, setPreviewLoading] = useState(false); const [loading, setLoading] = useState(false); const [msg, setMsg] = useState(''); + const phonePromptSkipped = useRef(false); useEffect(() => { request('/user/addresses') @@ -161,6 +164,30 @@ export default function OrderConfirmPage() { } const returnPath = `/pages/order-confirm/index?productId=${productId}&qty=${quantity}&addressId=${addressId}${forceCross ? '&cross=1' : ''}`; + + if (!phonePromptSkipped.current) { + try { + const profile = await fetchUserProfile(); + if (!profile.phoneVerified) { + const { confirm, cancel } = await Taro.showModal({ + title: '建议绑定手机号', + content: '绑定后便于订单通知与售后联系;也可跳过,不绑定也能继续下单。', + confirmText: '去绑定', + cancelText: '暂不绑定', + }); + if (confirm) { + goLogin(returnPath, { needPhone: '1' }); + return; + } + if (cancel) { + phonePromptSkipped.current = true; + } + } + } catch { + /* 拉取档案失败不阻塞下单 */ + } + } + const ready = await ensurePayReady(returnPath); if (!ready) return; diff --git a/server/dukang-api/src/modules/iam/auth.service.ts b/server/dukang-api/src/modules/iam/auth.service.ts index cb22ad4..3be0690 100644 --- a/server/dukang-api/src/modules/iam/auth.service.ts +++ b/server/dukang-api/src/modules/iam/auth.service.ts @@ -1059,7 +1059,8 @@ export class AuthService { include: { avatar: true }, }); - if (user?.phone && user.phoneVerifiedAt) { + // 微信登录不再强制绑定手机号;phoneVerified=false 也可签发会话,下单页仅提示可选绑定 + if (user) { let activeUser: UserRow = guestId && guestId !== user.id ? await this.mergeUsers(guestId, user.id) : (user as UserRow); activeUser = await this.prisma.user.update({ @@ -1087,7 +1088,7 @@ export class AuthService { if (guestId) { try { const guest = await this.assertActiveUser(guestId); - if (guest.phoneVerifiedAt && !guest.wxOpenId) { + if (!guest.wxOpenId) { const activeUser = await this.attachWechatToUser(guest.id, session, clientApp, platform); return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey); } @@ -1096,36 +1097,34 @@ export class AuthService { } } - const wxSessionKey = randomUUID(); - await this.redis.setJson( - `wx:session:${wxSessionKey}`, - { - openId: session.openId, - unionId: session.unionId, - sessionKey: 'sessionKey' in session ? session.sessionKey : undefined, - accessToken: session.accessToken, - clientApp, - guestId: guestId?.toString(), - } satisfies WxSessionPayload, - 1800, - ); - - if (user && !user.phoneVerifiedAt) { - return { - needBindPhone: true, - wxSessionKey, - actorType: 'USER', - actorId: user.id.toString(), - phoneVerified: false, - user: this.formatUserProfile(user), - }; + let created = await this.prisma.user.create({ + data: { + userNo: generateUserNo(), + wxOpenId: session.openId, + wxUnionId: session.unionId, + nickname: '微信用户', + cityPreference: { + create: { + selectedCityCode: '410100', + selectedDistrict: '郑州市', + }, + }, + }, + include: { avatar: true }, + }); + if (session.accessToken) { + const synced = await this.syncWechatUserProfile(created.id, session.accessToken, session.openId); + if (synced) created = synced as UserRow; } - - return { - needBindPhone: true, - wxSessionKey, - phoneVerified: false, - }; + this.analyticsService.trackOneSafe(created.id, clientApp, { + eventName: 'wechat_login', + extraJson: { platform }, + }); + this.analyticsService.trackOneSafe(created.id, clientApp, { + eventName: 'login_success', + extraJson: { method: 'wechat', platform }, + }); + return this.buildSessionResponse(created, clientApp, created.deviceKey); } async bindWechatPhone( diff --git a/server/dukang-api/src/modules/trade/trade.controller.ts b/server/dukang-api/src/modules/trade/trade.controller.ts index 05a0e11..4dcc1f0 100644 --- a/server/dukang-api/src/modules/trade/trade.controller.ts +++ b/server/dukang-api/src/modules/trade/trade.controller.ts @@ -3,7 +3,6 @@ import type { Request } from 'express'; import { TradeService } from './trade.service'; import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard'; import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard'; -import { PhoneVerifiedGuard } from '../../common/guards/phone-verified.guard'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { PartnerProxyOrderCreateDto, @@ -22,7 +21,6 @@ export class TradeController { } @Post() - @UseGuards(PhoneVerifiedGuard) create(@CurrentUser() user: AuthUser, @Body() body: Record, @Req() req: Request) { return this.tradeService.createOrder(user.actorId, body as never, req); } @@ -43,7 +41,6 @@ export class TradeController { } @Post(':id/pay') - @UseGuards(PhoneVerifiedGuard) pay(@CurrentUser() user: AuthUser, @Param('id') id: string) { return this.tradeService.payOrder(user.actorId, BigInt(id), user.clientApp); } diff --git a/杜康好客-v3-PRD.md b/杜康好客-v3-PRD.md index 85bfa66..36fa540 100644 --- a/杜康好客-v3-PRD.md +++ b/杜康好客-v3-PRD.md @@ -229,7 +229,7 @@ | 模块 | 要点 | |------|------| -| 导航账号 | 四 Tab;无感登录;确认下单强制手机号;仅微信支付;7 天免登 | +| 导航账号 | 四 Tab;无感登录;确认下单提示绑定手机号(可选、不强制);仅微信支付;7 天免登 | | 商品下单 | 4 款酒祖杜康 SKU;同城≥2瓶免运费;跨城≥1箱;现场提货隐藏;锁单30分钟 | | 权益核销 | 1:1 发放;出码 3 分钟;核销前规则弹窗(OPT-011);附件一规则详情 | | 门店 | 列表/详情/搜索/省市区筛选;仅营业中展示;立即核销 | diff --git a/杜康好客-v3-现状对照.md b/杜康好客-v3-现状对照.md index d0864c7..d083911 100644 --- a/杜康好客-v3-现状对照.md +++ b/杜康好客-v3-现状对照.md @@ -99,7 +99,7 @@ | REQ | 功能 | 证据 | 备注 | |-----|------|------|------| | REQ-U-001 | 四 Tab 导航 | `src/layouts/TabLayout.tsx` | 形态为 H5 非小程序 | -| REQ-U-002 | 登录 + 下单强制手机号 | `OrderConfirmPage.tsx`、`PhoneVerifiedGuard` | ✅ | +| REQ-U-002 | 登录 + 下单提示手机号(可选) | `OrderConfirmPage` / `order-confirm`、微信登录签发会话 | ✅ | | REQ-U-003 | 微信支付 | `pay-wechat.ts` + 后端 callback | Mock/真实均有 | | REQ-U-004 | 定位/开城 | `HomePage.tsx`、`wechat-location.ts` | 🔶 H5 定位 API | | REQ-U-005~006 | 商品列表/详情 | `HomePage.tsx`、`ProductDetailPage.tsx` | ✅ seed 已对齐酒祖杜康 |