feat(add): 新增多页面新增和编辑表单功能

- 添加编辑和新增收货地址页面,支持表单数据加载和提交
- 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置
- 实现文章新增及编辑页面,包含图片上传及多种文章属性配置
- 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理
- 引入统一表单提交成功和失败处理,支持编辑模式数据回显
- 配置统一eslint和editorconfig规则,增强代码规范和编辑体验
- 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
This commit is contained in:
2026-04-11 12:22:29 +08:00
commit 07f5c92f4b
627 changed files with 85725 additions and 0 deletions

132
src/utils/common.ts Normal file
View 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
View 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
View 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
View 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
View 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
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;
}
}

192
src/utils/navigation.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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')}`;
}