用户端不要强制用户输入手机号
This commit is contained in:
@@ -23,7 +23,7 @@
|
|||||||
| Guard | 用途 |
|
| Guard | 用途 |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
| JwtAuthGuard | 需登录 |
|
| JwtAuthGuard | 需登录 |
|
||||||
| PhoneVerifiedGuard | C 端需绑定手机(下单/支付) |
|
| PhoneVerifiedGuard | (已废弃强制)C 端手机号改为下单页可选绑定 |
|
||||||
| OptionalJwtAuthGuard | 可选登录(bootstrap) |
|
| OptionalJwtAuthGuard | 可选登录(bootstrap) |
|
||||||
|
|
||||||
Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。
|
Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| ID | 模块 | 结论摘要 |
|
| ID | 模块 | 结论摘要 |
|
||||||
|----|------|----------|
|
|----|------|----------|
|
||||||
| REQ-U-001 | 导航 | 首页/门店/好客权益/个人中心 |
|
| REQ-U-001 | 导航 | 首页/门店/好客权益/个人中心 |
|
||||||
| REQ-U-002 | 登录 | 无感登录;下单强制手机号;微信/短信;7 天免登 |
|
| REQ-U-002 | 登录 | 无感登录;下单提示绑定手机号(可选);微信/短信;7 天免登 |
|
||||||
| REQ-U-003 | 支付 | 仅微信支付 |
|
| REQ-U-003 | 支付 | 仅微信支付 |
|
||||||
| REQ-U-004 | 定位 | 右上角市区;默认 IP;可授权精确定位 |
|
| REQ-U-004 | 定位 | 右上角市区;默认 IP;可授权精确定位 |
|
||||||
| REQ-U-005 | 首页商品 | 4 款酒祖杜康;大图、标题加粗、价格标红、权益金额 |
|
| REQ-U-005 | 首页商品 | 4 款酒祖杜康;大图、标题加粗、价格标红、权益金额 |
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ export default function PhoneVerifySheet({
|
|||||||
const sheetDesc =
|
const sheetDesc =
|
||||||
description ??
|
description ??
|
||||||
(mode === 'wechat_bind_phone'
|
(mode === 'wechat_bind_phone'
|
||||||
? '微信授权成功,请绑定手机号以完成支付'
|
? '建议绑定手机号,便于订单通知与售后;关闭可跳过继续支付'
|
||||||
: '下单前需验证手机号,以便接收订单通知');
|
: '建议绑定手机号,便于订单通知与售后;关闭可跳过继续下单');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="phone-verify-overlay" role="dialog" aria-modal="true">
|
<div className="phone-verify-overlay" role="dialog" aria-modal="true">
|
||||||
@@ -159,6 +159,9 @@ export default function PhoneVerifySheet({
|
|||||||
<button type="button" className="login-sms-btn" disabled={loading} onClick={submit}>
|
<button type="button" className="login-sms-btn" disabled={loading} onClick={submit}>
|
||||||
{loading ? '验证中...' : mode === 'wechat_bind_phone' ? '确认绑定' : '确认验证'}
|
{loading ? '验证中...' : mode === 'wechat_bind_phone' ? '确认绑定' : '确认验证'}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" className="phone-verify-skip" onClick={onClose}>
|
||||||
|
暂不绑定
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,12 +51,6 @@ export default function LoginPage() {
|
|||||||
}, [wxAuthorize]);
|
}, [wxAuthorize]);
|
||||||
|
|
||||||
function handleWechatLoginResult(result: WechatLoginResult) {
|
function handleWechatLoginResult(result: WechatLoginResult) {
|
||||||
if (result.needBindPhone && result.wxSessionKey) {
|
|
||||||
setBindMode(true);
|
|
||||||
setWxSessionKey(result.wxSessionKey);
|
|
||||||
setMsg('微信授权成功,请绑定手机号完成登录');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.accessToken) {
|
if (result.accessToken) {
|
||||||
applySession({
|
applySession({
|
||||||
accessToken: result.accessToken,
|
accessToken: result.accessToken,
|
||||||
@@ -68,6 +62,12 @@ export default function LoginPage() {
|
|||||||
void finishLogin(navigate, returnTo);
|
void finishLogin(navigate, returnTo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
|
setBindMode(true);
|
||||||
|
setWxSessionKey(result.wxSessionKey);
|
||||||
|
setMsg('微信授权成功,可绑定手机号(也可跳过,稍后在下单时再绑定)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureAgreed() {
|
function ensureAgreed() {
|
||||||
|
|||||||
@@ -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 { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import SubPageHeader from '../components/SubPageHeader';
|
import SubPageHeader from '../components/SubPageHeader';
|
||||||
import AppImage from '@dukang/shared-ui/AppImage';
|
import AppImage from '@dukang/shared-ui/AppImage';
|
||||||
@@ -73,6 +73,7 @@ export default function OrderConfirmPage() {
|
|||||||
const [previewLoading, setPreviewLoading] = useState(false);
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
|
const phonePromptSkipped = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
request<Address[]>('USER_H5', '/user/addresses').then((list) => {
|
request<Address[]>('USER_H5', '/user/addresses').then((list) => {
|
||||||
@@ -195,7 +196,7 @@ export default function OrderConfirmPage() {
|
|||||||
setMsg('请选择收货地址');
|
setMsg('请选择收货地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!phoneVerified) {
|
if (!phoneVerified && !phonePromptSkipped.current) {
|
||||||
setPendingSubmit(true);
|
setPendingSubmit(true);
|
||||||
setShowPhoneVerify(true);
|
setShowPhoneVerify(true);
|
||||||
return;
|
return;
|
||||||
@@ -204,6 +205,7 @@ export default function OrderConfirmPage() {
|
|||||||
const authResult = await ensureWechatAuthForPay();
|
const authResult = await ensureWechatAuthForPay();
|
||||||
if (!authResult.ok) {
|
if (!authResult.ok) {
|
||||||
if ('needBindPhone' in authResult) {
|
if ('needBindPhone' in authResult) {
|
||||||
|
// 兼容旧接口:微信授权后提示可选绑定
|
||||||
setWxSessionKey(authResult.wxSessionKey);
|
setWxSessionKey(authResult.wxSessionKey);
|
||||||
setShowWechatBindPhone(true);
|
setShowWechatBindPhone(true);
|
||||||
setPendingSubmit(true);
|
setPendingSubmit(true);
|
||||||
@@ -418,9 +420,35 @@ export default function OrderConfirmPage() {
|
|||||||
<PhoneVerifySheet
|
<PhoneVerifySheet
|
||||||
open={showPhoneVerify}
|
open={showPhoneVerify}
|
||||||
defaultPhone={selectedAddress?.phone}
|
defaultPhone={selectedAddress?.phone}
|
||||||
|
title="建议绑定手机号"
|
||||||
|
description="绑定后便于订单通知与售后联系;关闭即可跳过,不绑定也能继续下单。"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowPhoneVerify(false);
|
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}
|
onSuccess={handlePhoneVerified}
|
||||||
/>
|
/>
|
||||||
@@ -430,10 +458,26 @@ export default function OrderConfirmPage() {
|
|||||||
mode="wechat_bind_phone"
|
mode="wechat_bind_phone"
|
||||||
wxSessionKey={wxSessionKey ?? undefined}
|
wxSessionKey={wxSessionKey ?? undefined}
|
||||||
defaultPhone={selectedAddress?.phone}
|
defaultPhone={selectedAddress?.phone}
|
||||||
|
title="建议绑定手机号"
|
||||||
|
description="绑定后便于订单通知与售后联系;关闭即可跳过继续下单。"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowWechatBindPhone(false);
|
setShowWechatBindPhone(false);
|
||||||
setWxSessionKey(null);
|
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}
|
onSuccess={handleWechatPhoneBound}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5663,6 +5663,18 @@
|
|||||||
line-height: 1.5;
|
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 {
|
.login-msg--hint {
|
||||||
color: var(--color-success, #2d6a4f);
|
color: var(--color-success, #2d6a4f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,13 +68,6 @@ export default function WechatShareBootstrap() {
|
|||||||
handleWechatAuthCallback()
|
handleWechatAuthCallback()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
if (result.needBindPhone && result.wxSessionKey) {
|
|
||||||
goLogin(returnFromLogin, {
|
|
||||||
bindMode: '1',
|
|
||||||
wxSessionKey: result.wxSessionKey,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (saveWechatLoginResult(result)) {
|
if (saveWechatLoginResult(result)) {
|
||||||
toast('微信授权成功', 'success');
|
toast('微信授权成功', 'success');
|
||||||
if (
|
if (
|
||||||
@@ -83,6 +76,14 @@ export default function WechatShareBootstrap() {
|
|||||||
) {
|
) {
|
||||||
finishLoginNavigate(returnFromLogin || params.get('return') || undefined);
|
finishLoginNavigate(returnFromLogin || params.get('return') || undefined);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 兼容旧接口:仅 needBindPhone 时引导可选绑定,不阻塞浏览
|
||||||
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
|
goLogin(returnFromLogin, {
|
||||||
|
bindMode: '1',
|
||||||
|
wxSessionKey: result.wxSessionKey,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fetchClientConfig, fetchUserProfile, needsWechatAuthForPay } from './pa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付前门禁:
|
* 支付前门禁:
|
||||||
* - 未登录 / 未验手机 → 跳转登录页
|
* - 未登录 → 跳转登录页(微信授权即可,不强制手机号)
|
||||||
* - H5 微信内缺 openId → 尝试 OAuth(可能跳转微信授权页)
|
* - H5 微信内缺 openId → 尝试 OAuth(可能跳转微信授权页)
|
||||||
* - 小程序缺绑定 → 跳转登录页 needWechat
|
* - 小程序缺绑定 → 跳转登录页 needWechat
|
||||||
*/
|
*/
|
||||||
@@ -17,10 +17,6 @@ export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
|
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
|
||||||
if (!profile.phoneVerified) {
|
|
||||||
goLogin(returnPath, { needPhone: '1' });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!needsWechatAuthForPay(config, profile)) {
|
if (!needsWechatAuthForPay(config, profile)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -28,8 +24,9 @@ export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
|||||||
if (process.env.TARO_ENV === 'h5') {
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
const auth = await ensureWechatAuthForPay();
|
const auth = await ensureWechatAuthForPay();
|
||||||
if (auth.ok) return true;
|
if (auth.ok) return true;
|
||||||
|
// 旧版 needBindPhone 已不再返回;缺 openId 时走登录补微信绑定
|
||||||
if ('needBindPhone' in auth && auth.needBindPhone) {
|
if ('needBindPhone' in auth && auth.needBindPhone) {
|
||||||
goLogin(returnPath, { bindMode: '1', wxSessionKey: auth.wxSessionKey });
|
goLogin(returnPath, { needWechat: '1' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// redirecting:正在跳转微信 OAuth
|
// redirecting:正在跳转微信 OAuth
|
||||||
|
|||||||
@@ -121,17 +121,18 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) {
|
function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) {
|
||||||
|
// 微信授权成功即登录;手机号改为下单页可选绑定
|
||||||
|
if (result.accessToken) {
|
||||||
|
applySessionAndLeave(result, undefined, wxInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (result.needBindPhone && result.wxSessionKey) {
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
setBindMode(true);
|
setBindMode(true);
|
||||||
setWxSessionKey(result.wxSessionKey);
|
setWxSessionKey(result.wxSessionKey);
|
||||||
setMsg('微信授权成功,请绑定手机号完成登录');
|
setMsg('微信授权成功,可绑定手机号(也可跳过,稍后在下单时再绑定)');
|
||||||
setSentHint('');
|
setSentHint('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.accessToken) {
|
|
||||||
applySessionAndLeave(result, undefined, wxInfo);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMsg('微信登录未完成,请重试或使用手机号登录');
|
setMsg('微信登录未完成,请重试或使用手机号登录');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,13 +287,17 @@ export default function LoginPage() {
|
|||||||
<View className="login-welcome">
|
<View className="login-welcome">
|
||||||
<Text className="login-welcome-title">
|
<Text className="login-welcome-title">
|
||||||
{completeMode === 'phone'
|
{completeMode === 'phone'
|
||||||
? '完成手机验证'
|
? '建议绑定手机号'
|
||||||
: completeMode === 'wechat'
|
: completeMode === 'wechat'
|
||||||
? '完成微信授权'
|
? '完成微信授权'
|
||||||
: '欢迎来到杜康好客'}
|
: '欢迎来到杜康好客'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="login-welcome-sub">
|
<Text className="login-welcome-sub">
|
||||||
{completeMode ? '完成后将返回继续支付' : '买美酒,享好礼'}
|
{completeMode === 'phone'
|
||||||
|
? '便于订单通知与售后,也可稍后绑定'
|
||||||
|
: completeMode === 'wechat'
|
||||||
|
? '完成后将返回继续支付'
|
||||||
|
: '买美酒,享好礼'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -365,6 +370,17 @@ export default function LoginPage() {
|
|||||||
: '登录'}
|
: '登录'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{completeMode === 'phone' ? (
|
||||||
|
<View
|
||||||
|
className="login-skip-bind"
|
||||||
|
onClick={() => finishLoginNavigate(returnTo)}
|
||||||
|
style={{ marginTop: 12, textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<Text className="u-muted" style={{ fontSize: 14 }}>
|
||||||
|
暂不绑定,继续下单
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { View, Text, Image } from '@tarojs/components';
|
||||||
import Taro, { useRouter } from '@tarojs/taro';
|
import Taro, { useRouter } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
||||||
import { tryGetClientGpsLocation } from '../../lib/client-location';
|
import { tryGetClientGpsLocation } from '../../lib/client-location';
|
||||||
import { maskPhone } from '../../lib/phone';
|
import { maskPhone } from '../../lib/phone';
|
||||||
import { ensurePayReady } from '../../lib/pay-ready';
|
import { ensurePayReady } from '../../lib/pay-ready';
|
||||||
|
import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||||
import { request } from '../../lib/api';
|
import { request } from '../../lib/api';
|
||||||
import { getProductMainImage } from '../../lib/product-images';
|
import { getProductMainImage } from '../../lib/product-images';
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ export default function OrderConfirmPage() {
|
|||||||
const [previewLoading, setPreviewLoading] = useState(false);
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
|
const phonePromptSkipped = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
request<Address[]>('/user/addresses')
|
request<Address[]>('/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' : ''}`;
|
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);
|
const ready = await ensurePayReady(returnPath);
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1059,7 +1059,8 @@ export class AuthService {
|
|||||||
include: { avatar: true },
|
include: { avatar: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user?.phone && user.phoneVerifiedAt) {
|
// 微信登录不再强制绑定手机号;phoneVerified=false 也可签发会话,下单页仅提示可选绑定
|
||||||
|
if (user) {
|
||||||
let activeUser: UserRow =
|
let activeUser: UserRow =
|
||||||
guestId && guestId !== user.id ? await this.mergeUsers(guestId, user.id) : (user as UserRow);
|
guestId && guestId !== user.id ? await this.mergeUsers(guestId, user.id) : (user as UserRow);
|
||||||
activeUser = await this.prisma.user.update({
|
activeUser = await this.prisma.user.update({
|
||||||
@@ -1087,7 +1088,7 @@ export class AuthService {
|
|||||||
if (guestId) {
|
if (guestId) {
|
||||||
try {
|
try {
|
||||||
const guest = await this.assertActiveUser(guestId);
|
const guest = await this.assertActiveUser(guestId);
|
||||||
if (guest.phoneVerifiedAt && !guest.wxOpenId) {
|
if (!guest.wxOpenId) {
|
||||||
const activeUser = await this.attachWechatToUser(guest.id, session, clientApp, platform);
|
const activeUser = await this.attachWechatToUser(guest.id, session, clientApp, platform);
|
||||||
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey);
|
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey);
|
||||||
}
|
}
|
||||||
@@ -1096,36 +1097,34 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wxSessionKey = randomUUID();
|
let created = await this.prisma.user.create({
|
||||||
await this.redis.setJson(
|
data: {
|
||||||
`wx:session:${wxSessionKey}`,
|
userNo: generateUserNo(),
|
||||||
{
|
wxOpenId: session.openId,
|
||||||
openId: session.openId,
|
wxUnionId: session.unionId,
|
||||||
unionId: session.unionId,
|
nickname: '微信用户',
|
||||||
sessionKey: 'sessionKey' in session ? session.sessionKey : undefined,
|
cityPreference: {
|
||||||
accessToken: session.accessToken,
|
create: {
|
||||||
clientApp,
|
selectedCityCode: '410100',
|
||||||
guestId: guestId?.toString(),
|
selectedDistrict: '郑州市',
|
||||||
} satisfies WxSessionPayload,
|
},
|
||||||
1800,
|
},
|
||||||
);
|
},
|
||||||
|
include: { avatar: true },
|
||||||
if (user && !user.phoneVerifiedAt) {
|
});
|
||||||
return {
|
if (session.accessToken) {
|
||||||
needBindPhone: true,
|
const synced = await this.syncWechatUserProfile(created.id, session.accessToken, session.openId);
|
||||||
wxSessionKey,
|
if (synced) created = synced as UserRow;
|
||||||
actorType: 'USER',
|
|
||||||
actorId: user.id.toString(),
|
|
||||||
phoneVerified: false,
|
|
||||||
user: this.formatUserProfile(user),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
this.analyticsService.trackOneSafe(created.id, clientApp, {
|
||||||
return {
|
eventName: 'wechat_login',
|
||||||
needBindPhone: true,
|
extraJson: { platform },
|
||||||
wxSessionKey,
|
});
|
||||||
phoneVerified: false,
|
this.analyticsService.trackOneSafe(created.id, clientApp, {
|
||||||
};
|
eventName: 'login_success',
|
||||||
|
extraJson: { method: 'wechat', platform },
|
||||||
|
});
|
||||||
|
return this.buildSessionResponse(created, clientApp, created.deviceKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bindWechatPhone(
|
async bindWechatPhone(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Request } from 'express';
|
|||||||
import { TradeService } from './trade.service';
|
import { TradeService } from './trade.service';
|
||||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.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 { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import {
|
import {
|
||||||
PartnerProxyOrderCreateDto,
|
PartnerProxyOrderCreateDto,
|
||||||
@@ -22,7 +21,6 @@ export class TradeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(PhoneVerifiedGuard)
|
|
||||||
create(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>, @Req() req: Request) {
|
create(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>, @Req() req: Request) {
|
||||||
return this.tradeService.createOrder(user.actorId, body as never, req);
|
return this.tradeService.createOrder(user.actorId, body as never, req);
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,6 @@ export class TradeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/pay')
|
@Post(':id/pay')
|
||||||
@UseGuards(PhoneVerifiedGuard)
|
|
||||||
pay(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
pay(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||||
return this.tradeService.payOrder(user.actorId, BigInt(id), user.clientApp);
|
return this.tradeService.payOrder(user.actorId, BigInt(id), user.clientApp);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -229,7 +229,7 @@
|
|||||||
|
|
||||||
| 模块 | 要点 |
|
| 模块 | 要点 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 导航账号 | 四 Tab;无感登录;确认下单强制手机号;仅微信支付;7 天免登 |
|
| 导航账号 | 四 Tab;无感登录;确认下单提示绑定手机号(可选、不强制);仅微信支付;7 天免登 |
|
||||||
| 商品下单 | 4 款酒祖杜康 SKU;同城≥2瓶免运费;跨城≥1箱;现场提货隐藏;锁单30分钟 |
|
| 商品下单 | 4 款酒祖杜康 SKU;同城≥2瓶免运费;跨城≥1箱;现场提货隐藏;锁单30分钟 |
|
||||||
| 权益核销 | 1:1 发放;出码 3 分钟;核销前规则弹窗(OPT-011);附件一规则详情 |
|
| 权益核销 | 1:1 发放;出码 3 分钟;核销前规则弹窗(OPT-011);附件一规则详情 |
|
||||||
| 门店 | 列表/详情/搜索/省市区筛选;仅营业中展示;立即核销 |
|
| 门店 | 列表/详情/搜索/省市区筛选;仅营业中展示;立即核销 |
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@
|
|||||||
| REQ | 功能 | 证据 | 备注 |
|
| REQ | 功能 | 证据 | 备注 |
|
||||||
|-----|------|------|------|
|
|-----|------|------|------|
|
||||||
| REQ-U-001 | 四 Tab 导航 | `src/layouts/TabLayout.tsx` | 形态为 H5 非小程序 |
|
| 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-003 | 微信支付 | `pay-wechat.ts` + 后端 callback | Mock/真实均有 |
|
||||||
| REQ-U-004 | 定位/开城 | `HomePage.tsx`、`wechat-location.ts` | 🔶 H5 定位 API |
|
| REQ-U-004 | 定位/开城 | `HomePage.tsx`、`wechat-location.ts` | 🔶 H5 定位 API |
|
||||||
| REQ-U-005~006 | 商品列表/详情 | `HomePage.tsx`、`ProductDetailPage.tsx` | ✅ seed 已对齐酒祖杜康 |
|
| REQ-U-005~006 | 商品列表/详情 | `HomePage.tsx`、`ProductDetailPage.tsx` | ✅ seed 已对齐酒祖杜康 |
|
||||||
|
|||||||
Reference in New Issue
Block a user