feat(invite): 新增邀请功能及二维码扫码登录支持

- 添加邀请记录、统计、来源统计、小程序码等数据模型
- 实现小程序码生成、邀请关系绑定、邀请场景处理等接口
- 新增扫码登录相关接口,支持生成二维码、检查状态、确认登录等操作
- 实现二维码内容解析和设备信息获取工具函数
- 添加礼品卡核销相关接口和解密工具函数
- 集成环境配置管理,支持开发、生产、测试环境切换
- 在过期时间页面集成登录二维码和核销二维码处理逻辑
- 添加邀请参数解析工具函数,支持从小程序启动参数中提取邀请信息
This commit is contained in:
2025-09-25 01:02:35 +08:00
parent c7a4a726c7
commit 791e98a8ec
26 changed files with 4181 additions and 4 deletions

10
config/app.ts Normal file
View File

@@ -0,0 +1,10 @@
import { API_BASE_URL } from './env'
// 租户ID - 请根据实际情况修改
export const TenantId = '10519';
// 接口地址 - 请根据实际情况修改
export const BaseUrl = API_BASE_URL;
// 当前版本
export const Version = 'v3.0.8';
// 版权信息
export const Copyright = 'WebSoft Inc.';

42
config/env.ts Normal file
View File

@@ -0,0 +1,42 @@
// 环境变量配置
export const ENV_CONFIG = {
// 开发环境
development: {
API_BASE_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '开发环境',
DEBUG: 'true',
},
// 生产环境
production: {
API_BASE_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '时里院子市集',
DEBUG: 'false',
},
// 测试环境
test: {
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
APP_NAME: '测试环境',
DEBUG: 'true',
}
}
// 获取当前环境配置
export function getEnvConfig() {
const env = process.env.NODE_ENV || 'development'
if (env === 'production') {
return ENV_CONFIG.production
} else { // @ts-ignore
if (env === 'test') {
return ENV_CONFIG.test
} else {
return ENV_CONFIG.development
}
}
}
// 导出环境变量
export const {
API_BASE_URL,
APP_NAME,
DEBUG
} = getEnvConfig()

277
src/api/invite/index.ts Normal file
View File

