feat(add): 新增多页面新增和编辑表单功能
- 添加编辑和新增收货地址页面,支持表单数据加载和提交 - 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置 - 实现文章新增及编辑页面,包含图片上传及多种文章属性配置 - 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理 - 引入统一表单提交成功和失败处理,支持编辑模式数据回显 - 配置统一eslint和editorconfig规则,增强代码规范和编辑体验 - 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
This commit is contained in:
132
src/utils/common.ts
Normal file
132
src/utils/common.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { goTo } from './navigation'
|
||||
|
||||
export default function navTo(url: string, isLogin = false) {
|
||||
if (isLogin) {
|
||||
if (!Taro.getStorageSync('access_token') || !Taro.getStorageSync('UserId')) {
|
||||
Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 500
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 使用新的导航工具,自动处理路径格式化
|
||||
goTo(url)
|
||||
}
|
||||
|
||||
|
||||
// 转base64
|
||||
export function fileToBase64(filePath:string) {
|
||||
return new Promise((resolve) => {
|
||||
let fileManager = Taro.getFileSystemManager();
|
||||
fileManager.readFile({
|
||||
filePath,
|
||||
encoding: 'base64',
|
||||
success: (e: any) => {
|
||||
resolve(`data:image/jpg;base64,${e.data}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 转义微信富文本图片样式
|
||||
* @param htmlText
|
||||
*/
|
||||
export function wxParse(htmlText:string) {
|
||||
// Replace <img> tags with max-width, height and margin styles to remove spacing
|
||||
htmlText = htmlText.replace(/\<img/gi, '<img style="max-width:100%;height:auto;margin:0;padding:0;display:block;"');
|
||||
|
||||
// Replace style attributes that do not contain text-align, add margin:0 to remove spacing
|
||||
htmlText = htmlText.replace(/style\s*?=\s*?(['"])(?!.*?text-align)[\s\S]*?\1/ig, 'style="max-width:100%;height:auto;margin:0;padding:0;display:block;"');
|
||||
|
||||
return htmlText;
|
||||
}
|
||||
|
||||
|
||||
export function copyText(text: string) {
|
||||
Taro.setClipboardData({
|
||||
data: text,
|
||||
success: function () {
|
||||
Taro.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
},
|
||||
fail: function () {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享商品链接
|
||||
* @param goodsId 商品ID
|
||||
*/
|
||||
export function shareGoodsLink(goodsId: string | number) {
|
||||
// 构建分享链接,这里需要根据你的实际域名调整
|
||||
const baseUrl = 'https://your-domain.com'; // 请替换为你的实际域名
|
||||
const shareUrl = `${baseUrl}/shop/goodsDetail/index?id=${goodsId}`;
|
||||
|
||||
copyText(shareUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取字符串,确保不超过指定的汉字长度
|
||||
* @param text 原始文本
|
||||
* @param maxLength 最大汉字长度,默认30
|
||||
* @returns 截取后的文本
|
||||
*/
|
||||
export function truncateText(text: string, maxLength: number = 30): string {
|
||||
if (!text) return '';
|
||||
|
||||
// 如果长度不超过限制,直接返回
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// 超过长度则截取
|
||||
return text.substring(0, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param amount
|
||||
* @param currency
|
||||
*/
|
||||
export function formatCurrency(amount: number, currency: string = 'CNY'): string {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单标题
|
||||
* @param goodsNames 商品名称数组
|
||||
* @param maxLength 最大长度,默认30
|
||||
* @returns 订单标题
|
||||
*/
|
||||
export function generateOrderTitle(goodsNames: string[], maxLength: number = 30): string {
|
||||
if (!goodsNames || goodsNames.length === 0) {
|
||||
return '商品订单';
|
||||
}
|
||||
|
||||
let title = '';
|
||||
if (goodsNames.length === 1) {
|
||||
title = goodsNames[0];
|
||||
} else {
|
||||
title = `${goodsNames[0]}等${goodsNames.length}件商品`;
|
||||
}
|
||||
|
||||
return truncateText(title, maxLength);
|
||||
}
|
||||
200
src/utils/couponUtils.ts
Normal file
200
src/utils/couponUtils.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { ShopUserCoupon } from '@/api/shop/shopUserCoupon/model'
|
||||
import { CouponCardProps } from '@/components/CouponCard'
|
||||
|
||||
/**
|
||||
* 将后端优惠券数据转换为前端组件所需格式
|
||||
*/
|
||||
export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps => {
|
||||
// 解析金额
|
||||
let amount = 0
|
||||
if (coupon.type === 10) {
|
||||
// 满减券:使用reducePrice
|
||||
amount = parseFloat(coupon.reducePrice || '0')
|
||||
} else if (coupon.type === 20) {
|
||||
// 折扣券:使用discount
|
||||
amount = coupon.discount || 0
|
||||
} else if (coupon.type === 30) {
|
||||
// 免费券:金额为0
|
||||
amount = 0
|
||||
}
|
||||
|
||||
// 解析最低消费金额
|
||||
const minAmount = parseFloat(coupon.minPrice || '0')
|
||||
|
||||
// 确定主题颜色
|
||||
const getTheme = (type?: number): CouponCardProps['theme'] => {
|
||||
switch (type) {
|
||||
case 10: return 'red' // 满减券-红色
|
||||
case 20: return 'orange' // 折扣券-橙色
|
||||
case 30: return 'green' // 免费券-绿色
|
||||
default: return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: coupon.id,
|
||||
amount,
|
||||
minAmount: minAmount > 0 ? minAmount : undefined,
|
||||
type: coupon.type as 10 | 20 | 30,
|
||||
status: coupon.status as 0 | 1 | 2,
|
||||
statusText: coupon.statusText,
|
||||
title: coupon.name || coupon.description || '优惠券',
|
||||
description: coupon.description,
|
||||
startTime: coupon.startTime,
|
||||
endTime: coupon.endTime,
|
||||
isExpiringSoon: coupon.isExpiringSoon,
|
||||
daysRemaining: coupon.daysRemaining,
|
||||
hoursRemaining: coupon.hoursRemaining,
|
||||
theme: getTheme(coupon.type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算优惠券折扣金额
|
||||
*/
|
||||
export const calculateCouponDiscount = (
|
||||
coupon: CouponCardProps,
|
||||
totalAmount: number
|
||||
): number => {
|
||||
// 检查是否满足使用条件
|
||||
if (coupon.minAmount && totalAmount < coupon.minAmount) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 检查优惠券状态
|
||||
if (coupon.status !== 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch (coupon.type) {
|
||||
case 10: // 满减券
|
||||
return coupon.amount
|
||||
case 20: // 折扣券
|
||||
return totalAmount * (1 - coupon.amount / 10)
|
||||
case 30: // 免费券
|
||||
return totalAmount
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查优惠券是否可用
|
||||
*/
|
||||
export const isCouponUsable = (
|
||||
coupon: CouponCardProps,
|
||||
totalAmount: number
|
||||
): boolean => {
|
||||
// 状态检查
|
||||
if (coupon.status !== 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 金额条件检查
|
||||
if (coupon.minAmount && totalAmount < coupon.minAmount) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优惠券不可用原因
|
||||
*/
|
||||
export const getCouponUnusableReason = (
|
||||
coupon: CouponCardProps,
|
||||
totalAmount: number
|
||||
): string => {
|
||||
if (coupon.status === 1) {
|
||||
return '优惠券已使用'
|
||||
}
|
||||
|
||||
if (coupon.status === 2) {
|
||||
return '优惠券已过期'
|
||||
}
|
||||
|
||||
if (coupon.minAmount && totalAmount < coupon.minAmount) {
|
||||
return `需满${coupon.minAmount}元才能使用`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化优惠券标题
|
||||
*/
|
||||
export const formatCouponTitle = (coupon: CouponCardProps): string => {
|
||||
if (coupon.title) {
|
||||
return coupon.title
|
||||
}
|
||||
|
||||
switch (coupon.type) {
|
||||
case 10: // 满减券
|
||||
if (coupon.minAmount && coupon.minAmount > 0) {
|
||||
return `满${coupon.minAmount}减${coupon.amount}`
|
||||
}
|
||||
return `立减${coupon.amount}元`
|
||||
case 20: // 折扣券
|
||||
if (coupon.minAmount && coupon.minAmount > 0) {
|
||||
return `满${coupon.minAmount}享${coupon.amount}折`
|
||||
}
|
||||
return `${coupon.amount}折优惠`
|
||||
case 30: // 免费券
|
||||
return '免费券'
|
||||
default:
|
||||
return '优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序优惠券列表
|
||||
* 按照优惠金额从大到小排序,同等优惠金额按过期时间排序
|
||||
*/
|
||||
export const sortCoupons = (
|
||||
coupons: CouponCardProps[],
|
||||
totalAmount: number
|
||||
): CouponCardProps[] => {
|
||||
return [...coupons].sort((a, b) => {
|
||||
// 先按可用性排序
|
||||
const aUsable = isCouponUsable(a, totalAmount)
|
||||
const bUsable = isCouponUsable(b, totalAmount)
|
||||
|
||||
if (aUsable && !bUsable) return -1
|
||||
if (!aUsable && bUsable) return 1
|
||||
|
||||
// 都可用或都不可用时,按优惠金额排序
|
||||
const aDiscount = calculateCouponDiscount(a, totalAmount)
|
||||
const bDiscount = calculateCouponDiscount(b, totalAmount)
|
||||
|
||||
if (aDiscount !== bDiscount) {
|
||||
return bDiscount - aDiscount // 优惠金额大的在前
|
||||
}
|
||||
|
||||
// 优惠金额相同时,按过期时间排序(即将过期的在前)
|
||||
if (a.endTime && b.endTime) {
|
||||
return new Date(a.endTime).getTime() - new Date(b.endTime).getTime()
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤可用优惠券
|
||||
*/
|
||||
export const filterUsableCoupons = (
|
||||
coupons: CouponCardProps[],
|
||||
totalAmount: number
|
||||
): CouponCardProps[] => {
|
||||
return coupons.filter(coupon => isCouponUsable(coupon, totalAmount))
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤不可用优惠券
|
||||
*/
|
||||
export const filterUnusableCoupons = (
|
||||
coupons: CouponCardProps[],
|
||||
totalAmount: number
|
||||
): CouponCardProps[] => {
|
||||
return coupons.filter(coupon => !isCouponUsable(coupon, totalAmount))
|
||||
}
|
||||
110
src/utils/domain.ts
Normal file
110
src/utils/domain.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// 解析域名结构
|
||||
export function getHost(): any {
|
||||
const host = window.location.host;
|
||||
return host.split('.');
|
||||
}
|
||||
|
||||
// 是否https
|
||||
export function isHttps() {
|
||||
const protocol = window.location.protocol;
|
||||
if (protocol == 'https:') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始域名
|
||||
* @return http://www.domain.com
|
||||
*/
|
||||
export function getOriginDomain(): string {
|
||||
return window.origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名的第一部分
|
||||
* 获取tenantId
|
||||
* @return 10140
|
||||
*/
|
||||
export function getDomainPart1(): any {
|
||||
const split = getHost();
|
||||
if (split[0] == '127') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof (split[0])) {
|
||||
return split[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过解析泛域名获取租户ID
|
||||
* https://10140.wsdns.cn
|
||||
* @return 10140
|
||||
*/
|
||||
export function getTenantId() {
|
||||
let tenantId = localStorage.getItem('TenantId');
|
||||
if(getDomainPart1()){
|
||||
tenantId = getDomainPart1();
|
||||
return tenantId;
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根域名
|
||||
* hostname
|
||||
*/
|
||||
export function getHostname(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名
|
||||
* @return https://www.domain.com
|
||||
*/
|
||||
export function getDomain(): string {
|
||||
return window.location.protocol + '//www.' + getRootDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根域名
|
||||
* abc.com
|
||||
*/
|
||||
export function getRootDomain(): string {
|
||||
const split = getHost();
|
||||
return split[split.length - 2] + '.' + split[split.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取二级域名
|
||||
* @return abc.com
|
||||
*/
|
||||
export function getSubDomainPath(): string {
|
||||
const split = getHost();
|
||||
if (split.length == 2) {
|
||||
return '';
|
||||
}
|
||||
return split[split.length - 3];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品标识
|
||||
* @return 10048
|
||||
*/
|
||||
export function getProductCode(): string | null {
|
||||
const subDomain = getSubDomainPath();
|
||||
if (subDomain == undefined) {
|
||||
return null;
|
||||
}
|
||||
const split = subDomain.split('-');
|
||||
return split[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台域名
|
||||
*/
|
||||
export function navSubDomain(path: string): string {
|
||||
return `${window.location.protocol}//${path}.${getRootDomain()}`;
|
||||
}
|
||||
|
||||
302
src/utils/errorHandler.ts
Normal file
302
src/utils/errorHandler.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
// 定义本地的RequestError类,避免循环依赖
|
||||
export class RequestError extends Error {
|
||||
public type: string;
|
||||
public code?: number;
|
||||
public data?: any;
|
||||
|
||||
constructor(message: string, type: string, code?: number, data?: any) {
|
||||
super(message);
|
||||
this.name = 'RequestError';
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
// 错误类型枚举
|
||||
export enum ErrorType {
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
|
||||
BUSINESS_ERROR = 'BUSINESS_ERROR',
|
||||
AUTH_ERROR = 'AUTH_ERROR',
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
|
||||
}
|
||||
|
||||
// 错误级别枚举
|
||||
export enum ErrorLevel {
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal'
|
||||
}
|
||||
|
||||
// 错误信息接口
|
||||
export interface ErrorInfo {
|
||||
message: string;
|
||||
level: ErrorLevel;
|
||||
type: string;
|
||||
stack?: string;
|
||||
extra?: any;
|
||||
timestamp: number;
|
||||
userId?: string;
|
||||
page?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局错误处理器
|
||||
*/
|
||||
class GlobalErrorHandler {
|
||||
private static instance: GlobalErrorHandler;
|
||||
private errorQueue: ErrorInfo[] = [];
|
||||
private maxQueueSize = 50;
|
||||
|
||||
private constructor() {
|
||||
this.setupGlobalErrorHandlers();
|
||||
}
|
||||
|
||||
public static getInstance(): GlobalErrorHandler {
|
||||
if (!GlobalErrorHandler.instance) {
|
||||
GlobalErrorHandler.instance = new GlobalErrorHandler();
|
||||
}
|
||||
return GlobalErrorHandler.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局错误处理器
|
||||
*/
|
||||
private setupGlobalErrorHandlers() {
|
||||
// 捕获未处理的Promise rejection
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
this.handleError(event.reason, ErrorLevel.ERROR, 'UnhandledPromiseRejection');
|
||||
event.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
public handleError(
|
||||
error: any,
|
||||
level: ErrorLevel = ErrorLevel.ERROR,
|
||||
type: string = 'Unknown',
|
||||
extra?: any
|
||||
) {
|
||||
const errorInfo = this.createErrorInfo(error, level, type, extra);
|
||||
|
||||
// 添加到错误队列
|
||||
this.addToQueue(errorInfo);
|
||||
|
||||
// 根据错误级别决定处理方式
|
||||
switch (level) {
|
||||
case ErrorLevel.FATAL:
|
||||
this.handleFatalError(errorInfo);
|
||||
break;
|
||||
case ErrorLevel.ERROR:
|
||||
this.handleNormalError(errorInfo);
|
||||
break;
|
||||
case ErrorLevel.WARNING:
|
||||
this.handleWarning(errorInfo);
|
||||
break;
|
||||
case ErrorLevel.INFO:
|
||||
this.handleInfo(errorInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
// 上报错误
|
||||
this.reportError(errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误信息对象
|
||||
*/
|
||||
private createErrorInfo(
|
||||
error: any,
|
||||
level: ErrorLevel,
|
||||
type: string,
|
||||
extra?: any
|
||||
): ErrorInfo {
|
||||
let message = '未知错误';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
stack = error.stack;
|
||||
} else if (error instanceof RequestError) {
|
||||
message = error.message;
|
||||
type = error.type;
|
||||
extra = { ...extra, code: error.code, data: error.data };
|
||||
} else if (typeof error === 'string') {
|
||||
message = error;
|
||||
} else if (error && typeof error === 'object') {
|
||||
message = error.message || error.errMsg || JSON.stringify(error);
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
level,
|
||||
type,
|
||||
stack,
|
||||
extra,
|
||||
timestamp: Date.now(),
|
||||
userId: Taro.getStorageSync('UserId') || undefined,
|
||||
page: this.getCurrentPage()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路径
|
||||
*/
|
||||
private getCurrentPage(): string {
|
||||
try {
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage?.route || 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到错误队列
|
||||
*/
|
||||
private addToQueue(errorInfo: ErrorInfo) {
|
||||
this.errorQueue.push(errorInfo);
|
||||
|
||||
// 保持队列大小
|
||||
if (this.errorQueue.length > this.maxQueueSize) {
|
||||
this.errorQueue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理致命错误
|
||||
*/
|
||||
private handleFatalError(errorInfo: ErrorInfo) {
|
||||
console.error('Fatal Error:', errorInfo);
|
||||
|
||||
Taro.showModal({
|
||||
title: '严重错误',
|
||||
content: '应用遇到严重错误,需要重启',
|
||||
showCancel: false,
|
||||
confirmText: '重启应用',
|
||||
success: () => {
|
||||
Taro.reLaunch({ url: '/pages/index/index' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理普通错误
|
||||
*/
|
||||
private handleNormalError(errorInfo: ErrorInfo) {
|
||||
console.error('Error:', errorInfo);
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let title = '操作失败';
|
||||
|
||||
if (errorInfo.type === ErrorType.NETWORK_ERROR) {
|
||||
title = '网络连接失败';
|
||||
} else if (errorInfo.type === ErrorType.TIMEOUT_ERROR) {
|
||||
title = '请求超时';
|
||||
} else if (errorInfo.type === ErrorType.AUTH_ERROR) {
|
||||
title = '认证失败';
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: errorInfo.message || title,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理警告
|
||||
*/
|
||||
private handleWarning(errorInfo: ErrorInfo) {
|
||||
console.warn('Warning:', errorInfo);
|
||||
|
||||
// 警告通常不需要用户交互,只记录日志
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理信息
|
||||
*/
|
||||
private handleInfo(errorInfo: ErrorInfo) {
|
||||
console.info('Info:', errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报错误到服务器
|
||||
*/
|
||||
private reportError(errorInfo: ErrorInfo) {
|
||||
try {
|
||||
// 这里可以实现错误上报逻辑
|
||||
// 例如发送到后端日志系统、第三方监控服务等
|
||||
|
||||
// 示例:发送到后端
|
||||
// request.post('/api/error/report', errorInfo).catch(() => {
|
||||
// // 上报失败也不要影响用户体验
|
||||
// });
|
||||
|
||||
// 开发环境下打印详细信息
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.group('🚨 Error Report');
|
||||
console.log('Message:', errorInfo.message);
|
||||
console.log('Level:', errorInfo.level);
|
||||
console.log('Type:', errorInfo.type);
|
||||
console.log('Page:', errorInfo.page);
|
||||
console.log('UserId:', errorInfo.userId);
|
||||
console.log('Timestamp:', new Date(errorInfo.timestamp).toLocaleString());
|
||||
if (errorInfo.stack) {
|
||||
console.log('Stack:', errorInfo.stack);
|
||||
}
|
||||
if (errorInfo.extra) {
|
||||
console.log('Extra:', errorInfo.extra);
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
} catch (reportError) {
|
||||
console.error('Failed to report error:', reportError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误队列
|
||||
*/
|
||||
public getErrorQueue(): ErrorInfo[] {
|
||||
return [...this.errorQueue];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空错误队列
|
||||
*/
|
||||
public clearErrorQueue() {
|
||||
this.errorQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const errorHandler = GlobalErrorHandler.getInstance();
|
||||
|
||||
// 便捷方法
|
||||
export const handleError = (error: any, level?: ErrorLevel, type?: string, extra?: any) => {
|
||||
errorHandler.handleError(error, level, type, extra);
|
||||
};
|
||||
|
||||
export const handleFatalError = (error: any, extra?: any) => {
|
||||
errorHandler.handleError(error, ErrorLevel.FATAL, 'FatalError', extra);
|
||||
};
|
||||
|
||||
export const handleWarning = (error: any, extra?: any) => {
|
||||
errorHandler.handleError(error, ErrorLevel.WARNING, 'Warning', extra);
|
||||
};
|
||||
|
||||
export const handleInfo = (message: string, extra?: any) => {
|
||||
errorHandler.handleError(message, ErrorLevel.INFO, 'Info', extra);
|
||||
};
|
||||
|
||||
export default errorHandler;
|
||||
485
src/utils/invite.ts
Normal file
485
src/utils/invite.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
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 = 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': '好友分享',
|
||||
'goods_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
31
src/utils/jsonUtils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
192
src/utils/navigation.ts
Normal file
192
src/utils/navigation.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
/**
|
||||
* 导航选项接口
|
||||
*/
|
||||
export interface NavigationOptions {
|
||||
/** 页面路径 */
|
||||
url: string
|
||||
/** 页面参数 */
|
||||
params?: Record<string, any>
|
||||
/** 是否替换当前页面(使用redirectTo) */
|
||||
replace?: boolean
|
||||
/** 是否重新启动应用(使用reLaunch) */
|
||||
relaunch?: boolean
|
||||
/** 是否切换到tabBar页面(使用switchTab) */
|
||||
switchTab?: boolean
|
||||
/** 成功回调 */
|
||||
success?: (res: any) => void
|
||||
/** 失败回调 */
|
||||
fail?: (res: any) => void
|
||||
/** 完成回调 */
|
||||
complete?: (res: any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化页面路径
|
||||
* @param url 原始路径
|
||||
* @returns 格式化后的路径
|
||||
*/
|
||||
function formatUrl(url: string): string {
|
||||
// 如果不是以"/"开头,自动添加
|
||||
if (!url.startsWith('/')) {
|
||||
url = '/' + url
|
||||
}
|
||||
|
||||
// 如果不是以"/pages/"开头,自动添加
|
||||
// if (!url.startsWith('/pages/')) {
|
||||
// // 移除开头的"/",然后添加"/pages/"
|
||||
// url = '/pages/' + url.replace(/^\/+/, '')
|
||||
// }
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建带参数的URL
|
||||
* @param url 基础URL
|
||||
* @param params 参数对象
|
||||
* @returns 完整的URL
|
||||
*/
|
||||
function buildUrlWithParams(url: string, params?: Record<string, any>): string {
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
return url
|
||||
}
|
||||
|
||||
const queryString = Object.entries(params)
|
||||
.map(([key, value]) => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('&')
|
||||
|
||||
return queryString ? `${url}?${queryString}` : url
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的页面导航函数
|
||||
* @param options 导航选项
|
||||
*/
|
||||
export function navigateTo(options: NavigationOptions | string): void {
|
||||
console.log(options,'options')
|
||||
// 如果传入的是字符串,转换为选项对象
|
||||
const opts: NavigationOptions = typeof options === 'string'
|
||||
? { url: options }
|
||||
: options
|
||||
// 格式化URL
|
||||
const formattedUrl = formatUrl(opts.url)
|
||||
|
||||
// 构建完整URL(包含参数)
|
||||
const fullUrl = buildUrlWithParams(formattedUrl, opts.params)
|
||||
|
||||
// 默认错误处理函数
|
||||
const defaultFail = (res?: any) => {
|
||||
console.error('页面导航失败:', res)
|
||||
if (opts.fail) {
|
||||
opts.fail(res)
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 根据不同的导航类型选择对应的Taro方法
|
||||
if (opts.switchTab) {
|
||||
Taro.switchTab({
|
||||
url: fullUrl,
|
||||
success: opts.success,
|
||||
fail: defaultFail,
|
||||
complete: opts.complete
|
||||
})
|
||||
} else if (opts.relaunch) {
|
||||
Taro.reLaunch({
|
||||
url: fullUrl,
|
||||
success: opts.success,
|
||||
fail: defaultFail,
|
||||
complete: opts.complete
|
||||
})
|
||||
} else if (opts.replace) {
|
||||
Taro.redirectTo({
|
||||
url: fullUrl,
|
||||
success: opts.success,
|
||||
fail: defaultFail,
|
||||
complete: opts.complete
|
||||
})
|
||||
} else {
|
||||
console.log('这里🌶。 ', fullUrl)
|
||||
|
||||
Taro.navigateTo({
|
||||
url: fullUrl,
|
||||
success: opts.success,
|
||||
fail: defaultFail,
|
||||
complete: opts.complete
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航到指定页面(默认方式)
|
||||
* @param url 页面路径
|
||||
* @param params 页面参数
|
||||
*/
|
||||
export function goTo(url: string, params?: Record<string, any>): void {
|
||||
navigateTo({ url, params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换当前页面
|
||||
* @param url 页面路径
|
||||
* @param params 页面参数
|
||||
*/
|
||||
export function redirectTo(url: string, params?: Record<string, any>): void {
|
||||
navigateTo({ url, params, replace: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新启动应用
|
||||
* @param url 页面路径
|
||||
* @param params 页面参数
|
||||
*/
|
||||
export function reLaunch(url: string, params?: Record<string, any>): void {
|
||||
navigateTo({ url, params, relaunch: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到tabBar页面
|
||||
* @param url 页面路径
|
||||
*/
|
||||
export function switchTab(url: string): void {
|
||||
navigateTo({ url, switchTab: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
* @param delta 返回的页面数,默认为1
|
||||
*/
|
||||
export function goBack(delta: number = 1): void {
|
||||
Taro.navigateBack({ delta })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面栈
|
||||
*/
|
||||
export function getCurrentPages() {
|
||||
return Taro.getCurrentPages()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路径
|
||||
*/
|
||||
export function getCurrentRoute(): string {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
return currentPage ? currentPage.route || '' : ''
|
||||
}
|
||||
|
||||
// 导出默认的导航函数
|
||||
export default navigateTo
|
||||
155
src/utils/networkCheck.ts
Normal file
155
src/utils/networkCheck.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
/**
|
||||
* 网络连接检测工具
|
||||
*/
|
||||
export class NetworkChecker {
|
||||
/**
|
||||
* 检查网络连接状态
|
||||
*/
|
||||
static async checkNetworkStatus(): Promise<{
|
||||
isConnected: boolean;
|
||||
networkType: string;
|
||||
message: string;
|
||||
}> {
|
||||
try {
|
||||
const networkInfo = await Taro.getNetworkType();
|
||||
const isConnected = networkInfo.networkType !== 'none';
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
networkType: networkInfo.networkType,
|
||||
message: isConnected
|
||||
? `网络连接正常 (${networkInfo.networkType})`
|
||||
: '网络连接异常'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('检查网络状态失败:', error);
|
||||
return {
|
||||
isConnected: false,
|
||||
networkType: 'unknown',
|
||||
message: '无法检测网络状态'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试API连接
|
||||
*/
|
||||
static async testAPIConnection(baseUrl: string): Promise<{
|
||||
success: boolean;
|
||||
responseTime: number;
|
||||
message: string;
|
||||
}> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await Taro.request({
|
||||
url: `${baseUrl}/health`,
|
||||
method: 'GET',
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: response.statusCode === 200,
|
||||
responseTime,
|
||||
message: `API连接${response.statusCode === 200 ? '正常' : '异常'} (${responseTime}ms)`
|
||||
};
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
console.error('API连接测试失败:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
responseTime,
|
||||
message: `API连接失败 (${responseTime}ms): ${error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合网络诊断
|
||||
*/
|
||||
static async diagnoseNetwork(baseUrl: string): Promise<{
|
||||
network: any;
|
||||
api: any;
|
||||
suggestions: string[];
|
||||
}> {
|
||||
console.log('🔍 开始网络诊断...');
|
||||
|
||||
const network = await this.checkNetworkStatus();
|
||||
const api = await this.testAPIConnection(baseUrl);
|
||||
|
||||
const suggestions: string[] = [];
|
||||
|
||||
if (!network.isConnected) {
|
||||
suggestions.push('请检查网络连接');
|
||||
suggestions.push('尝试切换网络环境(WiFi/移动数据)');
|
||||
}
|
||||
|
||||
if (!api.success) {
|
||||
suggestions.push('服务器可能暂时不可用');
|
||||
suggestions.push('请稍后重试');
|
||||
if (api.responseTime > 10000) {
|
||||
suggestions.push('网络响应较慢,建议检查网络质量');
|
||||
}
|
||||
}
|
||||
|
||||
if (network.networkType === 'wifi') {
|
||||
suggestions.push('WiFi连接正常,如仍有问题请检查路由器');
|
||||
} else if (network.networkType === '4g' || network.networkType === '5g') {
|
||||
suggestions.push('移动网络连接,请确保有足够的流量');
|
||||
}
|
||||
|
||||
console.log('📊 网络诊断结果:', { network, api, suggestions });
|
||||
|
||||
return { network, api, suggestions };
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示网络诊断结果
|
||||
*/
|
||||
static async showNetworkDiagnosis(baseUrl: string) {
|
||||
Taro.showLoading({ title: '诊断网络中...', mask: true });
|
||||
|
||||
try {
|
||||
const diagnosis = await this.diagnoseNetwork(baseUrl);
|
||||
Taro.hideLoading();
|
||||
|
||||
const content = [
|
||||
`网络状态: ${diagnosis.network.message}`,
|
||||
`API连接: ${diagnosis.api.message}`,
|
||||
'',
|
||||
'建议:',
|
||||
...diagnosis.suggestions.map(s => `• ${s}`)
|
||||
].join('\n');
|
||||
|
||||
Taro.showModal({
|
||||
title: '网络诊断结果',
|
||||
content,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
});
|
||||
} catch (error) {
|
||||
Taro.hideLoading();
|
||||
console.error('网络诊断失败:', error);
|
||||
|
||||
Taro.showModal({
|
||||
title: '诊断失败',
|
||||
content: '无法完成网络诊断,请检查网络连接后重试',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法
|
||||
*/
|
||||
export const checkNetwork = () => NetworkChecker.checkNetworkStatus();
|
||||
export const testAPI = (baseUrl: string) => NetworkChecker.testAPIConnection(baseUrl);
|
||||
export const diagnoseNetwork = (baseUrl: string) => NetworkChecker.diagnoseNetwork(baseUrl);
|
||||
export const showNetworkDiagnosis = (baseUrl: string) => NetworkChecker.showNetworkDiagnosis(baseUrl);
|
||||
32
src/utils/orderGoods.ts
Normal file
32
src/utils/orderGoods.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ShopOrderGoods } from '@/api/shop/shopOrderGoods/model';
|
||||
|
||||
/**
|
||||
* Normalize order goods data returned by the order/page API.
|
||||
*
|
||||
* In practice different backends may return different field names (orderGoods/orderGoodsList/goodsList...),
|
||||
* and the item fields can also differ (goodsName/title/name, totalNum/quantity, etc.).
|
||||
*
|
||||
* We normalize them to ShopOrderGoods so list pages can render without doing N+1 requests per order.
|
||||
*/
|
||||
export const normalizeOrderGoodsList = (order: any): ShopOrderGoods[] => {
|
||||
const raw =
|
||||
order?.orderGoods ||
|
||||
order?.orderGoodsList ||
|
||||
order?.goodsList ||
|
||||
order?.goods ||
|
||||
[];
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
return raw.map((g: any) => ({
|
||||
...g,
|
||||
goodsId: g?.goodsId ?? g?.itemId ?? g?.goods_id,
|
||||
skuId: g?.skuId ?? g?.sku_id,
|
||||
// When the API returns minimal fields, fall back to order title to avoid blank names.
|
||||
goodsName: g?.goodsName ?? g?.goodsTitle ?? g?.title ?? g?.name ?? order?.title ?? '商品',
|
||||
image: g?.image ?? g?.goodsImage ?? g?.cover ?? g?.pic,
|
||||
spec: g?.spec ?? g?.specInfo ?? g?.spec_name,
|
||||
totalNum: g?.totalNum ?? g?.quantity ?? g?.num ?? g?.count,
|
||||
price: g?.price ?? g?.payPrice ?? g?.goodsPrice ?? g?.unitPrice
|
||||
}));
|
||||
};
|
||||
|
||||
501
src/utils/payment.ts
Normal file
501
src/utils/payment.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { createOrder, WxPayResult } from '@/api/shop/shopOrder';
|
||||
import { OrderCreateRequest } from '@/api/shop/shopOrder/model';
|
||||
import { getSelectedStoreFromStorage, getSelectedStoreIdFromStorage } from '@/utils/storeSelection';
|
||||
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
|
||||
import type { ShopWarehouse } from '@/api/shop/shopWarehouse/model';
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 支付类型枚举
|
||||
*/
|
||||
export enum PaymentType {
|
||||
BALANCE = 0, // 余额支付
|
||||
WECHAT = 1, // 微信支付
|
||||
ALIPAY = 3, // 支付宝支付
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果回调
|
||||
*/
|
||||
export interface PaymentCallback {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一支付处理类
|
||||
*/
|
||||
export class PaymentHandler {
|
||||
// 简单缓存,避免频繁请求(小程序单次运行生命周期内有效)
|
||||
private static storeRidersCache = new Map<number, ShopStoreRider[]>();
|
||||
private static warehousesCache: ShopWarehouse[] | null = null;
|
||||
|
||||
/**
|
||||
* 执行支付
|
||||
* @param orderData 订单数据
|
||||
* @param paymentType 支付类型
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
static async pay(
|
||||
orderData: OrderCreateRequest,
|
||||
paymentType: PaymentType,
|
||||
callback?: PaymentCallback
|
||||
): Promise<void> {
|
||||
Taro.showLoading({ title: '支付中...' });
|
||||
|
||||
try {
|
||||
// 若调用方未指定门店,则自动注入“已选门店”,用于订单门店归属/统计。
|
||||
if (orderData.storeId === undefined || orderData.storeId === null) {
|
||||
const storeId = getSelectedStoreIdFromStorage();
|
||||
if (storeId) {
|
||||
orderData.storeId = storeId;
|
||||
}
|
||||
}
|
||||
if (!orderData.storeName) {
|
||||
const store = getSelectedStoreFromStorage();
|
||||
if (store?.name) {
|
||||
orderData.storeName = store.name;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动派单:按门店骑手优先级(dispatchPriority)选择 riderId(不覆盖手动指定)
|
||||
if ((orderData.riderId === undefined || orderData.riderId === null) && orderData.storeId) {
|
||||
const riderUserId = await this.pickRiderUserIdForStore(orderData.storeId);
|
||||
if (riderUserId) {
|
||||
orderData.riderId = riderUserId;
|
||||
}
|
||||
}
|
||||
|
||||
// 仓库选择:若未指定 warehouseId,则按“离门店最近”兜底选择一个(不覆盖手动指定)
|
||||
if ((orderData.warehouseId === undefined || orderData.warehouseId === null) && orderData.storeId) {
|
||||
const warehouseId = await this.pickWarehouseIdForStore(orderData.storeId);
|
||||
if (warehouseId) {
|
||||
orderData.warehouseId = warehouseId;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置支付类型
|
||||
orderData.payType = paymentType;
|
||||
|
||||
console.log('创建订单请求:', orderData);
|
||||
|
||||
// 创建订单
|
||||
const result = await createOrder(orderData);
|
||||
|
||||
console.log('订单创建结果:', result);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('创建订单失败');
|
||||
}
|
||||
|
||||
// 验证订单创建结果
|
||||
if (!result.orderNo) {
|
||||
throw new Error('订单号获取失败');
|
||||
}
|
||||
|
||||
let paymentSuccess = false;
|
||||
|
||||
// 根据支付类型处理
|
||||
switch (paymentType) {
|
||||
case PaymentType.WECHAT:
|
||||
await this.handleWechatPay(result);
|
||||
paymentSuccess = true;
|
||||
break;
|
||||
case PaymentType.BALANCE:
|
||||
paymentSuccess = await this.handleBalancePay(result);
|
||||
break;
|
||||
case PaymentType.ALIPAY:
|
||||
await this.handleAlipay(result);
|
||||
paymentSuccess = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error('不支持的支付方式');
|
||||
}
|
||||
|
||||
// 只有确认支付成功才显示成功提示和跳转
|
||||
if (paymentSuccess) {
|
||||
console.log('支付成功,订单号:', result.orderNo);
|
||||
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
callback?.onSuccess?.();
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
Taro.navigateTo({ url: '/user/order/order' });
|
||||
}, 2000);
|
||||
} else {
|
||||
throw new Error('支付未完成');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('支付失败:', error);
|
||||
|
||||
// 获取详细错误信息
|
||||
const errorMessage = this.getErrorMessage(error);
|
||||
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error'
|
||||
});
|
||||
|
||||
// 标记错误已处理,避免上层重复处理
|
||||
error.handled = true;
|
||||
callback?.onError?.(errorMessage);
|
||||
|
||||
// 重新抛出错误,让上层知道支付失败
|
||||
throw error;
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
callback?.onComplete?.();
|
||||
}
|
||||
}
|
||||
|
||||
private static parseLngLat(raw: string | undefined): { lng: number; lat: number } | null {
|
||||
const text = (raw || '').trim();
|
||||
if (!text) return null;
|
||||
const parts = text.split(/[,\s]+/).filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
const a = parseFloat(parts[0]);
|
||||
const b = parseFloat(parts[1]);
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90;
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180;
|
||||
if (looksLikeLngLat) return { lng: a, lat: b };
|
||||
if (looksLikeLatLng) return { lng: b, lat: a };
|
||||
return null;
|
||||
}
|
||||
|
||||
private static distanceMeters(a: { lng: number; lat: number }, b: { lng: number; lat: number }) {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180;
|
||||
const R = 6371000;
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLng = toRad(b.lng - a.lng);
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
const sin1 = Math.sin(dLat / 2);
|
||||
const sin2 = Math.sin(dLng / 2);
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2;
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||
}
|
||||
|
||||
private static async getRidersForStore(storeId: number): Promise<ShopStoreRider[]> {
|
||||
const cached = this.storeRidersCache.get(storeId);
|
||||
if (cached) return cached;
|
||||
|
||||
// 后端字段可能叫 dealerId 或 storeId,这里都带上,服务端忽略未知字段即可。
|
||||
// 这里做一次路径兼容(camel vs kebab),避免接口路径不一致导致整单失败。
|
||||
const list = await this.listByCompatEndpoint<ShopStoreRider>(
|
||||
['/shop/shop-store-rider'],
|
||||
{
|
||||
storeId: storeId,
|
||||
status: 1
|
||||
}
|
||||
);
|
||||
|
||||
const usable = (list || []).filter(r => r?.isDelete !== 1 && (r.status === undefined || r.status === 1));
|
||||
this.storeRidersCache.set(storeId, usable);
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static async pickRiderUserIdForStore(storeId: number): Promise<number | undefined> {
|
||||
const riders = await this.getRidersForStore(storeId);
|
||||
if (!riders.length) return undefined;
|
||||
|
||||
// 优先:启用 + 在线 + 自动派单,再按 dispatchPriority 由高到低
|
||||
const score = (r: ShopStoreRider) => {
|
||||
const enabled = (r.status === undefined || r.status === 1) ? 1 : 0;
|
||||
const online = r.workStatus === 1 ? 1 : 0;
|
||||
const auto = r.autoDispatchEnabled === 1 ? 1 : 0;
|
||||
const p = typeof r.dispatchPriority === 'number' ? r.dispatchPriority : 0;
|
||||
return enabled * 1000 + online * 100 + auto * 10 + p;
|
||||
};
|
||||
|
||||
const sorted = [...riders].sort((a, b) => score(b) - score(a));
|
||||
return sorted[0]?.userId;
|
||||
}
|
||||
|
||||
private static async getWarehouses(): Promise<ShopWarehouse[]> {
|
||||
if (this.warehousesCache) return this.warehousesCache;
|
||||
const list = await this.listByCompatEndpoint<ShopWarehouse>(
|
||||
['/shop/shop-warehouse'],
|
||||
{}
|
||||
);
|
||||
const usable = (list || []).filter(w => w?.isDelete !== 1 && (w.status === undefined || w.status === 1));
|
||||
this.warehousesCache = usable;
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static async pickWarehouseIdForStore(storeId: number): Promise<number | undefined> {
|
||||
const store = getSelectedStoreFromStorage();
|
||||
if (!store?.id || store.id !== storeId) return undefined;
|
||||
|
||||
// 一门店一默认仓库:优先使用门店自带的 warehouseId
|
||||
if (store.warehouseId) return store.warehouseId;
|
||||
|
||||
const storeCoords = this.parseLngLat(store.lngAndLat || store.location);
|
||||
if (!storeCoords) return undefined;
|
||||
|
||||
const warehouses = await this.getWarehouses();
|
||||
if (!warehouses.length) return undefined;
|
||||
|
||||
// 优先选择“门店仓”,否则选最近的任意仓库
|
||||
const candidates = warehouses.filter(w => w.type?.includes('门店') || w.type?.includes('门店仓'));
|
||||
const list = candidates.length ? candidates : warehouses;
|
||||
|
||||
const withDistance = list
|
||||
.map(w => {
|
||||
const coords = this.parseLngLat(w.lngAndLat);
|
||||
if (!coords) return { w, d: Number.POSITIVE_INFINITY };
|
||||
return { w, d: this.distanceMeters(storeCoords, coords) };
|
||||
})
|
||||
.sort((a, b) => a.d - b.d);
|
||||
|
||||
return withDistance[0]?.w?.id;
|
||||
}
|
||||
|
||||
private static async listByCompatEndpoint<T>(
|
||||
urls: string[],
|
||||
params: Record<string, any>
|
||||
): Promise<T[]> {
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res: any = await (request as any).get(url, params, { showError: false });
|
||||
if (res?.code === 0 && Array.isArray(res?.data)) {
|
||||
return res.data as T[];
|
||||
}
|
||||
} catch (_e) {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信支付
|
||||
*/
|
||||
private static async handleWechatPay(result: WxPayResult): Promise<void> {
|
||||
console.log('处理微信支付:', result);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('微信支付参数错误');
|
||||
}
|
||||
|
||||
// 验证微信支付必要参数
|
||||
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
|
||||
throw new Error('微信支付参数不完整');
|
||||
}
|
||||
|
||||
try {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
nonceStr: result.nonceStr,
|
||||
package: result.package,
|
||||
signType: result.signType as any, // 类型转换,因为微信支付的signType是字符串
|
||||
paySign: result.paySign,
|
||||
});
|
||||
|
||||
console.log('微信支付成功');
|
||||
} catch (payError: any) {
|
||||
console.error('微信支付失败:', payError);
|
||||
|
||||
// 处理微信支付特定错误
|
||||
if (payError.errMsg) {
|
||||
if (payError.errMsg.includes('cancel')) {
|
||||
throw new Error('用户取消支付');
|
||||
} else if (payError.errMsg.includes('fail')) {
|
||||
throw new Error('微信支付失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('微信支付失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理余额支付
|
||||
*/
|
||||
private static async handleBalancePay(result: any): Promise<boolean> {
|
||||
console.log('处理余额支付:', result);
|
||||
|
||||
if (!result || !result.orderNo) {
|
||||
throw new Error('余额支付参数错误');
|
||||
}
|
||||
|
||||
// 检查支付状态 - 根据后端返回的字段调整
|
||||
if (result.payStatus === false || result.payStatus === 0 || result.payStatus === '0') {
|
||||
throw new Error('余额不足或支付失败');
|
||||
}
|
||||
|
||||
// 检查订单状态 - 1表示已付款
|
||||
if (result.orderStatus !== undefined && result.orderStatus !== 1) {
|
||||
throw new Error('订单状态异常,支付可能未成功');
|
||||
}
|
||||
|
||||
// 验证实际扣款金额
|
||||
if (result.payPrice !== undefined) {
|
||||
const payPrice = parseFloat(result.payPrice);
|
||||
if (payPrice <= 0) {
|
||||
throw new Error('支付金额异常');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有错误信息字段,检查是否有错误
|
||||
if (result.error || result.errorMsg) {
|
||||
throw new Error(result.error || result.errorMsg);
|
||||
}
|
||||
|
||||
console.log('余额支付验证通过');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付宝支付
|
||||
*/
|
||||
private static async handleAlipay(_result: any): Promise<void> {
|
||||
// 支付宝支付逻辑,根据实际情况实现
|
||||
throw new Error('支付宝支付暂未实现');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细错误信息
|
||||
*/
|
||||
private static getErrorMessage(error: any): string {
|
||||
if (!error.message) {
|
||||
return '支付失败,请重试';
|
||||
}
|
||||
|
||||
const message = error.message;
|
||||
|
||||
// 余额相关错误
|
||||
if (message.includes('余额不足') || message.includes('balance')) {
|
||||
return '账户余额不足,请充值后重试';
|
||||
}
|
||||
|
||||
// 优惠券相关错误
|
||||
if (message.includes('优惠券') || message.includes('coupon')) {
|
||||
return '优惠券使用失败,请重新选择';
|
||||
}
|
||||
|
||||
// 库存相关错误
|
||||
if (message.includes('库存') || message.includes('stock')) {
|
||||
return '商品库存不足,请减少购买数量';
|
||||
}
|
||||
|
||||
// 地址相关错误
|
||||
if (message.includes('地址') || message.includes('address')) {
|
||||
return '收货地址信息有误,请重新选择';
|
||||
}
|
||||
|
||||
// 订单相关错误
|
||||
if (message.includes('订单') || message.includes('order')) {
|
||||
return '订单创建失败,请重试';
|
||||
}
|
||||
|
||||
// 网络相关错误
|
||||
if (message.includes('网络') || message.includes('network') || message.includes('timeout')) {
|
||||
return '网络连接异常,请检查网络后重试';
|
||||
}
|
||||
|
||||
// 微信支付相关错误
|
||||
if (message.includes('微信') || message.includes('wechat') || message.includes('wx')) {
|
||||
return '微信支付失败,请重试';
|
||||
}
|
||||
|
||||
// 返回原始错误信息
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷支付方法
|
||||
*/
|
||||
export const quickPay = {
|
||||
/**
|
||||
* 微信支付
|
||||
*/
|
||||
wechat: (orderData: OrderCreateRequest, callback?: PaymentCallback) => {
|
||||
return PaymentHandler.pay(orderData, PaymentType.WECHAT, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 余额支付
|
||||
*/
|
||||
balance: (orderData: OrderCreateRequest, callback?: PaymentCallback) => {
|
||||
return PaymentHandler.pay(orderData, PaymentType.BALANCE, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付宝支付
|
||||
*/
|
||||
alipay: (orderData: OrderCreateRequest, callback?: PaymentCallback) => {
|
||||
return PaymentHandler.pay(orderData, PaymentType.ALIPAY, callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建单商品订单数据
|
||||
*/
|
||||
export function buildSingleGoodsOrder(
|
||||
goodsId: number,
|
||||
quantity: number = 1,
|
||||
addressId?: number,
|
||||
options?: {
|
||||
comments?: string;
|
||||
deliveryType?: number;
|
||||
couponId?: any;
|
||||
selfTakeMerchantId?: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
buyerRemarks?: string;
|
||||
}
|
||||
): OrderCreateRequest {
|
||||
return {
|
||||
goodsItems: [
|
||||
{
|
||||
goodsId,
|
||||
quantity,
|
||||
skuId: options?.skuId,
|
||||
specInfo: options?.specInfo
|
||||
}
|
||||
],
|
||||
addressId,
|
||||
payType: PaymentType.WECHAT, // 默认微信支付,会被PaymentHandler覆盖
|
||||
comments: options?.buyerRemarks || options?.comments || '',
|
||||
deliveryType: options?.deliveryType || 0,
|
||||
couponId: options?.couponId,
|
||||
selfTakeMerchantId: options?.selfTakeMerchantId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建购物车订单数据
|
||||
*/
|
||||
export function buildCartOrder(
|
||||
cartItems: Array<{ goodsId: number; quantity: number }>,
|
||||
addressId?: number,
|
||||
options?: {
|
||||
comments?: string;
|
||||
deliveryType?: number;
|
||||
couponId?: number;
|
||||
selfTakeMerchantId?: number;
|
||||
}
|
||||
): OrderCreateRequest {
|
||||
return {
|
||||
goodsItems: cartItems.map(item => ({
|
||||
goodsId: item.goodsId,
|
||||
quantity: item.quantity
|
||||
})),
|
||||
addressId,
|
||||
payType: PaymentType.WECHAT, // 默认微信支付,会被PaymentHandler覆盖
|
||||
comments: options?.comments || '购物车下单',
|
||||
deliveryType: options?.deliveryType || 0,
|
||||
couponId: options?.couponId,
|
||||
selfTakeMerchantId: options?.selfTakeMerchantId
|
||||
};
|
||||
}
|
||||
436
src/utils/request.ts
Normal file
436
src/utils/request.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { BaseUrl, TenantId } from "@/config/app";
|
||||
|
||||
// 请求配置接口
|
||||
interface RequestConfig {
|
||||
url: string;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
data?: any;
|
||||
header?: Record<string, string>;
|
||||
timeout?: number;
|
||||
retry?: number;
|
||||
showLoading?: boolean;
|
||||
showError?: boolean;
|
||||
returnRaw?: boolean; // 是否返回原始响应数据
|
||||
}
|
||||
|
||||
// API响应接口
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message?: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
// 错误类型枚举
|
||||
enum ErrorType {
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
|
||||
BUSINESS_ERROR = 'BUSINESS_ERROR',
|
||||
AUTH_ERROR = 'AUTH_ERROR',
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
|
||||
}
|
||||
|
||||
// 自定义错误类
|
||||
class RequestError extends Error {
|
||||
public type: ErrorType;
|
||||
public code?: number;
|
||||
public data?: any;
|
||||
|
||||
constructor(message: string, type: ErrorType, code?: number, data?: any) {
|
||||
super(message);
|
||||
this.name = 'RequestError';
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
// 请求配置
|
||||
const DEFAULT_CONFIG = {
|
||||
timeout: 10000, // 10秒超时
|
||||
retry: 2, // 重试2次
|
||||
showLoading: false,
|
||||
showError: true
|
||||
};
|
||||
|
||||
let baseUrl = BaseUrl;
|
||||
|
||||
// 开发环境配置
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// baseUrl = 'http://localhost:9200/api'
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
const requestInterceptor = (config: RequestConfig): RequestConfig => {
|
||||
// 添加认证token
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
const tenantId = Taro.getStorageSync('TenantId') || TenantId;
|
||||
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'TenantId': tenantId
|
||||
};
|
||||
|
||||
if (token) {
|
||||
defaultHeaders['Authorization'] = token;
|
||||
}
|
||||
|
||||
config.header = { ...defaultHeaders, ...config.header };
|
||||
|
||||
// 显示加载提示
|
||||
if (config.showLoading) {
|
||||
Taro.showLoading({ title: '加载中...' });
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 响应拦截器
|
||||
const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
|
||||
// 隐藏加载提示
|
||||
if (config.showLoading) {
|
||||
Taro.hideLoading();
|
||||
}
|
||||
|
||||
const { statusCode, data } = response;
|
||||
|
||||
// 调试信息(仅开发环境)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('API Response:', { statusCode, url: config.url, success: statusCode === 200 });
|
||||
}
|
||||
|
||||
// HTTP状态码检查
|
||||
if (statusCode !== 200) {
|
||||
throw new RequestError(
|
||||
`HTTP错误: ${statusCode}`,
|
||||
ErrorType.NETWORK_ERROR,
|
||||
statusCode,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有数据,抛出错误
|
||||
if (data === null || data === undefined) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('API响应数据为空:', { statusCode, url: config.url });
|
||||
}
|
||||
throw new RequestError(
|
||||
'响应数据为空',
|
||||
ErrorType.NETWORK_ERROR,
|
||||
statusCode,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
// 业务状态码检查
|
||||
if (typeof data === 'object' && data !== null && 'code' in data) {
|
||||
const apiResponse = data as ApiResponse<T>;
|
||||
|
||||
// 成功响应
|
||||
if (apiResponse.code === 0) {
|
||||
// 如果配置了返回原始响应,则返回完整响应
|
||||
if (config.returnRaw) {
|
||||
return data as T;
|
||||
}
|
||||
// 否则返回data部分
|
||||
return apiResponse.data as T;
|
||||
}
|
||||
|
||||
// 认证错误
|
||||
if (apiResponse.code === 401 || apiResponse.code === 403) {
|
||||
handleAuthError();
|
||||
throw new RequestError(
|
||||
apiResponse.message || '认证失败',
|
||||
ErrorType.AUTH_ERROR,
|
||||
apiResponse.code,
|
||||
apiResponse.data
|
||||
);
|
||||
}
|
||||
|
||||
// 业务错误
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('API业务错误:', { code: apiResponse.code, message: apiResponse.message });
|
||||
}
|
||||
throw new RequestError(
|
||||
apiResponse.message || '请求失败',
|
||||
ErrorType.BUSINESS_ERROR,
|
||||
apiResponse.code,
|
||||
apiResponse.data
|
||||
);
|
||||
}
|
||||
|
||||
// 如果不是标准的API响应格式,直接返回数据
|
||||
return data as T;
|
||||
};
|
||||
|
||||
// 处理认证错误
|
||||
const handleAuthError = () => {
|
||||
// 清除本地存储的认证信息
|
||||
try {
|
||||
Taro.removeStorageSync('access_token');
|
||||
Taro.removeStorageSync('User');
|
||||
Taro.removeStorageSync('UserId');
|
||||
Taro.removeStorageSync('TenantId');
|
||||
Taro.removeStorageSync('Phone');
|
||||
} catch (error) {
|
||||
console.error('清除认证信息失败:', error);
|
||||
}
|
||||
|
||||
// 显示提示并跳转到登录页
|
||||
Taro.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({ url: '/passport/login' });
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// 错误处理
|
||||
const handleError = (error: RequestError, config: RequestConfig) => {
|
||||
console.error('请求错误:', error);
|
||||
|
||||
if (config.showLoading) {
|
||||
Taro.hideLoading();
|
||||
}
|
||||
|
||||
if (config.showError) {
|
||||
let title = '请求失败';
|
||||
|
||||
switch (error.type) {
|
||||
case ErrorType.NETWORK_ERROR:
|
||||
title = '网络连接失败';
|
||||
break;
|
||||
case ErrorType.TIMEOUT_ERROR:
|
||||
title = '请求超时';
|
||||
break;
|
||||
case ErrorType.BUSINESS_ERROR:
|
||||
title = error.message || '操作失败';
|
||||
break;
|
||||
case ErrorType.AUTH_ERROR:
|
||||
title = '认证失败';
|
||||
break;
|
||||
default:
|
||||
title = '未知错误';
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 重试机制
|
||||
const retryRequest = async <T>(
|
||||
config: RequestConfig,
|
||||
retryCount: number = 0
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await executeRequest<T>(config);
|
||||
} catch (error) {
|
||||
const requestError = error as RequestError;
|
||||
|
||||
// 如果是认证错误或业务错误,不重试
|
||||
if (requestError.type === ErrorType.AUTH_ERROR ||
|
||||
requestError.type === ErrorType.BUSINESS_ERROR) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 如果还有重试次数
|
||||
if (retryCount < (config.retry || DEFAULT_CONFIG.retry)) {
|
||||
console.log(`请求失败,正在重试 ${retryCount + 1}/${config.retry || DEFAULT_CONFIG.retry}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1))); // 递增延迟
|
||||
return retryRequest<T>(config, retryCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 执行请求
|
||||
const executeRequest = <T>(config: RequestConfig): Promise<T> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new RequestError('请求超时', ErrorType.TIMEOUT_ERROR));
|
||||
}, config.timeout || DEFAULT_CONFIG.timeout);
|
||||
|
||||
Taro.request({
|
||||
url: config.url,
|
||||
method: config.method || 'GET',
|
||||
data: config.data || {},
|
||||
header: config.header || {},
|
||||
success: (res) => {
|
||||
clearTimeout(timer);
|
||||
try {
|
||||
const result = responseInterceptor<T>(res, config);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(new RequestError(
|
||||
err.errMsg || '网络请求失败',
|
||||
ErrorType.NETWORK_ERROR,
|
||||
undefined,
|
||||
err
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 主请求函数
|
||||
export async function request<T>(options: RequestConfig): Promise<T> {
|
||||
try {
|
||||
// 请求拦截
|
||||
const config = requestInterceptor({ ...DEFAULT_CONFIG, ...options });
|
||||
|
||||
// 执行请求(带重试)
|
||||
const result = await retryRequest<T>(config);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const requestError = error as RequestError;
|
||||
handleError(requestError, options);
|
||||
throw requestError;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建完整URL
|
||||
const buildUrl = (url: string): string => {
|
||||
if (url.indexOf('http') === -1) {
|
||||
return baseUrl + url;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
// 构建查询参数
|
||||
const buildQueryString = (params: Record<string, any>): string => {
|
||||
const queryString = Object.keys(params)
|
||||
.filter(key => params[key] !== undefined && params[key] !== null)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
return queryString ? `?${queryString}` : '';
|
||||
};
|
||||
|
||||
// GET请求 - 返回完整的ApiResult响应(适配后台生成的代码)
|
||||
export function get<T>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
const fullUrl = buildUrl(url) + (params ? buildQueryString(params) : '');
|
||||
return request<T>({
|
||||
url: fullUrl,
|
||||
method: 'GET',
|
||||
returnRaw: true,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
// POST请求 - 返回完整的ApiResult响应(适配后台生成的代码)
|
||||
export function post<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'POST',
|
||||
data,
|
||||
returnRaw: true,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
// PUT请求 - 返回完整的ApiResult响应(适配后台生成的代码)
|
||||
export function put<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'PUT',
|
||||
data,
|
||||
returnRaw: true,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
// PATCH请求 - 返回完整的ApiResult响应(适配后台生成的代码)
|
||||
export function patch<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'PATCH',
|
||||
data,
|
||||
returnRaw: true,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE请求 - 返回完整的ApiResult响应(适配后台生成的代码)
|
||||
export function del<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'DELETE',
|
||||
data,
|
||||
returnRaw: true,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
// 便捷方法 - 自动提取data字段(用于不需要处理完整ApiResult的场景)
|
||||
export function getData<T>(url: string, params?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
const fullUrl = buildUrl(url) + (params ? buildQueryString(params) : '');
|
||||
return request<T>({
|
||||
url: fullUrl,
|
||||
method: 'GET',
|
||||
returnRaw: false,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
export function postData<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'POST',
|
||||
data,
|
||||
returnRaw: false,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
export function putData<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'PUT',
|
||||
data,
|
||||
returnRaw: false,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
export function delData<T>(url: string, data?: any, config?: Partial<RequestConfig>): Promise<T> {
|
||||
return request<T>({
|
||||
url: buildUrl(url),
|
||||
method: 'DELETE',
|
||||
data,
|
||||
returnRaw: false,
|
||||
...config
|
||||
});
|
||||
}
|
||||
|
||||
// 导出错误类型和错误类,供外部使用
|
||||
export { ErrorType, RequestError };
|
||||
|
||||
// 默认导出
|
||||
export default {
|
||||
request,
|
||||
// 主要方法 - 返回完整ApiResult(适配后台生成代码)
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
patch,
|
||||
del,
|
||||
// 便捷方法 - 自动提取data字段
|
||||
getData,
|
||||
postData,
|
||||
putData,
|
||||
delData,
|
||||
ErrorType,
|
||||
RequestError
|
||||
};
|
||||
20
src/utils/server.ts
Normal file
20
src/utils/server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import {User} from "@/api/system/user/model";
|
||||
|
||||
// 模版套餐ID - 请根据实际情况修改
|
||||
export const TEMPLATE_ID = '5';
|
||||
// 服务接口 - 请根据实际情况修改
|
||||
export const SERVER_API_URL = 'https://server.websoft.top/api';
|
||||
// export const SERVER_API_URL = 'http://127.0.0.1:8000/api';
|
||||
/**
|
||||
* 保存用户信息到本地存储
|
||||
* @param token
|
||||
* @param user
|
||||
*/
|
||||
export function saveStorageByLoginUser(token: string, user: User) {
|
||||
Taro.setStorageSync('TenantId',user.tenantId)
|
||||
Taro.setStorageSync('access_token', token)
|
||||
Taro.setStorageSync('UserId', user.userId)
|
||||
Taro.setStorageSync('Phone', user.phone)
|
||||
Taro.setStorageSync('User', user)
|
||||
}
|
||||
27
src/utils/storeSelection.ts
Normal file
27
src/utils/storeSelection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import type { ShopStore } from '@/api/shop/shopStore/model';
|
||||
|
||||
export const SELECTED_STORE_STORAGE_KEY = 'SelectedStore';
|
||||
|
||||
export function getSelectedStoreFromStorage(): ShopStore | null {
|
||||
try {
|
||||
const raw = Taro.getStorageSync(SELECTED_STORE_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
return (typeof raw === 'string' ? JSON.parse(raw) : raw) as ShopStore;
|
||||
} catch (_e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSelectedStoreToStorage(store: ShopStore | null) {
|
||||
if (!store) {
|
||||
Taro.removeStorageSync(SELECTED_STORE_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
Taro.setStorageSync(SELECTED_STORE_STORAGE_KEY, store);
|
||||
}
|
||||
|
||||
export function getSelectedStoreIdFromStorage(): number | undefined {
|
||||
return getSelectedStoreFromStorage()?.id;
|
||||
}
|
||||
|
||||
166
src/utils/test-invite.ts
Normal file
166
src/utils/test-invite.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 邀请参数解析测试工具
|
||||
*/
|
||||
|
||||
import { parseInviteParams } from './invite'
|
||||
|
||||
/**
|
||||
* 测试不同格式的邀请参数解析
|
||||
*/
|
||||
export function testInviteParamsParsing() {
|
||||
console.log('=== 开始测试邀请参数解析 ===')
|
||||
|
||||
// 测试用例1: uid_格式
|
||||
const testCase1 = {
|
||||
scene: 'uid_33103',
|
||||
path: 'pages/index/index'
|
||||
}
|
||||
console.log('测试用例1 - uid格式:')
|
||||
console.log('输入:', testCase1)
|
||||
const result1 = parseInviteParams(testCase1)
|
||||
console.log('输出:', result1)
|
||||
console.log('预期: { inviter: "33103", source: "qrcode", t: "..." }')
|
||||
console.log('结果:', result1?.inviter === '33103' && result1?.source === 'qrcode' ? '✅ 通过' : '❌ 失败')
|
||||
console.log('')
|
||||
|
||||
// 测试用例2: 传统格式
|
||||
const testCase2 = {
|
||||
scene: 'inviter=12345&source=share&t=1640995200000',
|
||||
path: 'pages/index/index'
|
||||
}
|
||||
console.log('测试用例2 - 传统格式:')
|
||||
console.log('输入:', testCase2)
|
||||
const result2 = parseInviteParams(testCase2)
|
||||
console.log('输出:', result2)
|
||||
console.log('预期: { inviter: "12345", source: "share", t: "1640995200000" }')
|
||||
console.log('结果:', result2?.inviter === '12345' && result2?.source === 'share' ? '✅ 通过' : '❌ 失败')
|
||||
console.log('')
|
||||
|
||||
// 测试用例3: 数字类型的scene
|
||||
const testCase3 = {
|
||||
scene: 1047, // 数字类型
|
||||
path: 'pages/index/index'
|
||||
}
|
||||
console.log('测试用例3 - 数字类型scene:')
|
||||
console.log('输入:', testCase3)
|
||||
const result3 = parseInviteParams(testCase3)
|
||||
console.log('输出:', result3)
|
||||
console.log('预期: null (因为不是uid_格式)')
|
||||
console.log('结果:', result3 === null ? '✅ 通过' : '❌ 失败')
|
||||
console.log('')
|
||||
|
||||
// 测试用例4: 空参数
|
||||
const testCase4 = {}
|
||||
console.log('测试用例4 - 空参数:')
|
||||
console.log('输入:', testCase4)
|
||||
const result4 = parseInviteParams(testCase4)
|
||||
console.log('输出:', result4)
|
||||
console.log('预期: null')
|
||||
console.log('结果:', result4 === null ? '✅ 通过' : '❌ 失败')
|
||||
console.log('')
|
||||
|
||||
// 测试用例5: 无效的uid格式
|
||||
const testCase5 = {
|
||||
scene: 'uid_abc',
|
||||
path: 'pages/index/index'
|
||||
}
|
||||
console.log('测试用例5 - 无效uid格式:')
|
||||
console.log('输入:', testCase5)
|
||||
const result5 = parseInviteParams(testCase5)
|
||||
console.log('输出:', result5)
|
||||
console.log('预期: null (因为abc不是数字)')
|
||||
console.log('结果:', result5 === null ? '✅ 通过' : '❌ 失败')
|
||||
console.log('')
|
||||
|
||||
// 测试用例6: referrer参数
|
||||
const testCase6 = {
|
||||
referrer: '99999',
|
||||
path: 'pages/index/index'
|
||||
}
|
||||
console.log('测试用例6 - referrer参数:')
|
||||
console.log('输入:', testCase6)
|
||||
const result6 = parseInviteParams(testCase6)
|
||||
console.log('输出:', result6)
|
||||
console.log('预期: { inviter: "99999", source: "link" }')
|
||||
console.log('结果:', result6?.inviter === '99999' && result6?.source === 'link' ? '✅ 通过' : '❌ 失败')
|
||||
console.log('')
|
||||
|
||||
console.log('=== 邀请参数解析测试完成 ===')
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟小程序启动场景测试
|
||||
*/
|
||||
export function simulateMiniProgramLaunch() {
|
||||
console.log('=== 模拟小程序启动场景 ===')
|
||||
|
||||
// 模拟通过小程序码启动
|
||||
const qrcodeOptions = {
|
||||
path: 'pages/index/index',
|
||||
scene: 'uid_33103',
|
||||
shareTicket: undefined,
|
||||
referrerInfo: {}
|
||||
}
|
||||
|
||||
console.log('模拟小程序码启动:')
|
||||
console.log('启动参数:', qrcodeOptions)
|
||||
|
||||
const qrcodeResult = parseInviteParams(qrcodeOptions)
|
||||
console.log('解析结果:', qrcodeResult)
|
||||
|
||||
if (qrcodeResult && qrcodeResult.inviter === '33103') {
|
||||
console.log('✅ 小程序码邀请解析成功')
|
||||
return qrcodeResult
|
||||
} else {
|
||||
console.log('❌ 小程序码邀请解析失败')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请参数格式
|
||||
*/
|
||||
export function validateInviteParams(params: any) {
|
||||
console.log('=== 验证邀请参数格式 ===')
|
||||
console.log('参数:', params)
|
||||
|
||||
if (!params) {
|
||||
console.log('❌ 参数为空')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!params.inviter) {
|
||||
console.log('❌ 缺少inviter字段')
|
||||
return false
|
||||
}
|
||||
|
||||
if (isNaN(parseInt(params.inviter))) {
|
||||
console.log('❌ inviter不是有效数字')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!params.source) {
|
||||
console.log('❌ 缺少source字段')
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('✅ 邀请参数格式验证通过')
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行所有测试
|
||||
*/
|
||||
export function runAllTests() {
|
||||
console.log('🚀 开始运行所有邀请参数测试')
|
||||
|
||||
testInviteParamsParsing()
|
||||
|
||||
const simulationResult = simulateMiniProgramLaunch()
|
||||
|
||||
if (simulationResult) {
|
||||
validateInviteParams(simulationResult)
|
||||
}
|
||||
|
||||
console.log('🎉 所有测试完成')
|
||||
}
|
||||
39
src/utils/time.ts
Normal file
39
src/utils/time.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 获取当前时间
|
||||
*/
|
||||
export function formatCurrentDate() {
|
||||
// 创建一个Date对象,表示当前日期和时间
|
||||
const now = new Date();
|
||||
|
||||
// 获取年、月、日,并进行必要的格式化
|
||||
const day = String(now.getDate()).padStart(2, '0'); // 获取日,并确保是两位数
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // 获取月,并确保是两位数,月份是从0开始的,所以要加1
|
||||
const year = String(now.getFullYear()).slice(-2); // 获取年份的最后两位数字
|
||||
|
||||
return `${day}${month}${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时分秒
|
||||
*/
|
||||
export function formatHhmmss(){
|
||||
// 创建一个Date对象,表示当前日期和时间
|
||||
const now = new Date();
|
||||
// 获取当前的小时
|
||||
const hour = String(now.getHours()).padStart(2, '0');
|
||||
// 获取当前的分钟
|
||||
const minute = String(now.getMinutes()).padStart(2, '0');
|
||||
// 获取当前的秒数
|
||||
const second = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${String(Number(hour) - 8).padStart(2, '0')}${minute}${second}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前小时
|
||||
*/
|
||||
export function getCurrentHour() {
|
||||
const now = new Date();
|
||||
// 获取当前的小时
|
||||
const hour = String(now.getHours()).padStart(2, '0');
|
||||
return `${String(Number(hour) - 8).padStart(2, '0')}`;
|
||||
}
|
||||
Reference in New Issue
Block a user