From 7708968f53c025c8b7cbae2fd4c489216eefdd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 23 Aug 2025 12:18:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(invite):=20=E9=87=8D=E6=9E=84=E9=82=80?= =?UTF-8?q?=E8=AF=B7=E5=85=B3=E7=B3=BB=E5=BB=BA=E7=AB=8B=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 bindRefereeRelation 接口替换原有的 createInviteRelation 接口 - 优化邀请参数解析逻辑,支持 uid_xxx 格式的邀请码 - 重构 handleInviteRelation 函数,使用新的绑定推荐关系接口 - 新增 checkAndHandleInviteRelation 和 manualHandleInviteRelation 函数 - 优化首页和订单列表的相关逻辑,以支持新的邀请关系建立流程 - 更新文档中的相关描述,如将"下级成员"改为"团队成员" --- docs/DEALER_OPTIMIZATION.md | 2 +- src/api/invite/index.ts | 42 +++++- src/components/AddCartBar.tsx | 4 +- src/dealer/qrcode/index.tsx | 6 +- src/dealer/team/index.tsx | 2 +- src/pages/index/index.tsx | 18 +++ src/user/order/components/OrderList.tsx | 140 ++++++++++++++----- src/user/order/order.tsx | 6 +- src/utils/invite.ts | 171 ++++++++++++++++++++++-- 9 files changed, 342 insertions(+), 49 deletions(-) diff --git a/docs/DEALER_OPTIMIZATION.md b/docs/DEALER_OPTIMIZATION.md index 9423e8c..d6f384a 100644 --- a/docs/DEALER_OPTIMIZATION.md +++ b/docs/DEALER_OPTIMIZATION.md @@ -61,7 +61,7 @@ #### 新增功能 - 成员活跃状态标识 - 贡献佣金和订单数统计 -- 下级成员数量显示 +- 团队成员数量显示 - 等级图标和颜色区分 ## 📊 技术改进 diff --git a/src/api/invite/index.ts b/src/api/invite/index.ts index 2d72ec2..9f973a1 100644 --- a/src/api/invite/index.ts +++ b/src/api/invite/index.ts @@ -33,6 +33,20 @@ export interface InviteRelationParam { inviteTime?: string; } +/** + * 绑定推荐关系参数 + */ +export interface BindRefereeParam { + // 推荐人ID + refereeId: number; + // 被推荐人ID (可选,如果不传则使用当前登录用户) + userId?: number; + // 推荐来源 + source?: string; + // 场景值 + scene?: string; +} + /** * 邀请统计数据 */ @@ -120,7 +134,7 @@ export async function generateInviteCode(inviterId: number) { } /** - * 建立邀请关系 + * 建立邀请关系 (旧接口,保留兼容性) */ export async function createInviteRelation(data: InviteRelationParam) { const res = await request.post>( @@ -133,6 +147,32 @@ export async function createInviteRelation(data: InviteRelationParam) { return Promise.reject(new Error(res.message)); } +/** + * 绑定推荐关系 (新接口) + */ +export async function bindRefereeRelation(data: BindRefereeParam) { + try { + const res = await request.post>( + '/shop/shop-dealer-referee', + { + refereeId: data.refereeId, + userId: data.userId, + source: data.source || 'qrcode', + scene: data.scene + } + ); + + if (res.code === 0) { + return res.data; + } + + throw new Error(res.message || '绑定推荐关系失败'); + } catch (error: any) { + console.error('绑定推荐关系API调用失败:', error); + throw new Error(error.message || '绑定推荐关系失败'); + } +} + /** * 处理邀请场景值 */ diff --git a/src/components/AddCartBar.tsx b/src/components/AddCartBar.tsx index def5f75..c095208 100644 --- a/src/components/AddCartBar.tsx +++ b/src/components/AddCartBar.tsx @@ -30,7 +30,7 @@ function AddCartBar() { navTo('/bszx/pay/pay?id=' + id) } } - const reload = (id) => { + const reload = (id: number) => { getCmsArticle(id).then(data => { setArticle(data) }) @@ -47,7 +47,7 @@ function AddCartBar() { useEffect(() => { const id = router?.params.id as number | undefined; setId(id) - reload(id); + reload(Number(id)); }, []); return ( diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index 789c6fa..ca482e7 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -126,9 +126,9 @@ const DealerQrcode: React.FC = () => { const inviteText = `🎉 邀请您加入我的团队! -扫描小程序码或搜索"网宿小店"小程序,即可享受优质商品和服务! +扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务! -💰 成为我的下级分销商,一起赚取丰厚佣金 +💰 成为我的团队成员,一起赚取丰厚佣金 🎁 新用户专享优惠等你来拿 邀请码:${dealerUser.userId} @@ -295,7 +295,7 @@ const DealerQrcode: React.FC = () => { - 好友通过您的二维码或链接注册成为您的下级分销商 + 好友通过您的二维码或链接注册成为您的团队成员 diff --git a/src/dealer/team/index.tsx b/src/dealer/team/index.tsx index 845516d..923aa17 100644 --- a/src/dealer/team/index.tsx +++ b/src/dealer/team/index.tsx @@ -207,7 +207,7 @@ const DealerTeam: React.FC = () => { {member.subMembers} - 下级成员 + 团队成员 diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 12b9ebe..5e7dba9 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -7,6 +7,7 @@ import {getShopInfo} from "@/api/layout"; import {Sticky} from '@nutui/nutui-react-taro' import Menu from "./Menu"; import Banner from "./Banner"; +import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite"; import './index.scss' // import GoodsList from "./GoodsList"; @@ -87,6 +88,23 @@ function Home() { getShopInfo().then(() => { }) + + // 检查是否有待处理的邀请关系 + if (hasPendingInvite()) { + console.log('检测到待处理的邀请关系') + // 延迟处理,确保用户信息已加载 + setTimeout(async () => { + try { + const success = await checkAndHandleInviteRelation() + if (success) { + console.log('首页邀请关系处理成功') + } + } catch (error) { + console.error('首页邀请关系处理失败:', error) + } + }, 2000) + } + // Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。 Taro.getSetting({ success: (res) => { diff --git a/src/user/order/components/OrderList.tsx b/src/user/order/components/OrderList.tsx index 609fb1c..dd39c76 100644 --- a/src/user/order/components/OrderList.tsx +++ b/src/user/order/components/OrderList.tsx @@ -1,5 +1,5 @@ -import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image} from '@nutui/nutui-react-taro' -import {useEffect, useState, CSSProperties} from "react"; +import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro' +import {useEffect, useState, useCallback, CSSProperties} from "react"; import {View, Text} from '@tarojs/components' import Taro from '@tarojs/taro'; import {InfiniteLoading} from '@nutui/nutui-react-taro' @@ -86,6 +86,7 @@ interface OrderListProps { onReload?: () => void; searchParams?: ShopOrderParam; showSearch?: boolean; + onSearchParamsChange?: (params: ShopOrderParam) => void; // 新增:通知父组件参数变化 } function OrderList(props: OrderListProps) { @@ -100,13 +101,17 @@ function OrderList(props: OrderListProps) { } return 0; }; - const [tapIndex, setTapIndex] = useState(() => { + const [tapIndex, setTapIndex] = useState(() => { const initialIndex = getInitialTabIndex(); console.log('初始化tapIndex:', initialIndex, '对应statusFilter:', props.searchParams?.statusFilter); return initialIndex; }) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [cancelDialogVisible, setCancelDialogVisible] = useState(false) + const [orderToCancel, setOrderToCancel] = useState(null) + const [confirmReceiveDialogVisible, setConfirmReceiveDialogVisible] = useState(false) + const [orderToConfirmReceive, setOrderToConfirmReceive] = useState(null) // 获取订单状态文本 const getOrderStatusText = (order: ShopOrder) => { @@ -174,7 +179,7 @@ function OrderList(props: OrderListProps) { return params; }; - const reload = async (resetPage = false, targetPage?: number) => { + const reload = useCallback(async (resetPage = false, targetPage?: number) => { setLoading(true); setError(null); // 清除之前的错误 const currentPage = resetPage ? 1 : (targetPage || page); @@ -228,18 +233,20 @@ function OrderList(props: OrderListProps) { ordersWithGoods.push(...batchResults); } - // 合并数据 - newList = resetPage ? ordersWithGoods : list?.concat(ordersWithGoods); + // 使用函数式更新避免依赖 list + setList(prevList => { + const newList = resetPage ? ordersWithGoods : (prevList || []).concat(ordersWithGoods); + return newList; + }); // 正确判断是否还有更多数据 const hasMoreData = res.list.length >= 10; // 假设每页10条数据 setHasMore(hasMoreData); } else { - newList = resetPage ? [] : list; + setList(prevList => resetPage ? [] : prevList); setHasMore(false); } - setList(newList || []); setPage(currentPage); setLoading(false); } catch (error) { @@ -252,51 +259,77 @@ function OrderList(props: OrderListProps) { icon: 'none' }); } - }; + }, [tapIndex, page, props.searchParams]); // 移除 list 依赖 - const reloadMore = async () => { + const reloadMore = useCallback(async () => { if (loading || !hasMore) return; // 防止重复加载 const nextPage = page + 1; setPage(nextPage); await reload(false, nextPage); + }, [loading, hasMore, page, reload]); + + // 确认收货 - 显示确认对话框 + const confirmReceive = (order: ShopOrder) => { + setOrderToConfirmReceive(order); + setConfirmReceiveDialogVisible(true); }; - // 确认收货 - const confirmReceive = async (order: ShopOrder) => { + // 确认收货 - 执行收货操作 + const handleConfirmReceive = async () => { + if (!orderToConfirmReceive) return; + try { + setConfirmReceiveDialogVisible(false); + await updateShopOrder({ - ...order, + ...orderToConfirmReceive, deliveryStatus: 30, // 已收货 orderStatus: 1 // 已完成 }); + Taro.showToast({ title: '确认收货成功', + icon: 'success' }); + await reload(true); // 重新加载列表 props.onReload?.(); // 通知父组件刷新 + + // 清空状态 + setOrderToConfirmReceive(null); } catch (error) { + console.error('确认收货失败:', error); Taro.showToast({ title: '确认收货失败', + icon: 'none' }); + // 重新显示对话框 + setConfirmReceiveDialogVisible(true); } }; + // 取消确认收货对话框 + const handleCancelReceiveDialog = () => { + setConfirmReceiveDialogVisible(false); + setOrderToConfirmReceive(null); + }; + // 取消订单 - const cancelOrder = async (order: ShopOrder) => { - try { - // 显示确认对话框 - const result = await Taro.showModal({ - title: '确认取消', - content: '确定要取消这个订单吗?', - confirmText: '确认取消', - cancelText: '我再想想' - }); + const cancelOrder = (order: ShopOrder) => { + setOrderToCancel(order); + setCancelDialogVisible(true); + }; - if (!result.confirm) return; + // 确认取消订单 + const handleConfirmCancel = async () => { + if (!orderToCancel) return; + + try { + setCancelDialogVisible(false); // 更新订单状态为已取消,而不是删除订单 await updateShopOrder({ - ...order, + ...orderToCancel, orderStatus: 2 // 已取消 }); @@ -312,9 +345,17 @@ function OrderList(props: OrderListProps) { title: '取消订单失败', icon: 'error' }); + } finally { + setOrderToCancel(null); } }; + // 取消对话框的取消操作 + const handleCancelDialog = () => { + setCancelDialogVisible(false); + setOrderToCancel(null); + }; + // 立即支付 const payOrder = async (order: ShopOrder) => { try { @@ -389,7 +430,7 @@ function OrderList(props: OrderListProps) { timeStamp: result.timeStamp, nonceStr: result.nonceStr, package: result.package, - signType: result.signType as any, + signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256', paySign: result.paySign, }); @@ -435,7 +476,7 @@ function OrderList(props: OrderListProps) { useEffect(() => { void reload(true); // 首次加载或tab切换时重置页码 - }, [tapIndex]); // 监听tapIndex变化 + }, [tapIndex]); // 只监听tapIndex变化,避免reload依赖循环 // 监听外部statusFilter变化,同步更新tab索引 useEffect(() => { @@ -459,7 +500,7 @@ function OrderList(props: OrderListProps) { setTapIndex(targetTabIndex); // 不需要调用reload,因为tapIndex变化会触发reload } - }, [props.searchParams?.statusFilter]); // 监听statusFilter变化 + }, [props.searchParams?.statusFilter, tapIndex]); // 监听statusFilter变化 return ( @@ -477,7 +518,20 @@ function OrderList(props: OrderListProps) { }} value={tapIndex} onChange={(paneKey) => { - setTapIndex(paneKey) + console.log('Tab切换:', paneKey, '类型:', typeof paneKey); + const newTapIndex = Number(paneKey); + setTapIndex(newTapIndex); + + // 通知父组件更新 searchParams.statusFilter + const currentTab = tabs.find(tab => tab.index === newTapIndex); + if (currentTab && props.onSearchParamsChange) { + const newSearchParams = { + ...props.searchParams, + statusFilter: currentTab.statusFilter + }; + console.log('通知父组件更新searchParams:', newSearchParams); + props.onSearchParamsChange(newSearchParams); + } }} > { @@ -533,8 +587,8 @@ function OrderList(props: OrderListProps) { {/* 订单列表 */} {list.length > 0 && list ?.filter((item) => { - // 如果是待付款标签页(tapIndex === 0),过滤掉支付已过期的订单 - if (tapIndex === 0 && !item.payStatus && item.orderStatus !== 2 && item.createTime) { + // 如果是待付款标签页(tapIndex === 1),过滤掉支付已过期的订单 + if (tapIndex === 1 && !item.payStatus && item.orderStatus !== 2 && item.createTime) { return !isPaymentExpired(item.createTime); } return true; @@ -626,7 +680,7 @@ function OrderList(props: OrderListProps) { {item.deliveryStatus === 20 && ( )} {/* 已完成状态:显示申请退款 */} @@ -645,6 +699,30 @@ function OrderList(props: OrderListProps) { )} + + {/* 取消订单确认对话框 */} + + 确定要取消这个订单吗? + + + {/* 确认收货确认对话框 */} + + 确定已经收到商品了吗?确认收货后订单将完成。 + ) } diff --git a/src/user/order/order.tsx b/src/user/order/order.tsx index 6d71102..a9c985d 100644 --- a/src/user/order/order.tsx +++ b/src/user/order/order.tsx @@ -96,7 +96,7 @@ function Order() { 我的订单 {/* 搜索和筛选工具栏 */} - + reload(searchParams)} searchParams={searchParams} showSearch={showSearch} + onSearchParamsChange={(newParams) => { + console.log('父组件接收到searchParams变化:', newParams); + setSearchParams(newParams); + }} /> ); diff --git a/src/utils/invite.ts b/src/utils/invite.ts index d4cf820..f4c901d 100644 --- a/src/utils/invite.ts +++ b/src/utils/invite.ts @@ -1,5 +1,5 @@ import Taro from '@tarojs/taro' -import { createInviteRelation } from '@/api/invite' +import { bindRefereeRelation } from '@/api/invite' /** * 邀请参数接口 @@ -19,7 +19,22 @@ export function parseInviteParams(options: any): InviteParams | null { if (options.scene) { // 确保 scene 是字符串类型 const sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene) + console.log('解析scene参数:', sceneStr) + // 处理 uid_xxx 格式的邀请码 + if (sceneStr.startsWith('uid_')) { + const inviterId = sceneStr.replace('uid_', '') + if (inviterId && !isNaN(parseInt(inviterId))) { + console.log('检测到uid格式邀请码:', inviterId) + return { + inviter: inviterId, + source: 'qrcode', + t: Date.now().toString() + } + } + } + + // 处理传统的 key=value&key=value 格式 const params: InviteParams = {} const pairs = sceneStr.split('&') @@ -40,7 +55,10 @@ export function parseInviteParams(options: any): InviteParams | null { } }) - return params.inviter ? params : null + if (params.inviter) { + console.log('检测到传统格式邀请码:', params) + return params + } } // 从 query 参数中解析邀请信息(兼容旧版本) @@ -119,34 +137,61 @@ export function clearInviteParams() { */ export async function handleInviteRelation(userId: number): Promise { try { + console.log('开始处理邀请关系,当前用户ID:', userId) + const inviteParams = getStoredInviteParams() if (!inviteParams || !inviteParams.inviter) { + console.log('没有找到邀请参数,跳过邀请关系建立') return false } + console.log('找到邀请参数:', inviteParams) + const inviterId = parseInt(inviteParams.inviter) if (isNaN(inviterId) || inviterId === userId) { // 邀请人ID无效或自己邀请自己 + console.log('邀请人ID无效或自己邀请自己,清除邀请参数') clearInviteParams() return false } - // 建立邀请关系 - await createInviteRelation({ - inviterId: inviterId, - inviteeId: userId, - source: inviteParams.source || 'unknown', - scene: `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}`, - inviteTime: new Date().toISOString() + console.log(`准备建立邀请关系: ${inviterId} -> ${userId}`) + + // 使用新的绑定推荐关系接口 + await bindRefereeRelation({ + refereeId: inviterId, + userId: userId, + source: inviteParams.source || 'qrcode', + scene: inviteParams.source === 'qrcode' ? `uid_${inviterId}` : `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}` }) // 清除本地存储的邀请参数 clearInviteParams() console.log(`邀请关系建立成功: ${inviterId} -> ${userId}`) + + // 显示成功提示 + setTimeout(() => { + Taro.showToast({ + title: '邀请关系建立成功', + icon: 'success', + duration: 2000 + }) + }, 500) + return true } catch (error) { console.error('建立邀请关系失败:', error) + + // 显示错误提示 + setTimeout(() => { + Taro.showToast({ + title: '邀请关系建立失败', + icon: 'error', + duration: 2000 + }) + }, 500) + return false } } @@ -229,3 +274,111 @@ export function trackInviteSource(source: string, inviterId?: number) { console.error('统计邀请来源失败:', error) } } + +/** + * 检查并处理当前用户的邀请关系 + * 用于在用户登录后立即检查是否需要建立邀请关系 + */ +export async function checkAndHandleInviteRelation(): Promise { + try { + // 获取当前用户信息 + const userInfo = Taro.getStorageSync('userInfo') + if (!userInfo || !userInfo.userId) { + console.log('用户未登录,无法处理邀请关系') + return false + } + + return await handleInviteRelation(userInfo.userId) + } catch (error) { + console.error('检查邀请关系失败:', error) + return false + } +} + +/** + * 手动触发邀请关系建立 + * 用于在特定页面或时机手动建立邀请关系 + */ +export async function manualHandleInviteRelation(userId: number): Promise { + try { + console.log('手动触发邀请关系建立,用户ID:', userId) + + const inviteParams = getStoredInviteParams() + if (!inviteParams || !inviteParams.inviter) { + console.log('没有待处理的邀请参数') + return false + } + + const result = await handleInviteRelation(userId) + + if (result) { + // 显示成功提示 + Taro.showModal({ + title: '邀请成功', + content: '您已成功加入邀请人的团队!', + showCancel: false, + confirmText: '知道了' + }) + } + + return result + } catch (error) { + console.error('手动处理邀请关系失败:', error) + return false + } +} + +/** + * 直接绑定推荐关系 + * 用于直接调用绑定推荐关系接口 + */ +export async function bindReferee(refereeId: number, userId?: number, source: string = 'qrcode'): Promise { + try { + console.log('直接绑定推荐关系:', { refereeId, userId, source }) + + // 如果没有传入userId,尝试从本地存储获取 + let targetUserId = userId + if (!targetUserId) { + const userInfo = Taro.getStorageSync('userInfo') + if (userInfo && userInfo.userId) { + targetUserId = userInfo.userId + } else { + throw new Error('无法获取用户ID') + } + } + + // 防止自己推荐自己 + if (refereeId === targetUserId) { + throw new Error('不能推荐自己') + } + + await bindRefereeRelation({ + refereeId: refereeId, + userId: targetUserId, + source: source, + scene: source === 'qrcode' ? `uid_${refereeId}` : undefined + }) + + console.log(`推荐关系绑定成功: ${refereeId} -> ${targetUserId}`) + + // 显示成功提示 + Taro.showToast({ + title: '推荐关系绑定成功', + icon: 'success', + duration: 2000 + }) + + return true + } catch (error: any) { + console.error('绑定推荐关系失败:', error) + + // 显示错误提示 + Taro.showToast({ + title: error.message || '绑定推荐关系失败', + icon: 'error', + duration: 2000 + }) + + return false + } +}