@@ -0,0 +1,277 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
/**
* 小程序码生成参数
*/
export interface MiniProgramCodeParam {
// 小程序页面路径
page?: string;
// 场景值最大32个可见字符
scene: string;
// 二维码宽度,单位 px最小 280px最大 1280px
width?: number;
// 是否检查页面是否存在
checkPath?: boolean;
// 环境版本
envVersion?: 'release' | 'trial' | 'develop';
}
/**
* 邀请关系参数
*/
export interface InviteRelationParam {
// 邀请人ID
inviterId: number;
// 被邀请人ID
inviteeId: number;
// 邀请来源
source: string;
// 场景值
scene?: string;
// 邀请时间
inviteTime?: string;
}
/**
* 绑定推荐关系参数
*/
export interface BindRefereeParam {
// 推荐人ID
dealerId: number;
// 被推荐人ID (可选,如果不传则使用当前登录用户)
userId?: number;
// 推荐来源
source?: string;
// 场景值
scene?: string;
}
/**
* 邀请统计数据
*/
export interface InviteStats {
// 总邀请数
totalInvites: number;
// 成功注册数
successfulRegistrations: number;
// 转化率
conversionRate: number;
// 今日邀请数
todayInvites: number;
// 本月邀请数
monthlyInvites: number;
// 邀请来源统计
sourceStats: InviteSourceStat[];
}
/**
* 邀请来源统计
*/
export interface InviteSourceStat {
source: string;
count: number;
successCount: number;
conversionRate: number;
}
/**
* 邀请记录
*/
export interface InviteRecord {
id?: number;
inviterId?: number;
inviteeId?: number;
inviterName?: string;
inviteeName?: string;
source?: string;
scene?: string;
status?: 'pending' | 'registered' | 'activated';
inviteTime?: string;
registerTime?: string;
activateTime?: string;
}
/**
* 邀请记录查询参数
*/
export interface InviteRecordParam {
page?: number;
limit?: number;
inviterId?: number;
status?: string;
source?: string;
startTime?: string;
endTime?: string;
}
/**
* 生成小程序码
*/
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
try {
const url = 'https://server.websoft.top/api/wx-login/getOrderQRCodeUnlimited/' + data.scene;
// 由于接口直接返回图片buffer我们直接构建完整的URL
return `${url}`;
} catch (error: any) {
throw new Error(error.message || '生成小程序码失败');
}
}
/**
* 生成邀请小程序码
*/
export async function generateInviteCode(inviterId: number) {
const scene = `uid_${inviterId}`;
return generateMiniProgramCode({
page: 'pages/index/index',
scene: scene,
width: 180,
checkPath: true,
envVersion: 'trial'
});
}
/**
* 建立邀请关系 (旧接口,保留兼容性)
*/
export async function createInviteRelation(data: InviteRelationParam) {
const res = await request.post<ApiResult<unknown>>(
'/invite/create-relation',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 绑定推荐关系 (新接口)
*/
export async function bindRefereeRelation(data: BindRefereeParam) {
try {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-dealer-referee',
{
dealerId: data.dealerId,
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 || '绑定推荐关系失败');
}
}
/**
* 处理邀请场景值
*/
export async function processInviteScene(scene: string, userId: number) {
const res = await request.post<ApiResult<unknown>>(
'/invite/process-scene',
{ scene, userId }
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取邀请统计数据
*/
export async function getInviteStats(inviterId: number) {
const res = await request.get<ApiResult<InviteStats>>(
`/invite/stats/${inviterId}`
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 分页查询邀请记录
*/
export async function pageInviteRecords(params: InviteRecordParam) {
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
'/invite/records/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的邀请记录
*/
export async function getMyInviteRecords(params: InviteRecordParam) {
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
'/invite/my-records',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 验证邀请码有效性
*/
export async function validateInviteCode(scene: string) {
const res = await request.post<ApiResult<{ valid: boolean; inviterId?: number; source?: string }>>(
'/invite/validate-code',
{ scene }
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 更新邀请状态
*/
export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') {
const res = await request.put<ApiResult<unknown>>(
`/invite/update-status/${inviteId}`,
{ status }
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取邀请排行榜
*/
export async function getInviteRanking(params?: { limit?: number; period?: 'day' | 'week' | 'month' }) {
const res = await request.get<ApiResult<Array<{
inviterId: number;
inviterName: string;
inviteCount: number;
successCount: number;
conversionRate: number;
}>>>(
'/invite/ranking',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,279 @@
import type { PageParam } from '@/api/index';
/**
* 邀请记录表
*/
export interface InviteRecord {
// 主键ID
id?: number;
// 邀请人ID
inviterId?: number;
// 被邀请人ID
inviteeId?: number;
// 邀请人姓名
inviterName?: string;
// 被邀请人姓名
inviteeName?: string;
// 邀请来源 (qrcode, link, share等)
source?: string;
// 场景值
scene?: string;
// 邀请状态: pending-待注册, registered-已注册, activated-已激活
status?: 'pending' | 'registered' | 'activated';
// 邀请时间
inviteTime?: string;
// 注册时间
registerTime?: string;
// 激活时间
activateTime?: string;
// 备注
comments?: string;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 邀请统计表
*/
export interface InviteStats {
// 主键ID
id?: number;
// 邀请人ID
inviterId?: number;
// 统计日期
statDate?: string;
// 总邀请数
totalInvites?: number;
// 成功注册数
successfulRegistrations?: number;
// 激活用户数
activatedUsers?: number;
// 转化率
conversionRate?: number;
// 今日邀请数
todayInvites?: number;
// 本周邀请数
weeklyInvites?: number;
// 本月邀请数
monthlyInvites?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 邀请来源统计表
*/
export interface InviteSourceStats {
// 主键ID
id?: number;
// 邀请人ID
inviterId?: number;
// 来源类型
source?: string;
// 来源名称
sourceName?: string;
// 邀请数量
inviteCount?: number;
// 成功数量
successCount?: number;
// 转化率
conversionRate?: number;
// 统计日期
statDate?: string;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 小程序码记录表
*/
export interface MiniProgramCode {
// 主键ID
id?: number;
// 邀请人ID
inviterId?: number;
// 场景值
scene?: string;
// 小程序码URL
codeUrl?: string;
// 页面路径
pagePath?: string;
// 二维码宽度
width?: number;
// 环境版本
envVersion?: string;
// 过期时间
expireTime?: string;
// 使用次数
useCount?: number;
// 最后使用时间
lastUseTime?: string;
// 状态: active-有效, expired-过期, disabled-禁用
status?: 'active' | 'expired' | 'disabled';
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 邀请记录搜索条件
*/
export interface InviteRecordParam extends PageParam {
// 邀请人ID
inviterId?: number;
// 被邀请人ID
inviteeId?: number;
// 邀请状态
status?: string;
// 邀请来源
source?: string;
// 开始时间
startTime?: string;
// 结束时间
endTime?: string;
// 关键词搜索
keywords?: string;
}
/**
* 邀请统计搜索条件
*/
export interface InviteStatsParam extends PageParam {
// 邀请人ID
inviterId?: number;
// 统计开始日期
startDate?: string;
// 统计结束日期
endDate?: string;
}
/**
* 邀请来源统计搜索条件
*/
export interface InviteSourceStatsParam extends PageParam {
// 邀请人ID
inviterId?: number;
// 来源类型
source?: string;
// 统计开始日期
startDate?: string;
// 统计结束日期
endDate?: string;
}
/**
* 小程序码搜索条件
*/
export interface MiniProgramCodeParam extends PageParam {
// 邀请人ID
inviterId?: number;
// 状态
status?: string;
// 场景值
scene?: string;
}
/**
* 邀请排行榜数据
*/
export interface InviteRanking {
// 邀请人ID
inviterId?: number;
// 邀请人姓名
inviterName?: string;
// 邀请人头像
inviterAvatar?: string;
// 邀请数量
inviteCount?: number;
// 成功数量
successCount?: number;
// 转化率
conversionRate?: number;
// 排名
rank?: number;
// 奖励金额
rewardAmount?: number;
}
/**
* 邀请奖励配置
*/
export interface InviteRewardConfig {
// 主键ID
id?: number;
// 奖励类型: register-注册奖励, activate-激活奖励, order-订单奖励
rewardType?: string;
// 奖励名称
rewardName?: string;
// 奖励金额
rewardAmount?: number;
// 奖励积分
rewardPoints?: number;
// 奖励优惠券ID
couponId?: number;
// 是否启用
enabled?: boolean;
// 生效时间
effectTime?: string;
// 失效时间
expireTime?: string;
// 备注
comments?: string;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 邀请奖励记录
*/
export interface InviteRewardRecord {
// 主键ID
id?: number;
// 邀请记录ID
inviteRecordId?: number;
// 邀请人ID
inviterId?: number;
// 被邀请人ID
inviteeId?: number;
// 奖励类型
rewardType?: string;
// 奖励金额
rewardAmount?: number;
// 奖励积分
rewardPoints?: number;
// 优惠券ID
couponId?: number;
// 发放状态: pending-待发放, issued-已发放, failed-发放失败
status?: 'pending' | 'issued' | 'failed';
// 发放时间
issueTime?: string;
// 失败原因
failReason?: string;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}

View File

@@ -0,0 +1,246 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api/index';
import Taro from '@tarojs/taro';
import {SERVER_API_URL} from "@/utils/server";
import {getUserInfo} from "@/api/layout";
/**
* 扫码登录相关接口
*/
// 生成扫码token请求参数
export interface GenerateQRTokenParam {
// 客户端类型web, app, wechat
clientType?: string;
// 设备信息
deviceInfo?: string;
// 过期时间(分钟)
expireMinutes?: number;
}
// 生成扫码token响应
export interface GenerateQRTokenResult {
// 扫码token
token: string;
// 二维码内容通常是包含token的URL或JSON
qrContent: string;
// 过期时间戳
expireTime: number;
// 二维码图片URL可选
qrImageUrl?: string;
}
// 扫码状态枚举
export enum QRLoginStatus {
PENDING = 'pending', // 等待扫码
SCANNED = 'scanned', // 已扫码,等待确认
CONFIRMED = 'confirmed', // 已确认登录
EXPIRED = 'expired', // 已过期
CANCELLED = 'cancelled' // 已取消
}
// 检查扫码状态响应
export interface QRLoginStatusResult {
// 当前状态
status: QRLoginStatus;
// 状态描述
message?: string;
// 如果已确认登录返回JWT token
accessToken?: string;
// 用户信息
userInfo?: {
userId: number;
nickname?: string;
avatar?: string;
phone?: string;
};
// 剩余有效时间(秒)
remainingTime?: number;
}
// 确认登录请求参数
export interface ConfirmLoginParam {
// 扫码token
token: string;
// 用户ID
userId: number;
// 登录平台web, app, wechat
platform?: string;
// 微信用户信息当platform为wechat时
wechatInfo?: {
openid?: string;
unionid?: string;
nickname?: string;
avatar?: string;
gender?: string;
};
// 设备信息
deviceInfo?: string;
}
// 确认登录响应
export interface ConfirmLoginResult {
// 是否成功
success: boolean;
// 消息
message: string;
// 登录用户信息
userInfo?: {
userId: number;
nickname?: string;
avatar?: string;
phone?: string;
};
}
/**
* 生成扫码登录token
*/
export async function generateQRToken(data?: GenerateQRTokenParam) {
const res = await request.post<ApiResult<GenerateQRTokenResult>>(
SERVER_API_URL + '/qr-login/generate',
{
clientType: 'wechat',
expireMinutes: 5,
...data
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 检查扫码登录状态
*/
export async function checkQRLoginStatus(token: string) {
const res = await request.get<ApiResult<QRLoginStatusResult>>(
SERVER_API_URL + `/qr-login/status/${token}`
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 确认扫码登录(通用接口)
*/
export async function confirmQRLogin(data: ConfirmLoginParam) {
const res = await request.post<ApiResult<ConfirmLoginResult>>(
SERVER_API_URL + '/qr-login/confirm',
data
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 微信小程序扫码登录确认(便捷接口)
*/
export async function confirmWechatQRLogin(token: string, userId: number) {
try {
// 获取微信用户信息
const userInfo = await getUserInfo();
const data: ConfirmLoginParam = {
token,
userId,
platform: 'wechat',
wechatInfo: {
nickname: userInfo?.nickname,
avatar: userInfo?.avatar,
gender: userInfo?.sex
},
deviceInfo: await getDeviceInfo()
};
const res = await request.post<ApiResult<any>>(
SERVER_API_URL + '/qr-login/confirm',
data
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
} catch (error: any) {
return Promise.reject(new Error(error.message || '确认登录失败'));
}
}
/**
* 获取设备信息
*/
async function getDeviceInfo() {
return new Promise<string>((resolve) => {
Taro.getSystemInfo({
success: (res) => {
const deviceInfo = {
platform: res.platform,
system: res.system,
version: res.version,
model: res.model,
brand: res.brand,
screenWidth: res.screenWidth,
screenHeight: res.screenHeight
};
resolve(JSON.stringify(deviceInfo));
},
fail: () => {
resolve('unknown');
}
});
});
}
/**
* 解析二维码内容提取token
*/
export function parseQRContent(qrContent: string): string | null {
try {
console.log('解析二维码内容1:', qrContent);
// 尝试解析JSON格式
if (qrContent.startsWith('{')) {
const parsed = JSON.parse(qrContent);
return parsed.token || parsed.qrCodeKey || null;
}
// 尝试解析URL格式
if (qrContent.includes('http')) {
const url = new URL(qrContent);
// 支持多种参数名
return url.searchParams.get('token') ||
url.searchParams.get('qrCodeKey') ||
url.searchParams.get('qr_code_key') ||
null;
}
// 尝试解析简单的key=value格式
if (qrContent.includes('=')) {
const params = new URLSearchParams(qrContent);
return params.get('token') ||
params.get('qrCodeKey') ||
params.get('qr_code_key') ||
null;
}
// 如果是以qr-login:开头的格式
if (qrContent.startsWith('qr-login:')) {
return qrContent.replace('qr-login:', '');
}
// 直接返回内容作为token如果是32位以上的字符串
if (qrContent.length >= 32) {
return qrContent;
}
return null;
} catch (error) {
console.error('解析二维码内容失败:', error);
return null;
}
}

View File

@@ -0,0 +1,260 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import {ShopGift, ShopGiftParam, GiftRedeemParam, GiftUseParam, QRCodeParam} from './model';
/**
* 分页查询礼品卡
*/
export async function pageShopGift(params: ShopGiftParam) {
const res = await request.get<ApiResult<PageResult<ShopGift>>>(
'/shop/shop-gift/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询礼品卡列表
*/
export async function listShopGift(params?: ShopGiftParam) {
const res = await request.get<ApiResult<ShopGift[]>>(
'/shop/shop-gift',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加礼品卡
*/
export async function addShopGift(data: ShopGift) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-gift',
data
);
if (res.code === 0) {
return res.message;
}
}
/**
* 生成礼品卡
*/
export async function makeShopGift(data: ShopGift) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-gift/make',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改礼品卡
*/
export async function updateShopGift(data: ShopGift) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-gift',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除礼品卡
*/
export async function removeShopGift(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-gift/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除礼品卡
*/
export async function removeBatchShopGift(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-gift/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询礼品卡
*/
export async function getShopGift(id: number) {
const res = await request.get<ApiResult<ShopGift>>(
'/shop/shop-gift/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据code查询礼品卡
* @param code
*/
export async function getShopGiftByCode(code: string) {
const res = await request.get<ApiResult<ShopGift>>(
'/shop/shop-gift/by-code/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 兑换礼品卡
*/
export async function redeemGift(params: GiftRedeemParam) {
const res = await request.post<ApiResult<ShopGift>>(
'/shop/shop-gift/redeem',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 使用礼品卡
*/
export async function useGift(params: GiftUseParam) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-gift/use',
params
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取用户的礼品卡列表
*/
export async function getUserGifts(params: ShopGiftParam) {
const res = await request.get<ApiResult<PageResult<ShopGift>>>(
'/shop/shop-gift/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 验证礼品卡兑换码
*/
export async function validateGiftCode(code: string) {
const res = await request.get<ApiResult<ShopGift>>(
`/shop/shop-gift/validate/${code}`
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function exportShopGift(ids?: number[]) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-gift/export',
ids
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 生成礼品卡核销码(可用)
*/
export async function generateVerificationCode(data: QRCodeParam) {
const res = await request.post<ApiResult<unknown>>(
'/qr-code/create-encrypted-qr-code',
data
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 验证核销码
*/
export async function verifyGiftCard(params: { verificationCode?: string; giftCode?: string }) {
const res = await request.post<ApiResult<ShopGift>>(
'/shop/shop-gift/verify',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 完成礼品卡核销
*/
export async function completeVerification(params: {
giftId: number;
verificationCode: string;
storeId?: number;
storeName?: string;
operatorId?: number;
operatorName?: string;
}) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-gift/complete-verification',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 解密二维码数据
*/
export async function decryptQrData(params: { token: string; encryptedData: string }) {
const res = await request.post<ApiResult<string>>(
'/qr-code/decrypt-qr-data',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,127 @@
import type { PageParam } from '@/api';
/**
* 礼品卡
*/
export interface ShopGift {
// 礼品卡ID
id?: number;
// 礼品卡名称
name?: string;
// 礼品卡描述
description?: string;
// 礼品卡兑换码
code?: string;
// 关联商品ID
goodsId?: number;
// 商品名称
goodsName?: string;
// 商品图片
goodsImage?: string;
// 礼品卡面值
faceValue?: string;
// 礼品卡类型 (10实物礼品卡 20虚拟礼品卡 30服务礼品卡)
type?: number;
// 领取时间
takeTime?: string;
// 过期时间
expireTime?: string;
// 有效期天数
validDays?: number;
// 操作人
operatorUserId?: number;
// 操作人名称
operatorUserName?: string;
// 是否展示
isShow?: string;
// 状态 (0未使用 1已使用 2已过期 3已失效)
status?: number;
// 备注
comments?: string;
// 使用说明
instructions?: string;
// 排序号
sortNumber?: number;
// 拥有者用户ID
userId?: number;
// 发放者用户ID
issuerUserId?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 数量
num?: number;
// 已发放数量
issuedCount?: number;
// 总发放数量
totalCount?: number;
// 使用门店/地址
useLocation?: string;
// 客服联系方式
contactInfo?: string;
// 核销时间
verificationTime?: string;
}
/**
* 礼品卡搜索条件
*/
export interface ShopGiftParam extends PageParam {
id?: number;
keywords?: string;
code?: string;
// 礼品卡类型筛选
type?: number;
// 状态筛选 (0未使用 1已使用 2失效)
status?: number;
// 用户ID筛选
userId?: number;
// 商品ID筛选
goodsId?: number;
// 是否过期筛选
isExpired?: boolean;
// 排序字段
sortBy?: 'createTime' | 'expireTime' | 'faceValue' | 'takeTime';
// 排序方向
sortOrder?: 'asc' | 'desc';
}
/**
* 礼品卡兑换参数
*/
export interface GiftRedeemParam {
// 兑换码
code: string;
// 用户ID
userId?: number;
}
/**
* 礼品卡使用参数
*/
export interface GiftUseParam {
// 礼品卡ID
giftId?: number;
// 使用地址/门店
useLocation?: string;
// 使用备注
useNote?: string;
}
export interface QRCodeParam {
// 二维码数据
data?: string;
// 二维码尺寸
width?: number;
// 二维码高度
height?: number;
// 二维码过期时间
expireMinutes?: number;
// 业务类型
businessType?: string;
}

View File

@@ -0,0 +1,126 @@
import React from 'react';
import { Button } from '@nutui/nutui-react-taro';
import { View } from '@tarojs/components';
import { Scan } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro';
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
export interface UnifiedQRButtonProps {
/** 按钮类型 */
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
/** 按钮大小 */
size?: 'large' | 'normal' | 'small';
/** 按钮文本 */
text?: string;
/** 是否显示图标 */
showIcon?: boolean;
/** 自定义样式类名 */
className?: string;
/** 扫码成功回调 */
onSuccess?: (result: UnifiedScanResult) => void;
/** 扫码失败回调 */
onError?: (error: string) => void;
/** 是否使用页面模式(跳转到专门页面) */
usePageMode?: boolean;
}
/**
* 统一扫码按钮组件
* 支持登录和核销两种类型的二维码扫描
*/
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
type = 'default',
size = 'small',
text = '扫码',
showIcon = true,
onSuccess,
onError,
usePageMode = false
}) => {
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
console.log(result,'useUnifiedQRScan>>result')
// 处理点击事件
const handleClick = async () => {
if (usePageMode) {
// 跳转到专门的统一扫码页面
if (canScan()) {
Taro.navigateTo({
url: '/passport/unified-qr/index'
});
} else {
Taro.showToast({
title: '请先登录小程序',
icon: 'error'
});
}
return;
}
// 直接执行扫码
try {
const scanResult = await startScan();
if (scanResult) {
onSuccess?.(scanResult);
// 根据扫码类型给出不同的后续提示
if (scanResult.type === ScanType.VERIFICATION) {
// 核销成功后可以继续扫码
setTimeout(() => {
Taro.showModal({
title: '核销成功',
content: '是否继续扫码核销其他礼品卡?',
success: (res) => {
if (res.confirm) {
handleClick(); // 递归调用继续扫码
}
}
});
}, 2000);
}
}
} catch (error: any) {
onError?.(error.message || '扫码失败');
}
};
const disabled = !canScan() || isLoading;
// 根据当前状态动态显示文本
const getButtonText = () => {
if (isLoading) {
switch (state) {
case 'scanning':
return '扫码中...';
case 'processing':
return '处理中...';
default:
return '扫码中...';
}
}
if (disabled && !canScan()) {
return '请先登录';
}
return text;
};
return (
<Button
type={type}
size={size}
loading={isLoading}
disabled={disabled}
onClick={handleClick}
>
<View className="flex items-center justify-center">
{showIcon && !isLoading && (
<Scan className="mr-1" />
)}
{getButtonText()}
</View>
</Button>
);
};
export default UnifiedQRButton;

View File

@@ -126,6 +126,13 @@ const Query = () => {
});
return false
}
if (!FormData.vinCode) {
Taro.showToast({
title: '请填入车架号',
icon: 'error'
});
return false
}
// 安装车辆
updateHjmCar({
@@ -760,6 +767,9 @@ const Query = () => {
<Cell className={'car-info-item-content'}>
GPS编号{FormData?.gpsNo}
</Cell>
<Cell className={'car-info-item-content'}>
{FormData?.vinCode}
</Cell>
<Cell className={'car-info-item-content'}>
{FormData.fenceName}
</Cell>

66
src/hooks/useAdminMode.ts Normal file
View File

@@ -0,0 +1,66 @@
import { useState, useCallback, useEffect } from 'react';
import Taro from '@tarojs/taro';
/**
* 管理员模式Hook
* 用于管理管理员用户的模式切换(普通用户模式 vs 管理员模式)
*/
export function useAdminMode() {
const [isAdminMode, setIsAdminMode] = useState<boolean>(false);
// 从本地存储加载管理员模式状态
useEffect(() => {
try {
const savedMode = Taro.getStorageSync('admin_mode');
if (savedMode !== undefined) {
setIsAdminMode(savedMode === 'true' || savedMode === true);
}
} catch (error) {
console.warn('Failed to load admin mode from storage:', error);
}
}, []);
// 切换管理员模式
const toggleAdminMode = useCallback(() => {
const newMode = !isAdminMode;
setIsAdminMode(newMode);
try {
// 保存到本地存储
Taro.setStorageSync('admin_mode', newMode);
// 显示切换提示
Taro.showToast({
title: newMode ? '已切换到管理员模式' : '已切换到普通用户模式',
icon: 'success',
duration: 1500
});
} catch (error) {
console.error('Failed to save admin mode to storage:', error);
}
}, [isAdminMode]);
// 设置管理员模式
const setAdminMode = useCallback((mode: boolean) => {
if (mode !== isAdminMode) {
setIsAdminMode(mode);
try {
Taro.setStorageSync('admin_mode', mode);
} catch (error) {
console.error('Failed to save admin mode to storage:', error);
}
}
}, [isAdminMode]);
// 重置为普通用户模式
const resetToUserMode = useCallback(() => {
setAdminMode(false);
}, [setAdminMode]);
return {
isAdminMode,
toggleAdminMode,
setAdminMode,
resetToUserMode
};
}

161
src/hooks/useCart.ts Normal file
View File

@@ -0,0 +1,161 @@
import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
// 购物车商品接口
export interface CartItem {
goodsId: number;
name: string;
price: string;
image: string;
quantity: number;
addTime: number;
skuId?: number;
specInfo?: string;
}
// 购物车Hook
export const useCart = () => {
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const [cartCount, setCartCount] = useState(0);
// 从本地存储加载购物车数据
const loadCartFromStorage = () => {
try {
const cartData = Taro.getStorageSync('cart_items');
if (cartData) {
const items = JSON.parse(cartData) as CartItem[];
setCartItems(items);
updateCartCount(items);
}
} catch (error) {
console.error('加载购物车数据失败:', error);
}
};
// 保存购物车数据到本地存储
const saveCartToStorage = (items: CartItem[]) => {
try {
Taro.setStorageSync('cart_items', JSON.stringify(items));
} catch (error) {
console.error('保存购物车数据失败:', error);
}
};
// 更新购物车数量
const updateCartCount = (items: CartItem[]) => {
const count = items.reduce((total, item) => total + item.quantity, 0);
setCartCount(count);
};
// 添加商品到购物车
const addToCart = (goods: {
goodsId: number;
name: string;
price: string;
image: string;
skuId?: number;
specInfo?: string;
}, quantity: number = 1) => {
const newItems = [...cartItems];
// 如果有SKU需要根据goodsId和skuId来判断是否为同一商品
const existingItemIndex = newItems.findIndex(item =>
item.goodsId === goods.goodsId &&
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
);
if (existingItemIndex >= 0) {
// 如果商品已存在,增加数量
newItems[existingItemIndex].quantity += quantity;
} else {
// 如果商品不存在,添加新商品
const newItem: CartItem = {
goodsId: goods.goodsId,
name: goods.name,
price: goods.price,
image: goods.image,
quantity,
addTime: Date.now(),
skuId: goods.skuId,
specInfo: goods.specInfo
};
newItems.push(newItem);
}
setCartItems(newItems);
updateCartCount(newItems);
saveCartToStorage(newItems);
// 显示成功提示
Taro.showToast({
title: '加入购物车成功',
icon: 'success',
duration: 1500
});
};
// 从购物车移除商品
const removeFromCart = (goodsId: number) => {
const newItems = cartItems.filter(item => item.goodsId !== goodsId);
setCartItems(newItems);
updateCartCount(newItems);
saveCartToStorage(newItems);
};
// 更新商品数量
const updateQuantity = (goodsId: number, quantity: number) => {
if (quantity <= 0) {
removeFromCart(goodsId);
return;
}
const newItems = cartItems.map(item =>
item.goodsId === goodsId ? { ...item, quantity } : item
);
setCartItems(newItems);
updateCartCount(newItems);
saveCartToStorage(newItems);
};
// 清空购物车
const clearCart = () => {
setCartItems([]);
setCartCount(0);
Taro.removeStorageSync('cart_items');
};
// 获取购物车总价
const getTotalPrice = () => {
return cartItems.reduce((total, item) => {
return total + (parseFloat(item.price) * item.quantity);
}, 0).toFixed(2);
};
// 检查商品是否在购物车中
const isInCart = (goodsId: number) => {
return cartItems.some(item => item.goodsId === goodsId);
};
// 获取商品在购物车中的数量
const getItemQuantity = (goodsId: number) => {
const item = cartItems.find(item => item.goodsId === goodsId);
return item ? item.quantity : 0;
};
// 初始化时加载购物车数据
useEffect(() => {
loadCartFromStorage();
}, []);
return {
cartItems,
cartCount,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
getTotalPrice,
isInCart,
getItemQuantity,
loadCartFromStorage
};
};

View File

@@ -0,0 +1,81 @@
import {useState, useEffect, useCallback} from 'react'
import Taro from '@tarojs/taro'
import {getShopDealerApply} from '@/api/shop/shopDealerApply'
import type {ShopDealerApply} from '@/api/shop/shopDealerApply/model'
// Hook 返回值接口
export interface UseDealerApplyReturn {
// 经销商用户信息
dealerApply: ShopDealerApply | null
// 加载状态
loading: boolean
// 错误信息
error: string | null
// 刷新数据
refresh: () => Promise<void>
}
/**
* 经销商用户 Hook - 简化版本
* 只查询经销商用户信息和判断是否存在
*/
export const useDealerApply = (): UseDealerApplyReturn => {
const [dealerApply, setDealerApply] = useState<ShopDealerApply | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const userId = Taro.getStorageSync('UserId');
// 获取经销商用户数据
const fetchDealerData = useCallback(async () => {
if (!userId) {
console.log('🔍 用户未登录,提前返回')
setDealerApply(null)
return
}
try {
setLoading(true)
setError(null)
// 查询当前用户的经销商信息
const dealer = await getShopDealerApply(userId)
if (dealer) {
setDealerApply(dealer)
} else {
setDealerApply(null)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
setError(errorMessage)
setDealerApply(null)
} finally {
setLoading(false)
}
}, [userId])
// 刷新数据
const refresh = useCallback(async () => {
await fetchDealerData()
}, [fetchDealerData])
// 初始化加载数据
useEffect(() => {
if (userId) {
console.log('🔍 调用 fetchDealerData')
fetchDealerData()
} else {
console.log('🔍 用户ID不存在不调用 fetchDealerData')
}
}, [fetchDealerData, userId])
return {
dealerApply,
loading,
error,
refresh
}
}

View File

@@ -0,0 +1,81 @@
import {useState, useEffect, useCallback} from 'react'
import Taro from '@tarojs/taro'
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
// Hook 返回值接口
export interface UseDealerUserReturn {
// 经销商用户信息
dealerUser: ShopDealerUser | null
// 加载状态
loading: boolean
// 错误信息
error: string | null
// 刷新数据
refresh: () => Promise<void>
}
/**
* 经销商用户 Hook - 简化版本
* 只查询经销商用户信息和判断是否存在
*/
export const useDealerUser = (): UseDealerUserReturn => {
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const userId = Taro.getStorageSync('UserId');
// 获取经销商用户数据
const fetchDealerData = useCallback(async () => {
if (!userId) {
console.log('🔍 用户未登录,提前返回')
setDealerUser(null)
return
}
try {
setLoading(true)
setError(null)
// 查询当前用户的经销商信息
const dealer = await getShopDealerUser(userId)
if (dealer) {
setDealerUser(dealer)
} else {
setDealerUser(null)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
setError(errorMessage)
setDealerUser(null)
} finally {
setLoading(false)
}
}, [userId])
// 刷新数据
const refresh = useCallback(async () => {
await fetchDealerData()
}, [fetchDealerData])
// 初始化加载数据
useEffect(() => {
if (userId) {
console.log('🔍 调用 fetchDealerData')
fetchDealerData()
} else {
console.log('🔍 用户ID不存在不调用 fetchDealerData')
}
}, [fetchDealerData, userId])
return {
dealerUser,
loading,
error,
refresh
}
}

120
src/hooks/useOrderStats.ts Normal file
View File

@@ -0,0 +1,120 @@
import { useState, useEffect, useCallback } from 'react';
import { UserOrderStats } from '@/api/user';
import Taro from '@tarojs/taro';
import {pageShopOrder} from "@/api/shop/shopOrder";
/**
* 订单统计Hook
* 用于管理用户订单各状态的数量统计
*/
export const useOrderStats = () => {
const [orderStats, setOrderStats] = useState<UserOrderStats>({
pending: 0, // 待付款
paid: 0, // 待发货
shipped: 0, // 待收货
completed: 0, // 已完成
refund: 0, // 退货/售后
total: 0 // 总订单数
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 获取订单统计数据
*/
const fetchOrderStats = useCallback(async (showToast = false) => {
try {
setLoading(true);
setError(null);
if(!Taro.getStorageSync('UserId')){
return false;
}
// TODO 读取订单数量
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
setOrderStats({
pending: pending?.count || 0,
paid: paid?.count || 0,
shipped: shipped?.count || 0,
completed: completed?.count || 0,
refund: refund?.count || 0,
total: total?.count || 0
})
if (showToast) {
Taro.showToast({
title: '数据已更新',
icon: 'success',
duration: 1500
});
}
} catch (err: any) {
const errorMessage = err.message || '获取订单统计失败';
setError(errorMessage);
console.error('获取订单统计失败:', err);
if (showToast) {
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 2000
});
}
} finally {
setLoading(false);
}
}, []);
/**
* 刷新订单统计数据
*/
const refreshOrderStats = useCallback(() => {
return fetchOrderStats(true);
}, [fetchOrderStats]);
/**
* 获取指定状态的订单数量
*/
const getOrderCount = useCallback((status: keyof UserOrderStats) => {
return orderStats[status] || 0;
}, [orderStats]);
/**
* 检查是否有待处理的订单
*/
const hasPendingOrders = useCallback(() => {
return orderStats.pending > 0 || orderStats.paid > 0 || orderStats.shipped > 0;
}, [orderStats]);
/**
* 获取总的待处理订单数量
*/
const getTotalPendingCount = useCallback(() => {
return orderStats.pending + orderStats.paid + orderStats.shipped;
}, [orderStats]);
// 组件挂载时自动获取数据
useEffect(() => {
fetchOrderStats();
}, [fetchOrderStats]);
return {
orderStats,
loading,
error,
fetchOrderStats,
refreshOrderStats,
getOrderCount,
hasPendingOrders,
getTotalPendingCount
};
};
export default useOrderStats;

View File

@@ -0,0 +1,163 @@
import { useState, useEffect, useMemo } from 'react';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
// 扩展dayjs支持duration
dayjs.extend(duration);
export interface CountdownTime {
hours: number;
minutes: number;
seconds: number;
isExpired: boolean;
totalMinutes: number; // 总剩余分钟数
}
/**
* 支付倒计时Hook
* @param createTime 订单创建时间
* @param payStatus 支付状态
* @param realTime 是否实时更新详情页用true列表页用false
* @param timeoutHours 超时小时数默认24小时
*/
export const usePaymentCountdown = (
createTime?: string,
payStatus?: boolean,
realTime: boolean = false,
timeoutHours: number = 24
): CountdownTime => {
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
hours: 0,
minutes: 0,
seconds: 0,
isExpired: false,
totalMinutes: 0
});
// 计算剩余时间的函数
const calculateTimeLeft = useMemo(() => {
return (): CountdownTime => {
if (!createTime || payStatus) {
return {
hours: 0,
minutes: 0,
seconds: 0,
isExpired: false,
totalMinutes: 0
};
}
const createTimeObj = dayjs(createTime);
const expireTime = createTimeObj.add(timeoutHours, 'hour');
const now = dayjs();
const diff = expireTime.diff(now);
if (diff <= 0) {
return {
hours: 0,
minutes: 0,
seconds: 0,
isExpired: true,
totalMinutes: 0
};
}
const durationObj = dayjs.duration(diff);
const hours = Math.floor(durationObj.asHours());
const minutes = durationObj.minutes();
const seconds = durationObj.seconds();
const totalMinutes = Math.floor(durationObj.asMinutes());
return {
hours,
minutes,
seconds,
isExpired: false,
totalMinutes
};
};
}, [createTime, payStatus, timeoutHours]);
useEffect(() => {
if (!createTime || payStatus) {
setTimeLeft({
hours: 0,
minutes: 0,
seconds: 0,
isExpired: false,
totalMinutes: 0
});
return;
}
// 立即计算一次
const initialTime = calculateTimeLeft();
setTimeLeft(initialTime);
// 如果不需要实时更新,直接返回
if (!realTime) {
return;
}
// 如果需要实时更新,设置定时器
const timer = setInterval(() => {
const newTimeLeft = calculateTimeLeft();
setTimeLeft(newTimeLeft);
// 如果已过期,清除定时器
if (newTimeLeft.isExpired) {
clearInterval(timer);
}
}, 1000);
return () => clearInterval(timer);
}, [createTime, payStatus, realTime, calculateTimeLeft]);
return timeLeft;
};
/**
* 格式化倒计时文本
* @param timeLeft 倒计时时间对象
* @param showSeconds 是否显示秒数
*/
export const formatCountdownText = (
timeLeft: CountdownTime,
showSeconds: boolean = false
): string => {
if (timeLeft.isExpired) {
return '已过期';
}
if (timeLeft.hours > 0) {
if (showSeconds) {
return `${timeLeft.hours}小时${timeLeft.minutes}${timeLeft.seconds}`;
} else {
return `${timeLeft.hours}小时${timeLeft.minutes}分钟`;
}
} else if (timeLeft.minutes > 0) {
if (showSeconds) {
return `${timeLeft.minutes}${timeLeft.seconds}`;
} else {
return `${timeLeft.minutes}分钟`;
}
} else {
return `${timeLeft.seconds}`;
}
};
/**
* 判断是否为紧急状态剩余时间少于1小时
*/
export const isUrgentCountdown = (timeLeft: CountdownTime): boolean => {
return !timeLeft.isExpired && timeLeft.totalMinutes < 60;
};
/**
* 判断是否为非常紧急状态剩余时间少于10分钟
*/
export const isCriticalCountdown = (timeLeft: CountdownTime): boolean => {
return !timeLeft.isExpired && timeLeft.totalMinutes < 10;
};
export default usePaymentCountdown;

228
src/hooks/useQRLogin.ts Normal file
View File

@@ -0,0 +1,228 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import Taro from '@tarojs/taro';
import {
confirmWechatQRLogin,
parseQRContent,
type ConfirmLoginResult
} from '@/api/passport/qr-login';
/**
* 扫码登录状态
*/
export enum ScanLoginState {
IDLE = 'idle', // 空闲状态
SCANNING = 'scanning', // 正在扫码
CONFIRMING = 'confirming', // 正在确认登录
SUCCESS = 'success', // 登录成功
ERROR = 'error' // 登录失败
}
/**
* 扫码登录Hook
*/
export function useQRLogin() {
const [state, setState] = useState<ScanLoginState>(ScanLoginState.IDLE);
const [error, setError] = useState<string>('');
const [result, setResult] = useState<ConfirmLoginResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
// 用于取消操作的引用
const cancelRef = useRef<boolean>(false);
/**
* 重置状态
*/
const reset = useCallback(() => {
setState(ScanLoginState.IDLE);
setError('');
setResult(null);
setIsLoading(false);
cancelRef.current = false;
}, []);
/**
* 开始扫码登录
*/
const startScan = useCallback(async () => {
try {
reset();
setState(ScanLoginState.SCANNING);
// 检查用户是否已登录
const userId = Taro.getStorageSync('UserId');
if (!userId) {
throw new Error('请先登录小程序');
}
// 调用扫码API
const scanResult = await new Promise<string>((resolve, reject) => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
if (res.result) {
resolve(res.result);
} else {
reject(new Error('扫码结果为空'));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '扫码失败'));
}
});
});
// 检查是否被取消
if (cancelRef.current) {
return;
}
// 解析二维码内容
const token = parseQRContent(scanResult);
console.log('解析二维码内容2:',token)
if (!token) {
throw new Error('无效的登录二维码');
}
// 确认登录
setState(ScanLoginState.CONFIRMING);
setIsLoading(true);
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
console.log(confirmResult,'confirmResult>>>>')
if (cancelRef.current) {
return;
}
if (confirmResult.success) {
setState(ScanLoginState.SUCCESS);
setResult(confirmResult);
// 显示成功提示
Taro.showToast({
title: '登录确认成功',
icon: 'success',
duration: 2000
});
} else {
throw new Error(confirmResult.message || '登录确认失败');
}
} catch (err: any) {
if (!cancelRef.current) {
setState(ScanLoginState.ERROR);
const errorMessage = err.message || '扫码登录失败';
setError(errorMessage);
// 显示错误提示
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 3000
});
}
} finally {
setIsLoading(false);
}
}, [reset]);
/**
* 取消扫码登录
*/
const cancel = useCallback(() => {
cancelRef.current = true;
reset();
}, [reset]);
/**
* 处理扫码结果(用于已有扫码结果的情况)
*/
const handleScanResult = useCallback(async (qrContent: string) => {
try {
reset();
setState(ScanLoginState.CONFIRMING);
setIsLoading(true);
// 检查用户是否已登录
const userId = Taro.getStorageSync('UserId');
if (!userId) {
throw new Error('请先登录小程序');
}
// 解析二维码内容
const token = parseQRContent(qrContent);
if (!token) {
throw new Error('无效的登录二维码');
}
// 确认登录
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
if (confirmResult.success) {
setState(ScanLoginState.SUCCESS);
setResult(confirmResult);
// 显示成功提示
Taro.showToast({
title: '登录确认成功',
icon: 'success',
duration: 2000
});
} else {
throw new Error(confirmResult.message || '登录确认失败');
}
} catch (err: any) {
setState(ScanLoginState.ERROR);
const errorMessage = err.message || '登录确认失败';
setError(errorMessage);
// 显示错误提示
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 3000
});
} finally {
setIsLoading(false);
}
}, [reset]);
/**
* 检查是否可以进行扫码登录
*/
const canScan = useCallback(() => {
const userId = Taro.getStorageSync('UserId');
const accessToken = Taro.getStorageSync('access_token');
return !!(userId && accessToken);
}, []);
// 组件卸载时取消操作
useEffect(() => {
return () => {
cancelRef.current = true;
};
}, []);
return {
// 状态
state,
error,
result,
isLoading,
// 方法
startScan,
cancel,
reset,
handleScanResult,
canScan,
// 便捷状态判断
isIdle: state === ScanLoginState.IDLE,
isScanning: state === ScanLoginState.SCANNING,
isConfirming: state === ScanLoginState.CONFIRMING,
isSuccess: state === ScanLoginState.SUCCESS,
isError: state === ScanLoginState.ERROR
};
}

323
src/hooks/useShopInfo.ts Normal file
View File

@@ -0,0 +1,323 @@
import {useState, useEffect, useCallback} from 'react';
import Taro from '@tarojs/taro';
import {AppInfo} from '@/api/cms/cmsWebsite/model';
import {getShopInfo} from '@/api/layout';
// 本地存储键名
const SHOP_INFO_STORAGE_KEY = 'shop_info';
const SHOP_INFO_CACHE_TIME_KEY = 'shop_info_cache_time';
// 缓存有效期(毫秒)- 默认30分钟
const CACHE_DURATION = 30 * 60 * 1000;
/**
* 商店信息Hook
* 提供商店信息的获取、缓存和管理功能
*/
export const useShopInfo = () => {
const [shopInfo, setShopInfo] = useState<AppInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 从本地存储加载商店信息
const loadShopInfoFromStorage = useCallback(() => {
try {
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
const cacheTime = Taro.getStorageSync(SHOP_INFO_CACHE_TIME_KEY);
if (cachedData && cacheTime) {
const now = Date.now();
const timeDiff = now - cacheTime;
// 检查缓存是否过期
if (timeDiff < CACHE_DURATION) {
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
setShopInfo(shopData);
setLoading(false);
return true; // 返回true表示使用了缓存
} else {
// 缓存过期,清除旧数据
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
}
}
} catch (error) {
console.error('加载商店信息缓存失败:', error);
}
return false; // 返回false表示没有使用缓存
}, []);
// 保存商店信息到本地存储
const saveShopInfoToStorage = useCallback((data: AppInfo) => {
try {
Taro.setStorageSync(SHOP_INFO_STORAGE_KEY, data);
Taro.setStorageSync(SHOP_INFO_CACHE_TIME_KEY, Date.now());
} catch (error) {
console.error('保存商店信息缓存失败:', error);
}
}, []);
// 从服务器获取商店信息
const fetchShopInfo = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await getShopInfo();
setShopInfo(data);
// 保存到本地存储
saveShopInfoToStorage(data);
return data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('获取商店信息失败:', error);
setError(errorMessage);
// 如果网络请求失败,尝试使用缓存数据(即使过期)
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
if (cachedData) {
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
setShopInfo(shopData);
console.warn('网络请求失败,使用缓存数据');
}
return null;
} finally {
setLoading(false);
}
}, [saveShopInfoToStorage]);
// 刷新商店信息
const refreshShopInfo = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await getShopInfo();
setShopInfo(data);
// 保存到本地存储
saveShopInfoToStorage(data);
return data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('刷新商店信息失败:', error);
setError(errorMessage);
return null;
} finally {
setLoading(false);
}
}, [saveShopInfoToStorage]);
// 清除缓存
const clearCache = useCallback(() => {
try {
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
setShopInfo(null);
setError(null);
} catch (error) {
console.error('清除商店信息缓存失败:', error);
}
}, []);
// 获取应用名称
const getAppName = useCallback(() => {
return shopInfo?.appName || '商城';
}, [shopInfo]);
// 获取网站名称(兼容旧方法名)
const getWebsiteName = useCallback(() => {
return shopInfo?.appName || '商城';
}, [shopInfo]);
// 获取应用Logo
const getAppLogo = useCallback(() => {
return shopInfo?.logo || shopInfo?.icon || '';
}, [shopInfo]);
// 获取网站Logo兼容旧方法名
const getWebsiteLogo = useCallback(() => {
return shopInfo?.logo || shopInfo?.icon || '';
}, [shopInfo]);
// 获取应用图标
const getAppIcon = useCallback(() => {
return shopInfo?.icon || shopInfo?.logo || '';
}, [shopInfo]);
// 获取深色模式LogoAppInfo中无此字段使用普通Logo
const getDarkLogo = useCallback(() => {
return shopInfo?.logo || shopInfo?.icon || '';
}, [shopInfo]);
// 获取应用域名
const getDomain = useCallback(() => {
return shopInfo?.domain || '';
}, [shopInfo]);
// 获取应用描述
const getDescription = useCallback(() => {
return shopInfo?.description || '';
}, [shopInfo]);
// 获取应用关键词
const getKeywords = useCallback(() => {
return shopInfo?.keywords || '';
}, [shopInfo]);
// 获取应用标题
const getTitle = useCallback(() => {
return shopInfo?.title || shopInfo?.appName || '';
}, [shopInfo]);
// 获取小程序二维码
const getMpQrCode = useCallback(() => {
return shopInfo?.mpQrCode || '';
}, [shopInfo]);
// 获取联系电话AppInfo中无此字段从config中获取
const getPhone = useCallback(() => {
return (shopInfo?.config as any)?.phone || '';
}, [shopInfo]);
// 获取邮箱AppInfo中无此字段从config中获取
const getEmail = useCallback(() => {
return (shopInfo?.config as any)?.email || '';
}, [shopInfo]);
// 获取地址AppInfo中无此字段从config中获取
const getAddress = useCallback(() => {
return (shopInfo?.config as any)?.address || '';
}, [shopInfo]);
// 获取ICP备案号AppInfo中无此字段从config中获取
const getIcpNo = useCallback(() => {
return (shopInfo?.config as any)?.icpNo || '';
}, [shopInfo]);
// 获取应用状态
const getStatus = useCallback(() => {
return {
running: shopInfo?.running || 0,
statusText: shopInfo?.statusText || '',
statusIcon: shopInfo?.statusIcon || '',
expired: shopInfo?.expired || false,
expiredDays: shopInfo?.expiredDays || 0,
soon: shopInfo?.soon || 0
};
}, [shopInfo]);
// 获取应用配置
const getConfig = useCallback(() => {
return shopInfo?.config || {};
}, [shopInfo]);
// 获取应用设置
const getSetting = useCallback(() => {
return shopInfo?.setting || {};
}, [shopInfo]);
// 获取服务器时间
const getServerTime = useCallback(() => {
return shopInfo?.serverTime || {};
}, [shopInfo]);
// 获取导航菜单
const getNavigation = useCallback(() => {
return {
topNavs: shopInfo?.topNavs || [],
bottomNavs: shopInfo?.bottomNavs || []
};
}, [shopInfo]);
// 检查是否支持搜索从config中获取
const isSearchEnabled = useCallback(() => {
return (shopInfo?.config as any)?.search === true;
}, [shopInfo]);
// 获取应用版本信息
const getVersionInfo = useCallback(() => {
return {
version: shopInfo?.version || 10,
expirationTime: shopInfo?.expirationTime || '',
expired: shopInfo?.expired || false,
expiredDays: shopInfo?.expiredDays || 0,
soon: shopInfo?.soon || 0
};
}, [shopInfo]);
// 检查应用是否过期
const isExpired = useCallback(() => {
return shopInfo?.expired === true;
}, [shopInfo]);
// 获取过期天数
const getExpiredDays = useCallback(() => {
return shopInfo?.expiredDays || 0;
}, [shopInfo]);
// 检查是否即将过期
const isSoonExpired = useCallback(() => {
return (shopInfo?.soon || 0) > 0;
}, [shopInfo]);
// 初始化时加载商店信息
useEffect(() => {
const initShopInfo = async () => {
// 先尝试从缓存加载
const hasCache = loadShopInfoFromStorage();
// 如果没有缓存或需要刷新,则从服务器获取
if (!hasCache) {
await fetchShopInfo();
}
};
initShopInfo();
}, []); // 空依赖数组,只在组件挂载时执行一次
return {
// 状态
shopInfo,
loading,
error,
// 方法
fetchShopInfo,
refreshShopInfo,
clearCache,
// 新的工具方法基于AppInfo字段
getAppName,
getAppLogo,
getAppIcon,
getDescription,
getKeywords,
getTitle,
getMpQrCode,
getDomain,
getConfig,
getSetting,
getServerTime,
getNavigation,
getStatus,
getVersionInfo,
isExpired,
getExpiredDays,
isSoonExpired,
// 兼容旧方法名
getWebsiteName,
getWebsiteLogo,
getDarkLogo,
getPhone,
getEmail,
getAddress,
getIcpNo,
isSearchEnabled
};
};

95
src/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,95 @@
import { useState, useEffect } from 'react'
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
import Taro from '@tarojs/taro'
export interface UseThemeReturn {
currentTheme: GradientTheme
setTheme: (themeName: string) => void
isAutoTheme: boolean
refreshTheme: () => void
}
/**
* 主题管理Hook
* 提供主题切换和状态管理功能
*/
export const useTheme = (): UseThemeReturn => {
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
// 获取当前主题
const getCurrentTheme = (): GradientTheme => {
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
if (savedTheme === 'auto') {
// 自动主题根据用户ID生成
const userId = Taro.getStorageSync('userId') || '1'
return gradientUtils.getThemeByUserId(userId)
} else {
// 手动选择的主题
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
}
}
// 初始化主题
useEffect(() => {
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
setIsAutoTheme(savedTheme === 'auto')
setCurrentTheme(getCurrentTheme())
}, [])
// 设置主题
const setTheme = (themeName: string) => {
try {
Taro.setStorageSync('user_theme', themeName)
setIsAutoTheme(themeName === 'auto')
setCurrentTheme(getCurrentTheme())
} catch (error) {
console.error('保存主题失败:', error)
}
}
// 刷新主题(用于自动主题模式下用户信息变更时)
const refreshTheme = () => {
setCurrentTheme(getCurrentTheme())
}
return {
currentTheme,
setTheme,
isAutoTheme,
refreshTheme
}
}
/**
* 获取当前主题的样式对象
* 用于直接应用到组件样式中
*/
export const useThemeStyles = () => {
const { currentTheme } = useTheme()
return {
// 主要背景样式
primaryBackground: {
background: currentTheme.background,
color: currentTheme.textColor
},
// 按钮样式
primaryButton: {
background: currentTheme.background,
border: 'none',
color: currentTheme.textColor
},
// 强调色
accentColor: currentTheme.primary,
// 文字颜色
textColor: currentTheme.textColor,
// 完整主题对象
theme: currentTheme
}
}

View File

@@ -0,0 +1,331 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import Taro from '@tarojs/taro';
import {
confirmWechatQRLogin,
parseQRContent
} from '@/api/passport/qr-login';
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
import { useUser } from "@/hooks/useUser";
import { isValidJSON } from "@/utils/jsonUtils";
import dayjs from 'dayjs';
/**
* 统一扫码状态
*/
export enum UnifiedScanState {
IDLE = 'idle', // 空闲状态
SCANNING = 'scanning', // 正在扫码
PROCESSING = 'processing', // 正在处理
SUCCESS = 'success', // 处理成功
ERROR = 'error' // 处理失败
}
/**
* 扫码类型
*/
export enum ScanType {
LOGIN = 'login', // 登录二维码
VERIFICATION = 'verification', // 核销二维码
UNKNOWN = 'unknown' // 未知类型
}
/**
* 统一扫码结果
*/
export interface UnifiedScanResult {
type: ScanType;
data: any;
message: string;
}
/**
* 统一扫码Hook
* 可以处理登录和核销两种类型的二维码
*/
export function useUnifiedQRScan() {
const { isAdmin } = useUser();
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
const [error, setError] = useState<string>('');
const [result, setResult] = useState<UnifiedScanResult | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [scanType, setScanType] = useState<ScanType>(ScanType.UNKNOWN);
// 用于取消操作的引用
const cancelRef = useRef<boolean>(false);
/**
* 重置状态
*/
const reset = useCallback(() => {
setState(UnifiedScanState.IDLE);
setError('');
setResult(null);
setIsLoading(false);
setScanType(ScanType.UNKNOWN);
cancelRef.current = false;
}, []);
/**
* 检测二维码类型
*/
const detectScanType = useCallback((scanResult: string): ScanType => {
try {
// 1. 检查是否为JSON格式核销二维码
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
return ScanType.VERIFICATION;
}
}
// 2. 检查是否为登录二维码
const loginToken = parseQRContent(scanResult);
if (loginToken) {
return ScanType.LOGIN;
}
// 3. 检查是否为纯文本核销码6位数字
if (/^\d{6}$/.test(scanResult.trim())) {
return ScanType.VERIFICATION;
}
return ScanType.UNKNOWN;
} catch (error) {
console.error('检测二维码类型失败:', error);
return ScanType.UNKNOWN;
}
}, []);
/**
* 处理登录二维码
*/
const handleLoginQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
const userId = Taro.getStorageSync('UserId');
if (!userId) {
throw new Error('请先登录小程序');
}
const token = parseQRContent(scanResult);
if (!token) {
throw new Error('无效的登录二维码');
}
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
if (confirmResult.status === 'confirmed') {
return {
type: ScanType.LOGIN,
data: confirmResult,
message: '登录成功'
};
} else {
throw new Error(confirmResult.message || '登录确认失败');
}
}, []);
/**
* 处理核销二维码
*/
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
if (!isAdmin()) {
throw new Error('您没有核销权限');
}
let code = '';
// 判断是否为加密的JSON格式
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
// 解密获取核销码
const decryptedData = await decryptQrData({
token: json.token,
encryptedData: json.data
});
if (decryptedData) {
code = decryptedData.toString();
} else {
throw new Error('解密失败');
}
}
} else {
// 直接使用扫码结果作为核销码
code = scanResult.trim();
}
if (!code) {
throw new Error('无法获取有效的核销码');
}
// 验证核销码
const gift = await getShopGiftByCode(code);
if (!gift) {
throw new Error('核销码无效');
}
if (gift.status === 1) {
throw new Error('此礼品码已使用');
}
if (gift.status === 2) {
throw new Error('此礼品码已失效');
}
if (gift.userId === 0) {
throw new Error('此礼品码未认领');
}
// 执行核销
await updateShopGift({
...gift,
status: 1,
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
});
return {
type: ScanType.VERIFICATION,
data: gift,
message: '核销成功'
};
}, [isAdmin]);
/**
* 开始扫码
*/
const startScan = useCallback(async (): Promise<UnifiedScanResult | null> => {
try {
reset();
setState(UnifiedScanState.SCANNING);
// 调用扫码API
const scanResult = await new Promise<string>((resolve, reject) => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
if (res.result) {
resolve(res.result);
} else {
reject(new Error('扫码结果为空'));
}
},
fail: (err) => {
reject(new Error(err.errMsg || '扫码失败'));
}
});
});
// 检查是否被取消
if (cancelRef.current) {
return null;
}
// 检测二维码类型
const type = detectScanType(scanResult);
setScanType(type);
if (type === ScanType.UNKNOWN) {
throw new Error('不支持的二维码类型');
}
// 开始处理
setState(UnifiedScanState.PROCESSING);
setIsLoading(true);
let result: UnifiedScanResult;
switch (type) {
case ScanType.LOGIN:
result = await handleLoginQR(scanResult);
break;
case ScanType.VERIFICATION:
result = await handleVerificationQR(scanResult);
break;
default:
throw new Error('未知的扫码类型');
}
if (cancelRef.current) {
return null;
}
setState(UnifiedScanState.SUCCESS);
setResult(result);
// 显示成功提示
Taro.showToast({
title: result.message,
icon: 'success',
duration: 2000
});
return result;
} catch (err: any) {
if (!cancelRef.current) {
setState(UnifiedScanState.ERROR);
const errorMessage = err.message || '处理失败';
setError(errorMessage);
// 显示错误提示
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 3000
});
}
return null;
} finally {
setIsLoading(false);
}
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
/**
* 取消扫码
*/
const cancel = useCallback(() => {
cancelRef.current = true;
reset();
}, [reset]);
/**
* 检查是否可以进行扫码
*/
const canScan = useCallback(() => {
const userId = Taro.getStorageSync('UserId');
const accessToken = Taro.getStorageSync('access_token');
return !!(userId && accessToken);
}, []);
// 组件卸载时取消操作
useEffect(() => {
return () => {
cancelRef.current = true;
};
}, []);
return {
// 状态
state,
error,
result,
isLoading,
scanType,
// 方法
startScan,
cancel,
reset,
canScan,
// 便捷状态判断
isIdle: state === UnifiedScanState.IDLE,
isScanning: state === UnifiedScanState.SCANNING,
isProcessing: state === UnifiedScanState.PROCESSING,
isSuccess: state === UnifiedScanState.SUCCESS,
isError: state === UnifiedScanState.ERROR
};
}

334
src/hooks/useUser.ts Normal file
View File

@@ -0,0 +1,334 @@
import { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
import { User } from '@/api/system/user/model';
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
// 用户Hook
export const useUser = () => {
const [user, setUser] = useState<User | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
// 自动登录通过OpenID
const autoLoginByOpenId = async () => {
try {
const res = await new Promise<any>((resolve, reject) => {
Taro.login({
success: (loginRes) => {
loginByOpenId({
code: loginRes.code,
tenantId: 10519
}).then(async (data) => {
if (data) {
// 保存登录信息
saveUserToStorage(data.access_token, data.user);
setUser(data.user);
setIsLoggedIn(true);
// 处理邀请关系
if (data.user?.userId) {
try {
const inviteSuccess = await handleInviteRelation(data.user.userId);
if (inviteSuccess) {
console.log('自动登录时邀请关系建立成功');
}
} catch (error) {
console.error('自动登录时处理邀请关系失败:', error);
}
}
resolve(data.user);
} else {
reject(new Error('自动登录失败'));
}
}).catch(_ => {
// 首次注册,跳转到邀请注册页面
const pages = Taro.getCurrentPages();
const currentPage = pages[pages.length - 1];
const inviteParams = getStoredInviteParams()
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
return Taro.navigateTo({
url: '/dealer/apply/add'
});
}
});
},
fail: reject
});
});
return res;
} catch (error) {
console.error('自动登录失败:', error);
return null;
}
};
// 从本地存储加载用户数据
const loadUserFromStorage = async () => {
try {
const token = Taro.getStorageSync('access_token');
const userData = Taro.getStorageSync('User');
const userId = Taro.getStorageSync('UserId');
const tenantId = Taro.getStorageSync('TenantId');
if (token && userData) {
const userInfo = typeof userData === 'string' ? JSON.parse(userData) : userData;
setUser(userInfo);
setIsLoggedIn(true);
} else if (token && userId) {
// 如果有token和userId但没有完整用户信息标记为已登录但需要获取用户信息
setIsLoggedIn(true);
setUser({ userId, tenantId } as User);
} else {
// 没有本地登录信息,尝试自动登录
console.log('没有本地登录信息,尝试自动登录...');
const autoLoginResult = await autoLoginByOpenId();
if (!autoLoginResult) {
setUser(null);
setIsLoggedIn(false);
}
}
} catch (error) {
console.error('加载用户数据失败:', error);
setUser(null);
setIsLoggedIn(false);
} finally {
setLoading(false);
}
};
// 保存用户数据到本地存储
const saveUserToStorage = (token: string, userInfo: User) => {
try {
Taro.setStorageSync('access_token', token);
Taro.setStorageSync('User', userInfo);
// 确保关键字段不为空时才保存,避免覆盖现有数据
if (userInfo.userId) {
Taro.setStorageSync('UserId', userInfo.userId);
}
if (userInfo.tenantId) {
Taro.setStorageSync('TenantId', userInfo.tenantId);
}
if (userInfo.phone) {
Taro.setStorageSync('Phone', userInfo.phone);
}
// 保存头像和昵称信息
if (userInfo.avatar) {
Taro.setStorageSync('Avatar', userInfo.avatar);
}
if (userInfo.nickname) {
Taro.setStorageSync('Nickname', userInfo.nickname);
}
} catch (error) {
console.error('保存用户数据失败:', error);
}
};
// 登录用户
const loginUser = (token: string, userInfo: User) => {
setUser(userInfo);
setIsLoggedIn(true);
saveUserToStorage(token, userInfo);
};
// 退出登录
const logoutUser = () => {
setUser(null);
setIsLoggedIn(false);
// 清除本地存储
try {
Taro.removeStorageSync('access_token');
Taro.removeStorageSync('User');
Taro.removeStorageSync('UserId');
Taro.removeStorageSync('TenantId');
Taro.removeStorageSync('Phone');
Taro.removeStorageSync('userInfo');
} catch (error) {
console.error('清除用户数据失败:', error);
}
};
// 从服务器获取最新用户信息
const fetchUserInfo = async () => {
if (!isLoggedIn) {
return null;
}
try {
setLoading(true);
const userInfo = await getUserInfo();
setUser(userInfo);
// 更新本地存储
const token = Taro.getStorageSync('access_token');
if (token) {
saveUserToStorage(token, userInfo);
}
return userInfo;
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取失败可能是token过期清除登录状态
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage?.includes('401') || errorMessage?.includes('未授权')) {
logoutUser();
}
return null;
} finally {
setLoading(false);
}
};
// 更新用户信息
const updateUser = async (userData: Partial<User>) => {
if (!user) {
throw new Error('用户未登录');
}
try {
// 先获取最新的用户信息,确保我们有完整的数据
const latestUserInfo = await getUserInfo();
// 合并最新的用户信息和要更新的数据
const updatedUser = { ...latestUserInfo, ...userData };
// 调用API更新用户信息
await updateUserInfo(updatedUser);
// 更新本地状态
setUser(updatedUser);
// 更新本地存储
const token = Taro.getStorageSync('access_token');
if (token) {
saveUserToStorage(token, updatedUser);
}
Taro.showToast({
title: '更新成功',
icon: 'success',
duration: 1500
});
return updatedUser;
} catch (error) {
console.error('更新用户信息失败:', error);
Taro.showToast({
title: '更新失败',
icon: 'error',
duration: 1500
});
throw error;
}
};
// 检查是否有特定权限
const hasPermission = (permission: string) => {
if (!user || !user.authorities) {
return false;
}
return user.authorities.some(auth => auth.authority === permission);
};
// 检查是否有特定角色
const hasRole = (roleCode: string) => {
if (!user || !user.roles) {
return false;
}
return user.roles.some(role => role.roleCode === roleCode);
};
// 获取用户头像URL
const getAvatarUrl = () => {
return user?.avatar || user?.avatarUrl || '';
};
const getUserId = () => {
return user?.userId;
};
// 获取用户显示名称
const getDisplayName = () => {
return user?.nickname || user?.realName || user?.username || '未登录';
};
// 获取用户显示的角色(同步版本)
const getRoleName = () => {
if(hasRole('superAdmin')){
return '超级管理员';
}
if(hasRole('admin')){
return '管理员';
}
if(hasRole('staff')){
return '员工';
}
if(hasRole('vip')){
return 'VIP会员';
}
return '注册用户';
}
// 检查用户是否已实名认证
const isCertified = () => {
return user?.certification === true;
};
// 检查用户是否是管理员
const isAdmin = () => {
return user?.isAdmin === true;
};
const isSuperAdmin = () => {
return user?.isSuperAdmin === true;
};
// 获取用户余额
const getBalance = () => {
return user?.balance || 0;
};
// 获取用户积分
const getPoints = () => {
return user?.points || 0;
};
// 初始化时加载用户数据
useEffect(() => {
loadUserFromStorage().catch(error => {
console.error('初始化用户数据失败:', error);
setLoading(false);
});
}, []);
return {
// 状态
user,
isLoggedIn,
loading,
// 方法
loginUser,
logoutUser,
fetchUserInfo,
updateUser,
loadUserFromStorage,
autoLoginByOpenId,
// 工具方法
hasPermission,
hasRole,
getAvatarUrl,
getDisplayName,
getRoleName,
isCertified,
isAdmin,
getBalance,
getPoints,
getUserId,
isSuperAdmin
};
};

136
src/hooks/useUserData.ts Normal file
View File

@@ -0,0 +1,136 @@
import { useState, useEffect, useCallback } from 'react'
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
import {pageShopGift} from "@/api/shop/shopGift";
import {useUser} from "@/hooks/useUser";
import Taro from '@tarojs/taro'
import {getUserInfo} from "@/api/layout";
interface UserData {
balance: number
points: number
coupons: number
giftCards: number
orders: {
pending: number
paid: number
shipped: number
completed: number
refund: number
}
}
interface UseUserDataReturn {
data: UserData | null
loading: boolean
error: string | null
refresh: () => Promise<void>
updateBalance: (newBalance: number) => void
updatePoints: (newPoints: number) => void
}
export const useUserData = (): UseUserDataReturn => {
const [data, setData] = useState<UserData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// 获取用户数据
const fetchUserData = useCallback(async () => {
try {
setLoading(true)
setError(null)
if(!Taro.getStorageSync('UserId')){
return;
}
// 并发请求所有数据
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
getUserInfo(),
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
])
const newData: UserData = {
balance: userDataRes?.balance || 0.00,
points: userDataRes?.points || 0,
coupons: couponsRes?.count || 0,
giftCards: giftCardsRes?.count || 0,
orders: {
pending: 0,
paid: 0,
shipped: 0,
completed: 0,
refund: 0
}
}
setData(newData)
} catch (err) {
setError(err instanceof Error ? err.message : '获取用户数据失败')
} finally {
setLoading(false)
}
}, [])
// 刷新数据
const refresh = useCallback(async () => {
await fetchUserData()
}, [fetchUserData])
// 更新余额(本地更新,避免频繁请求)
const updateBalance = useCallback((newBalance: number) => {
setData(prev => prev ? { ...prev, balance: newBalance } : null)
}, [])
// 更新积分
const updatePoints = useCallback((newPoints: number) => {
setData(prev => prev ? { ...prev, points: newPoints } : null)
}, [])
// 初始化加载
useEffect(() => {
fetchUserData().then()
}, [fetchUserData])
return {
data,
loading,
error,
refresh,
updateBalance,
updatePoints
}
}
// 轻量级版本 - 只获取基础数据
export const useUserBasicData = () => {
const {user} = useUser()
const [balance, setBalance] = useState<number>(0)
const [points, setPoints] = useState<number>(0)
const [loading, setLoading] = useState(false)
const fetchBasicData = useCallback(async () => {
setLoading(true)
try {
setBalance(user?.balance || 0)
setPoints(user?.points || 0)
} catch (error) {
console.error('获取基础数据失败:', error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchBasicData().then()
}, [fetchBasicData])
return {
balance,
points,
loading,
refresh: fetchBasicData,
updateBalance: setBalance,
updatePoints: setPoints
}
}

View File

@@ -5,17 +5,177 @@ import {Target, Scan, Truck} from '@nutui/icons-react-taro'
import {getUserInfo} from "@/api/layout";
import navTo from "@/utils/common";
import {pageHjmCar} from "@/api/hjm/hjmCar";
import { ScanType } from '@/hooks/useUnifiedQRScan';
import { isValidJSON } from '@/utils/jsonUtils';
import { parseQRContent, confirmWechatQRLogin } from '@/api/passport/qr-login';
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
import { useUser } from '@/hooks/useUser';
import dayjs from 'dayjs';
const ExpirationTime = () => {
const [isAdmin, setIsAdmin] = useState<boolean>(false)
const [roleName, setRoleName] = useState<string>()
const { isAdmin: isUserAdmin } = useUser();
// 检测二维码类型
const detectScanType = (scanResult: string): ScanType => {
try {
// 1. 检查是否为JSON格式核销二维码
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
return ScanType.VERIFICATION;
}
}
// 2. 检查是否为登录二维码
const loginToken = parseQRContent(scanResult);
if (loginToken) {
return ScanType.LOGIN;
}
// 3. 检查是否为纯文本核销码6位数字
if (/^\d{6}$/.test(scanResult.trim())) {
return ScanType.VERIFICATION;
}
return ScanType.UNKNOWN;
} catch (error) {
console.error('检测二维码类型失败:', error);
return ScanType.UNKNOWN;
}
};
// 处理登录二维码
const handleLoginQR = async (scanResult: string) => {
const userId = Taro.getStorageSync('UserId');
if (!userId) {
throw new Error('请先登录小程序');
}
const token = parseQRContent(scanResult);
if (!token) {
throw new Error('无效的登录二维码');
}
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
if (confirmResult.success || confirmResult.status === 'confirmed') {
Taro.showToast({
title: '登录确认成功',
icon: 'success',
duration: 2000
});
return true;
} else {
throw new Error(confirmResult.message || '登录确认失败');
}
};
// 处理核销二维码
const handleVerificationQR = async (scanResult: string) => {
if (!isUserAdmin()) {
throw new Error('您没有核销权限');
}
let code = '';
// 判断是否为加密的JSON格式
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
// 解密获取核销码
const decryptedData = await decryptQrData({
token: json.token,
encryptedData: json.data
});
if (decryptedData) {
code = decryptedData.toString();
} else {
throw new Error('解密失败');
}
}
} else {
// 直接使用扫码结果作为核销码
code = scanResult.trim();
}
if (!code) {
throw new Error('无法获取有效的核销码');
}
// 验证核销码
const gift = await getShopGiftByCode(code);
if (!gift) {
throw new Error('核销码无效');
}
if (gift.status === 1) {
throw new Error('此礼品码已使用');
}
if (gift.status === 2) {
throw new Error('此礼品码已失效');
}
if (gift.userId === 0) {
throw new Error('此礼品码未认领');
}
// 执行核销
await updateShopGift({
...gift,
status: 1,
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
});
Taro.showToast({
title: '核销成功',
icon: 'success',
duration: 2000
});
return true;
};
const onScanCode = () => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
success: async (res) => {
console.log(res, 'qrcode...')
Taro.navigateTo({url: '/hjm/query?id=' + res.result})
const scanContent = res.result;
// 检测二维码类型
const scanType = detectScanType(scanContent);
try {
if (scanType === ScanType.LOGIN) {
// 处理登录二维码
await handleLoginQR(scanContent);
console.log('登录二维码处理成功');
return;
} else if (scanType === ScanType.VERIFICATION) {
// 处理核销二维码
await handleVerificationQR(scanContent);
console.log('核销二维码处理成功');
return;
}
} catch (error: any) {
console.log('特殊二维码处理失败:', error.message);
Taro.showToast({
title: error.message,
icon: 'error',
duration: 2000
});
return;
}
// 如果不是特殊二维码,作为车辆查询处理
console.log('作为车辆查询二维码处理:', scanContent);
Taro.navigateTo({url: '/hjm/query?id=' + scanContent});
},
fail: (res) => {
console.log(res, '扫码失败')

View File

@@ -2,8 +2,13 @@ import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
import './login.scss';
import {User} from "@/api/system/user/model";
const Login = () => {
interface LoginProps {
done?: (data: User) => void;
}
const Login: React.FC<LoginProps> = ({ done }) => {
const [isAgree, setIsAgree] = useState(false)
const [env, setEnv] = useState<string>()

484
src/utils/invite.ts Normal file
View File

@@ -0,0 +1,484 @@
import Taro from '@tarojs/taro'
import { bindRefereeRelation } from '@/api/invite'
/**
* 邀请参数接口
*/
export interface InviteParams {
inviter?: string;
source?: string;
t?: string;
}
/**
* 解析小程序启动参数中的邀请信息
*/
export function parseInviteParams(options: any): InviteParams | null {
try {
// 优先从 query.scene 参数中解析邀请信息
let sceneStr: string | null = null
if (options.query && options.query.scene) {
sceneStr = typeof options.query.scene === 'string' ? options.query.scene : String(options.query.scene)
} else if (options.scene) {
// 兼容直接从 scene 参数解析
sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene)
}
// 从 scene 参数中解析邀请信息
if (sceneStr) {
// 处理 uid_xxx 格式的邀请码
if (sceneStr.startsWith('uid_')) {
const inviterId = sceneStr.replace('uid_', '')
if (inviterId && !isNaN(parseInt(inviterId))) {
return {
inviter: inviterId,
source: 'qrcode',
t: Date.now().toString()
}
}
}
// 处理传统的 key=value&key=value 格式
const params: InviteParams = {}
const pairs = sceneStr.split('&')
pairs.forEach((pair: string) => {
const [key, value] = pair.split('=')
if (key && value) {
switch (key) {
case 'inviter':
params.inviter = decodeURIComponent(value)
break
case 'source':
params.source = decodeURIComponent(value)
break
case 't':
params.t = decodeURIComponent(value)
break
}
}
})
if (params.inviter) {
return params
}
}
// 从 query 参数中解析邀请信息(处理首页分享链接)
if (options.query) {
const query = options.query
if (query.inviter) {
return {
inviter: query.inviter,
source: query.source || 'share',
t: query.t
}
}
// 兼容旧版本
if (query.referrer) {
return {
inviter: query.referrer,
source: 'link'
}
}
}
return null
} catch (error) {
console.error('解析邀请参数失败:', error)
return null
}
}
/**
* 保存邀请信息到本地存储
*/
export function saveInviteParams(params: InviteParams) {
try {
const saveData = {
...params,
timestamp: Date.now()
}
Taro.setStorageSync('invite_params', saveData)
} catch (error) {
console.error('保存邀请参数失败:', error)
}
}
/**
* 获取本地存储的邀请信息
*/
export function getStoredInviteParams(): InviteParams | null {
try {
const stored = Taro.getStorageSync('invite_params')
if (stored && stored.inviter) {
// 检查是否过期24小时
const now = Date.now()
const expireTime = 24 * 60 * 60 * 1000 // 24小时
if (now - stored.timestamp < expireTime) {
return {
inviter: stored.inviter,
source: stored.source || 'unknown',
t: stored.t
}
} else {
// 过期则清除
clearInviteParams()
}
}
return null
} catch (error) {
console.error('获取邀请参数失败:', error)
return null
}
}
/**
* 清除本地存储的邀请信息
*/
export function clearInviteParams() {
try {
Taro.removeStorageSync('invite_params')
} catch (error) {
console.error('清除邀请参数失败:', error)
}
}
/**
* 处理邀请关系建立
*/
export async function handleInviteRelation(userId: number): Promise<boolean> {
try {
const inviteParams = getStoredInviteParams()
if (!inviteParams || !inviteParams.inviter) {
return false
}
const inviterId = parseInt(inviteParams.inviter)
if (isNaN(inviterId) || inviterId === userId) {
// 邀请人ID无效或自己邀请自己
clearInviteParams()
return false
}
// 防重复检查:检查是否已经处理过这个邀请关系
const relationKey = `invite_relation_${inviterId}_${userId}`
const existingRelation = Taro.getStorageSync(relationKey)
if (existingRelation) {
clearInviteParams() // 清除邀请参数
return true // 返回true表示关系已存在
}
// 设置API调用超时
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('API调用超时')), 5000)
);
// 使用新的绑定推荐关系接口
const apiPromise = bindRefereeRelation({
dealerId: inviterId,
userId: userId,
source: inviteParams.source || 'qrcode',
scene: inviteParams.source === 'qrcode' ? `uid_${inviterId}` : `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}`
});
// 等待API调用完成或超时
await Promise.race([apiPromise, timeoutPromise]);
// 标记邀请关系已处理设置过期时间为7天
Taro.setStorageSync(relationKey, {
inviterId,
userId,
timestamp: Date.now(),
source: inviteParams.source || 'qrcode'
})
// 清除本地存储的邀请参数
clearInviteParams()
return true
} catch (error) {
console.error('建立邀请关系失败:', error)
// 如果是网络错误或超时,不清除邀请参数,允许稍后重试
const errorMessage = error instanceof Error ? error.message : String(error)
if (errorMessage.includes('超时') || errorMessage.includes('网络')) {
console.log('网络问题,保留邀请参数供稍后重试')
return false
}
// 其他错误(如业务逻辑错误),清除邀请参数
clearInviteParams()
return false
}
}
/**
* 检查是否有待处理的邀请
*/
export function hasPendingInvite(): boolean {
const params = getStoredInviteParams()
return !!(params && params.inviter)
}
/**
* 获取邀请来源的显示名称
*/
export function getSourceDisplayName(source: string): string {
const sourceMap: Record<string, string> = {
'qrcode': '小程序码',
'link': '分享链接',
'share': '好友分享',
'poster': '海报分享',
'unknown': '未知来源'
}
return sourceMap[source] || source
}
/**
* 验证邀请码格式
*/
export function validateInviteCode(scene: string): boolean {
try {
if (!scene) return false
// 检查是否包含必要的参数
const hasInviter = scene.includes('inviter=')
const hasSource = scene.includes('source=')
return hasInviter && hasSource
} catch (error) {
return false
}
}
/**
* 生成邀请场景值
*/
export function generateInviteScene(inviterId: number, source: string): string {
const timestamp = Date.now()
return `inviter=${inviterId}&source=${source}&t=${timestamp}`
}
/**
* 统计邀请来源
*/
export function trackInviteSource(source: string, inviterId?: number) {
try {
// 记录邀请来源统计
const trackData = {
source,
inviterId,
timestamp: Date.now(),
userAgent: Taro.getSystemInfoSync()
}
// 可以发送到统计服务
console.log('邀请来源统计:', trackData)
// 暂存到本地,后续可批量上报
const existingTracks = Taro.getStorageSync('invite_tracks') || []
existingTracks.push(trackData)
// 只保留最近100条记录
if (existingTracks.length > 100) {
existingTracks.splice(0, existingTracks.length - 100)
}
Taro.setStorageSync('invite_tracks', existingTracks)
} catch (error) {
console.error('统计邀请来源失败:', error)
}
}
/**
* 调试工具:打印所有邀请相关的存储信息
*/
export function debugInviteInfo() {
try {
console.log('=== 邀请参数调试信息 ===')
// 获取启动参数
const launchOptions = Taro.getLaunchOptionsSync()
console.log('启动参数:', JSON.stringify(launchOptions, null, 2))
// 获取存储的邀请参数
const storedParams = Taro.getStorageSync('invite_params')
console.log('存储的邀请参数:', JSON.stringify(storedParams, null, 2))
// 获取用户信息
const userId = Taro.getStorageSync('UserId')
const userInfo = Taro.getStorageSync('userInfo')
console.log('用户ID:', userId)
console.log('用户信息:', JSON.stringify(userInfo, null, 2))
// 获取邀请统计
const inviteTracks = Taro.getStorageSync('invite_tracks')
console.log('邀请统计:', JSON.stringify(inviteTracks, null, 2))
console.log('=== 调试信息结束 ===')
return {
launchOptions,
storedParams,
userId,
userInfo,
inviteTracks
}
} catch (error) {
console.error('获取调试信息失败:', error)
return null
}
}
/**
* 检查并处理当前用户的邀请关系
* 用于在用户登录后立即检查是否需要建立邀请关系
*/
export async function checkAndHandleInviteRelation(): Promise<boolean> {
try {
// 清理过期的防重记录
cleanExpiredInviteRelations()
// 获取当前用户信息
const userInfo = Taro.getStorageSync('userInfo')
const userId = Taro.getStorageSync('UserId')
const finalUserId = userId || userInfo?.userId
if (!finalUserId) {
console.log('用户未登录,无法处理邀请关系')
return false
}
console.log('使用用户ID处理邀请关系:', finalUserId)
// 设置整体超时保护
const timeoutPromise = new Promise<boolean>((_, reject) =>
setTimeout(() => reject(new Error('邀请关系处理整体超时')), 6000)
);
const handlePromise = handleInviteRelation(parseInt(finalUserId));
return await Promise.race([handlePromise, timeoutPromise]);
} catch (error) {
console.error('检查邀请关系失败:', error)
// 记录失败次数,避免无限重试
const failKey = 'invite_handle_fail_count'
const failCount = Taro.getStorageSync(failKey) || 0
if (failCount >= 3) {
console.log('邀请关系处理失败次数过多,清除邀请参数')
clearInviteParams()
Taro.removeStorageSync(failKey)
} else {
Taro.setStorageSync(failKey, failCount + 1)
}
return false
}
}
/**
* 手动触发邀请关系建立
* 用于在特定页面或时机手动建立邀请关系
*/
export async function manualHandleInviteRelation(userId: number): Promise<boolean> {
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 function cleanExpiredInviteRelations() {
try {
const keys = Taro.getStorageInfoSync().keys
const expireTime = 7 * 24 * 60 * 60 * 1000 // 7天
const now = Date.now()
keys.forEach(key => {
if (key.startsWith('invite_relation_')) {
try {
const data = Taro.getStorageSync(key)
if (data && data.timestamp && (now - data.timestamp > expireTime)) {
Taro.removeStorageSync(key)
}
} catch (error) {
// 如果读取失败,直接删除
Taro.removeStorageSync(key)
}
}
})
} catch (error) {
console.error('清理过期邀请关系记录失败:', error)
}
}
/**
* 直接绑定推荐关系
* 用于直接调用绑定推荐关系接口
*/
export async function bindReferee(refereeId: number, userId?: number, source: string = 'qrcode'): Promise<boolean> {
try {
// 如果没有传入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({
dealerId: refereeId,
userId: targetUserId,
source: source,
scene: source === 'qrcode' ? `uid_${refereeId}` : undefined
})
return true
} catch (error: any) {
console.error('绑定推荐关系失败:', error)
return false
}
}

31
src/utils/jsonUtils.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* 判断字符串是否为有效的JSON格式
* @param str 要检测的字符串
* @returns boolean
*/
export function isValidJSON(str: string): boolean {
if (typeof str !== 'string' || str.trim() === '') {
return false;
}
try {
JSON.parse(str);
return true;
} catch (error) {
return false;
}
}
/**
* 安全解析JSON失败时返回默认值
* @param str JSON字符串
* @param defaultValue 默认值
* @returns 解析结果或默认值
*/
export function safeJSONParse<T>(str: string, defaultValue: T): T {
try {
return JSON.parse(str);
} catch (error) {
return defaultValue;
}
}

View File

@@ -25,7 +25,8 @@
"@/components/*": ["./src/components/*"],
"@/utils/*": ["./src/utils/*"],
"@/assets/*": ["./src/assets/*"],
"@/api/*": ["./src/api/*"]
"@/api/*": ["./src/api/*"],
"@/hooks/*": ["./src/hooks/*"]
}
},
"include": ["./src", "./types"],