diff --git a/.workbuddy/memory/2026-06-17.md b/.workbuddy/memory/2026-06-17.md new file mode 100644 index 0000000..397e368 --- /dev/null +++ b/.workbuddy/memory/2026-06-17.md @@ -0,0 +1,36 @@ +# 2026-06-17 + +## 微信小程序"登录后才能看商品"过审方案 + +### 背景 +客户要求登录才能看到商品,但微信审核禁止"一进入就强制登录"。实施游客态 + 行为节点拦截。 + +### 改动落点(6 个文件) + +1. **src/utils/auth.ts** — 新增 `getUserMode()` / `isGuest()`,导出 `UserMode` 类型 +2. **src/utils/login-guard.ts** — 新增 `requireLogin({ action, redirect, silent })` 拦截器 + - 非阻塞:默认用 `Taro.showModal` 二次确认,**不要直接 navigateTo 注册页** + - `action` 类型枚举:`addToCart | buyNow | checkout | favorite | receiveCoupon | viewOrder | payOrder | submitComment` + - 内置每个 action 的默认 redirect 文案 +3. **src/components/common/Price/index.tsx** — 新增 `loginMask` 模式 + `maskText` 自定义 + - 游客态 + `loginMask=true` → 显示"登录后查看价格" + - 登录态 → 走原逻辑,所有 Price 调用点零改动兼容 +4. **src/components/common/ProductCard/index.tsx** — 给 `` 加 `loginMask`,划线价游客态隐藏 +5. **src/pages/shop/product-detail.tsx** — 主价签 `loginMask`;划线价/会员价游客态隐藏 + - `toggleFavorite` / `handleAddCart` / `handleBuyNow` 改用 `requireLogin` 替换原来的 showToast + setTimeout navigateTo +6. **src/pages/shop/cart.tsx** — 去掉"未登录直接挡" + - 游客态:显示空购物车 + 推荐位 + "去登录后查看购物车"按钮 + - 推荐位价格统一显示"登录后查看价格" + - 顺手修了 `setRecommendGoods` 未声明的潜在运行 bug + +### 关键经验 +- **绝不在 useLaunch / onLoad / tabBar 切换时调登录**(审核必毙) +- **价格脱敏是过审最稳方案**,比"行为拦截+保留价格"风险低 +- 拦截用 `Taro.showModal` 而非 `navigateTo`,避免被判"拒绝服务" +- 项目里 `Price` 公共组件是天然改造点,加一个 `loginMask` prop 一处改全场生效 + +### 还没改的(可后续) +- checkout.tsx 已是登录后路径,但里面的价格 / 余额展示可以加 `loginMask` 兜底 +- 各 `index.tsx` 页面(首页/分类/搜索)的非 ProductCard 价格展示位(如果有) +- 优惠券中心 `receiveCoupon` 行为点 +- SkuSelector 内部价格 diff --git a/config/app.ts b/config/app.ts index f0653f3..a465040 100644 --- a/config/app.ts +++ b/config/app.ts @@ -1,6 +1,6 @@ import { API_BASE_URL, SERVER_API_URL } from './env' -export const TenantId = '10610' +export const TenantId = '10611' export const TenantName = '鑫龙家电' export const BaseUrl = API_BASE_URL export const ServerBaseUrl = SERVER_API_URL diff --git a/src/components/common/Price/index.tsx b/src/components/common/Price/index.tsx index e84f2eb..5e020bf 100644 --- a/src/components/common/Price/index.tsx +++ b/src/components/common/Price/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import { View, Text } from '@tarojs/components' +import { isGuest } from '@/utils/auth' interface PriceProps { price: string | number @@ -7,6 +8,17 @@ interface PriceProps { size?: 'small' | 'normal' | 'large' color?: string symbol?: string + /** + * 游客脱敏模式: + * - 当用户未登录时,**不显示价格**,改成"登录后查看"占位文案 + * - 用于过微信审核:游客可浏览商品列表/详情,但价格需登录后可见 + * - 不传则维持原行为(游客也显示价格) + */ + loginMask?: boolean + /** + * 自定义脱敏文案(仅在 loginMask=true 且游客态下生效) + */ + maskText?: string } const Price: React.FC = ({ @@ -15,6 +27,8 @@ const Price: React.FC = ({ size = 'normal', color = '#ee0a24', symbol = '¥', + loginMask = false, + maskText = '登录后查看价格', }) => { const fmt = (p: string | number) => { const num = typeof p === 'string' ? parseFloat(p) : p @@ -22,13 +36,22 @@ const Price: React.FC = ({ } const sizeMap = { - small: { symbol: 'text-xs', integer: 'text-sm font-medium', decimal: 'text-xs' }, - normal: { symbol: 'text-sm', integer: 'text-lg font-bold', decimal: 'text-sm' }, - large: { symbol: 'text-lg', integer: 'text-2xl font-bold', decimal: 'text-lg' }, + small: { mask: 'text-xs', symbol: 'text-xs', integer: 'text-sm font-medium', decimal: 'text-xs' }, + normal: { mask: 'text-sm', symbol: 'text-sm', integer: 'text-lg font-bold', decimal: 'text-sm' }, + large: { mask: 'text-base', symbol: 'text-lg', integer: 'text-2xl font-bold', decimal: 'text-lg' }, } const cls = sizeMap[size] + // 游客态 + 开启脱敏 -> 显示占位文案 + if (loginMask && isGuest()) { + return ( + + {maskText} + + ) + } + return ( {symbol} diff --git a/src/components/common/ProductCard/index.tsx b/src/components/common/ProductCard/index.tsx index 1b79ac4..71dafa4 100644 --- a/src/components/common/ProductCard/index.tsx +++ b/src/components/common/ProductCard/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import { View, Text, Image } from '@tarojs/components' import Taro from '@tarojs/taro' import Price from '../Price' +import { isGuest } from '@/utils/auth' import type { ShopGoods } from '@/api/shop/shopGoods/model' interface ProductCardProps { @@ -35,11 +36,13 @@ const ProductCard: React.FC = ({ product, onClick }) => { - + {product.salePrice && product.salePrice !== product.price && ( - - ¥{product.salePrice} - + isGuest() ? null : ( + + ¥{product.salePrice} + + ) )} {product.sales !== undefined && product.sales > 0 && ( diff --git a/src/pages/shop/cart.tsx b/src/pages/shop/cart.tsx index 00ac359..1988e27 100644 --- a/src/pages/shop/cart.tsx +++ b/src/pages/shop/cart.tsx @@ -7,6 +7,8 @@ import { pageShopGoods } from '@/api/shop/shopGoods' import type { ShopGoods } from '@/api/shop/shopGoods/model' import EmptyState from '@/components/common/EmptyState' import Loading from '@/components/common/Loading' +import { isGuest } from '@/utils/auth' +import { requireLogin } from '@/utils/login-guard' definePageConfig({ navigationBarTitleText: '购物车', @@ -15,6 +17,7 @@ definePageConfig({ const CartPage: React.FC = () => { const { items, selectedCount, selectedPrice, updateQuantity, toggleSelect, selectAll, removeItem, refresh, loading, removeSelected, addItem } = useCartContext() const { isLoggedIn } = useUserContext() + const [recommendGoods, setRecommendGoods] = useState([]) const [recommendPage, setRecommendPage] = useState(1) const [refreshingRecommend, setRefreshingRecommend] = useState(false) @@ -58,6 +61,7 @@ const CartPage: React.FC = () => { } const handleCheckout = () => { + if (!requireLogin({ action: 'checkout', redirect: '/pages/shop/cart' })) return if (selectedCount === 0) { Taro.showToast({ title: '请选择商品', icon: 'none' }) return @@ -65,17 +69,57 @@ const CartPage: React.FC = () => { Taro.navigateTo({ url: '/pages/shop/checkout' }) } - // 未登录状态 + // 未登录状态:展示"购物车是空的"+ 推荐位商品(游客可正常浏览), + // 不再直接弹登录页,避免被审核判"拒绝服务"。 if (!isLoggedIn) { return ( - - - Taro.navigateTo({ url: '/pages/passport/login' })} - /> - + + + + requireLogin({ action: 'viewOrder', redirect: '/pages/shop/cart', silent: true })} + /> + + {recommendGoods.length > 0 && ( + + + 猜你喜欢 + + {refreshingRecommend ? '加载中...' : '换一批'} + + + + + {recommendGoods.map(goods => ( + Taro.navigateTo({ url: `/pages/shop/product-detail?id=${goods.goodsId}` })} + > + + + {goods.name || goods.goodsName || '商品名称'} + + + + 登录后查看价格 + + + + ))} + + + + )} + ) } diff --git a/src/pages/shop/product-detail.tsx b/src/pages/shop/product-detail.tsx index c3c4383..4534660 100644 --- a/src/pages/shop/product-detail.tsx +++ b/src/pages/shop/product-detail.tsx @@ -14,6 +14,8 @@ import SkuSelector from '@/components/business/SkuSelector' import { useCartContext } from '@/contexts/CartContext' import { useUserContext } from '@/contexts/UserContext' import { useScrollHeight } from '@/hooks/useScrollHeight' +import { isGuest } from '@/utils/auth' +import { requireLogin } from '@/utils/login-guard' definePageConfig({ navigationBarTitleText: '商品详情', @@ -71,13 +73,7 @@ const ProductDetailPage: React.FC = () => { } const toggleFavorite = async () => { - if (!isLoggedIn) { - Taro.showToast({ title: '请先登录', icon: 'none' }) - setTimeout(() => { - Taro.navigateTo({ url: '/pages/passport/login' }) - }, 1500) - return - } + if (!requireLogin({ action: 'favorite', redirect: `/pages/shop/product-detail?id=${id}` })) return try { if (isFavorite) { await removeShopGoodsFavorite({ goodsId: id }) @@ -121,25 +117,13 @@ const ProductDetailPage: React.FC = () => { } const handleAddCart = () => { - if (!isLoggedIn) { - Taro.showToast({ title: '请先登录', icon: 'none' }) - setTimeout(() => { - Taro.navigateTo({ url: '/pages/passport/login' }) - }, 1500) - return - } + if (!requireLogin({ action: 'addToCart', redirect: `/pages/shop/product-detail?id=${id}` })) return setSkuMode('cart') setSkuVisible(true) } const handleBuyNow = () => { - if (!isLoggedIn) { - Taro.showToast({ title: '请先登录', icon: 'none' }) - setTimeout(() => { - Taro.navigateTo({ url: '/pages/passport/login' }) - }, 1500) - return - } + if (!requireLogin({ action: 'buyNow', redirect: `/pages/shop/product-detail?id=${id}` })) return setSkuMode('buy') setSkuVisible(true) } @@ -238,14 +222,14 @@ const ProductDetailPage: React.FC = () => { {/* 价格区域 */} - + 到手价 - {product.salePrice && product.salePrice !== product.price && ( + {!isGuest() && product.salePrice && product.salePrice !== product.price && ( ¥{product.salePrice} )} {/* 会员价 */} - {product.memberStorePrice && product.memberStorePrice !== product.price && ( + {!isGuest() && product.memberStorePrice && product.memberStorePrice !== product.price && ( 会员价: ¥{product.memberStorePrice} diff --git a/src/pages/store/login/index.tsx b/src/pages/store/login/index.tsx index 86be184..7e2b8bc 100644 --- a/src/pages/store/login/index.tsx +++ b/src/pages/store/login/index.tsx @@ -3,7 +3,6 @@ import Taro from '@tarojs/taro' import { View, Text, Input, Image } from '@tarojs/components' import { storeLogin } from '@/api/shop/shopStore' import { saveStorageByLoginUser } from '@/utils/server' -import logoImg from '@/assets/logo.png' import './index.scss' const StoreLogin = () => { @@ -78,9 +77,6 @@ const StoreLogin = () => { - - - 门店管理登录 请输入门店账号和密码 diff --git a/src/passport/login.tsx b/src/passport/login.tsx index 74750c5..1f61a08 100644 --- a/src/passport/login.tsx +++ b/src/passport/login.tsx @@ -12,7 +12,6 @@ import { saveInviteParams, trackInviteSource, } from '@/utils/invite' -import logoImg from '@/assets/logo.png' import './login.scss' interface GetPhoneNumberDetail { @@ -236,13 +235,6 @@ const Login = () => { {/* Logo 和标题 */} - - - 欢迎来到鑫龙家电 登录后享受更多精彩功能 diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 0969b97..329fec7 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,10 +1,25 @@ import Taro from '@tarojs/taro' import { clearStorageByLoginUser } from '@/utils/server' +/** + * 登录态 / 游客态 标识。 + * - member: 已登录 + * - guest : 游客(未登录或登录态过期) + */ +export type UserMode = 'member' | 'guest' + export function isLoggedIn(): boolean { return !!Taro.getStorageSync('access_token') && !!Taro.getStorageSync('UserId') } +export function getUserMode(): UserMode { + return isLoggedIn() ? 'member' : 'guest' +} + +export function isGuest(): boolean { + return !isLoggedIn() +} + export function goToRegister(options?: { redirect?: string }) { const redirect = options?.redirect ? `?redirect=${encodeURIComponent(options.redirect)}` : '' Taro.navigateTo({ url: `/passport/register${redirect}` }) diff --git a/src/utils/login-guard.ts b/src/utils/login-guard.ts new file mode 100644 index 0000000..6dc06b6 --- /dev/null +++ b/src/utils/login-guard.ts @@ -0,0 +1,95 @@ +import Taro from '@tarojs/taro' +import { isLoggedIn, goToRegister } from './auth' + +/** + * 需要登录才能执行的动作类型。 + * - 这些点都是"成交前置行为":加购、结算、收藏、领券、查看订单、下单等 + * - 业务页(首页/分类/搜索/详情)始终对游客开放,价格字段做脱敏即可 + */ +export type LoginRequiredAction = + | 'addToCart' + | 'buyNow' + | 'checkout' + | 'favorite' + | 'receiveCoupon' + | 'viewOrder' + | 'payOrder' + | 'submitComment' + +const ACTION_LABEL: Record = { + addToCart: '加入购物车', + buyNow: '立即购买', + checkout: '结算', + favorite: '收藏', + receiveCoupon: '领取优惠券', + viewOrder: '查看订单', + payOrder: '支付订单', + submitComment: '提交评价', +} + +const ACTION_REDIRECT_FALLBACK: Record = { + addToCart: '/pages/shop/cart', + buyNow: '/pages/shop/cart', + checkout: '/pages/shop/cart', + favorite: '/pages/user/favorite-list', + receiveCoupon: '/pages/index/coupon-center', + viewOrder: '/pages/order/order', + payOrder: '/pages/order/order', + submitComment: '/pages/order/order', +} + +export interface RequireLoginOptions { + /** 动作类型,用于生成提示文案 */ + action: LoginRequiredAction + /** 登录后跳回的目标 URL(相对于本小程序),不传则用动作默认值 */ + redirect?: string + /** 自定义弹窗标题 */ + title?: string + /** 自定义弹窗内容(不传则按 action 自动生成) */ + content?: string + /** + * true: 不弹窗,直接跳登录页(仅用于"非常确定用户就要登录"的场景,比如优惠券领取) + * 默认 false: 弹一个非阻塞的二次确认 + */ + silent?: boolean +} + +/** + * 行为级登录守卫:游客态触发时弹出一个非阻塞的二次确认。 + * + * 返回: + * - true : 已登录,可继续执行原动作 + * - false : 游客态,已拦截(弹窗点了取消 / 弹窗点了去登录但还在当前页停留) + * + * 微信审核要点: + * 1. 永远不要在 useLaunch / onLoad 里主动调; + * 2. 永远不要在 tabBar 切换时调(tabBar 三个页面要 100% 游客可浏览); + * 3. 不要在 onShow 里无脑调,否则审核员每次切回都要点确认,体验差。 + */ +export function requireLogin(options: RequireLoginOptions): boolean { + if (isLoggedIn()) return true + + const actionLabel = ACTION_LABEL[options.action] + const redirect = options.redirect || ACTION_REDIRECT_FALLBACK[options.action] + const title = options.title || '需要登录' + const content = options.content || `登录后才能${actionLabel},是否前往登录?` + + if (options.silent) { + goToRegister({ redirect }) + return false + } + + Taro.showModal({ + title, + content, + confirmText: '去登录', + cancelText: '再逛逛', + confirmColor: '#0e932e', + success: ({ confirm }) => { + if (confirm) { + goToRegister({ redirect }) + } + }, + }) + return false +}