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 已对齐酒祖杜康 |