feat(registration): 优化经销商注册流程并增加地址定位功能

- 修改导航栏标题从“邀请注册”为“注册成为会员”
- 修复重复提交问题并移除不必要的submitting状态
- 增加昵称和头像的必填验证提示
- 添加用户角色缺失时的默认角色写入机制
- 集成地图选点功能,支持经纬度获取和地址解析
- 实现微信地址导入功能,自动填充基本信息
- 增加定位权限检查和错误处理机制
- 添加.gitignore规则忽略备份文件夹src__bak
- 移除已废弃的银行卡和客户管理页面代码
- 优化表单验证规则和错误提示信息
- 实现经销商注册成功后自动跳转到“我的”页面
- 添加用户信息缓存刷新机制确保角色信息同步
```
This commit is contained in:
2026-03-01 12:35:41 +08:00
parent 945351be91
commit eee4644d06
296 changed files with 28845 additions and 6664 deletions

30
src/utils/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
import Taro from '@tarojs/taro'
import { goTo } from '@/utils/navigation'
import { getStoredInviteParams } from '@/utils/invite'
export function isLoggedIn(): boolean {
const token = Taro.getStorageSync('access_token')
const userId = Taro.getStorageSync('UserId')
return !!token && !!userId
}
export function goToRegister(options?: { redirect?: string }) {
const inviteParams = getStoredInviteParams()
goTo('/passport/register', {
redirect: options?.redirect,
inviter: inviteParams?.inviter,
source: inviteParams?.source,
t: inviteParams?.t,
})
}
/**
* Ensure user is logged in; if not, navigate to register/login page.
* @returns true when already logged in; false when redirected to register
*/
export function ensureLoggedIn(redirect?: string): boolean {
if (isLoggedIn()) return true
goToRegister({ redirect })
return false
}

View File

@@ -1,24 +1,22 @@
import Taro from '@tarojs/taro'
import { goTo } from './navigation'
import { goToRegister, isLoggedIn } from '@/utils/auth'
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;
if (!isLoggedIn()) {
const redirect = url.startsWith('/') ? url : `/${url}`
goToRegister({ redirect })
return false
}
}
Taro.navigateTo({
url: url
})
// 使用新的导航工具,自动处理路径格式化
goTo(url)
}
// 转base64
export function fileToBase64(filePath: string) {
export function fileToBase64(filePath:string) {
return new Promise((resolve) => {
let fileManager = Taro.getFileSystemManager();
fileManager.readFile({
@@ -35,7 +33,7 @@ export function fileToBase64(filePath: string) {
* 转义微信富文本图片样式
* @param htmlText
*/
export function wxParse(htmlText: string) {
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;"');
@@ -78,18 +76,6 @@ export function shareGoodsLink(goodsId: string | number) {
copyText(shareUrl);
}
/**
* 显示分享引导提示
*/
export function showShareGuide() {
Taro.showModal({
title: '分享提示',
content: '请点击右上角的"..."按钮,然后选择"转发"来分享给好友,或选择"分享到朋友圈"',
showCancel: false,
confirmText: '知道了'
});
}
/**
* 截取字符串,确保不超过指定的汉字长度
* @param text 原始文本
@@ -142,30 +128,3 @@ export function generateOrderTitle(goodsNames: string[], maxLength: number = 30)
return truncateText(title, maxLength);
}
/**
* 下划线转驼峰命名
*/
export function toCamelCase(str: string): string {
return str.replace(/_([a-z])/g, function (_, letter) {
return letter.toUpperCase();
});
}
/**
* 下划线转大驼峰命名
*/
export function toCamelCaseUpper(str: string): string {
return toCamelCase(str).replace(/^[a-z]/, function (letter) {
return letter.toUpperCase();
});
}
/**
* 转为短下划线
*/
export function toShortUnderline(str: string): string {
return str.replace(/[A-Z]/g, function (letter) {
return '_' + letter.toLowerCase();
}).replace(/^_/, '');
}

View File

@@ -25,7 +25,7 @@ export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps =>
const getTheme = (type?: number): CouponCardProps['theme'] => {
switch (type) {
case 10: return 'red' // 满减券-红色
case 20: return 'orange' // 折扣券-橙色
case 20: return 'orange' // 折扣券-橙色
case 30: return 'green' // 免费券-绿色
default: return 'blue'
}
@@ -53,7 +53,7 @@ export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps =>
* 计算优惠券折扣金额
*/
export const calculateCouponDiscount = (
coupon: CouponCardProps,
coupon: CouponCardProps,
totalAmount: number
): number => {
// 检查是否满足使用条件
@@ -82,7 +82,7 @@ export const calculateCouponDiscount = (
* 检查优惠券是否可用
*/
export const isCouponUsable = (
coupon: CouponCardProps,
coupon: CouponCardProps,
totalAmount: number
): boolean => {
// 状态检查
@@ -102,13 +102,13 @@ export const isCouponUsable = (
* 获取优惠券不可用原因
*/
export const getCouponUnusableReason = (
coupon: CouponCardProps,
coupon: CouponCardProps,
totalAmount: number
): string => {
if (coupon.status === 1) {
return '优惠券已使用'
}
if (coupon.status === 2) {
return '优惠券已过期'
}
@@ -151,30 +151,30 @@ export const formatCouponTitle = (coupon: CouponCardProps): string => {
* 按照优惠金额从大到小排序,同等优惠金额按过期时间排序
*/
export const sortCoupons = (
coupons: CouponCardProps[],
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
})
}
@@ -183,7 +183,7 @@ export const sortCoupons = (
* 过滤可用优惠券
*/
export const filterUsableCoupons = (
coupons: CouponCardProps[],
coupons: CouponCardProps[],
totalAmount: number
): CouponCardProps[] => {
return coupons.filter(coupon => isCouponUsable(coupon, totalAmount))
@@ -193,7 +193,7 @@ export const filterUsableCoupons = (
* 过滤不可用优惠券
*/
export const filterUnusableCoupons = (
coupons: CouponCardProps[],
coupons: CouponCardProps[],
totalAmount: number
): CouponCardProps[] => {
return coupons.filter(coupon => !isCouponUsable(coupon, totalAmount))

View File

@@ -1,103 +0,0 @@
/**
* 客户状态管理工具函数
*/
// 客户状态类型定义
export type CustomerStatus = 'all' | 'pending' | 'signed' | 'cancelled';
// 客户状态配置
export const CUSTOMER_STATUS_CONFIG = {
all: {
label: '全部',
color: '#666666',
tagType: 'default' as const
},
pending: {
label: '跟进中',
color: '#ff8800',
tagType: 'warning' as const
},
signed: {
label: '已签约',
color: '#52c41a',
tagType: 'success' as const
},
cancelled: {
label: '已取消',
color: '#999999',
tagType: 'default' as const
}
};
/**
* 获取状态文本
*/
export const getStatusText = (status: CustomerStatus): string => {
return CUSTOMER_STATUS_CONFIG[status]?.label || '';
};
/**
* 获取状态标签类型
*/
export const getStatusTagType = (status: CustomerStatus) => {
return CUSTOMER_STATUS_CONFIG[status]?.tagType || 'default';
};
/**
* 获取状态颜色
*/
export const getStatusColor = (status: CustomerStatus): string => {
return CUSTOMER_STATUS_CONFIG[status]?.color || '#666666';
};
/**
* 获取所有状态选项
*/
export const getStatusOptions = () => {
return Object.entries(CUSTOMER_STATUS_CONFIG).map(([value, config]) => ({
value: value as CustomerStatus,
label: config.label
}));
};
/**
* 将数字状态映射为字符串状态
*/
export const mapApplyStatusToCustomerStatus = (applyStatus: number): CustomerStatus => {
switch (applyStatus) {
case 10:
return 'pending'; // 跟进中
case 20:
return 'signed'; // 已签约
case 30:
return 'cancelled'; // 已取消
default:
return 'pending'; // 默认为跟进中
}
};
/**
* 将字符串状态映射为数字状态
*/
export const mapCustomerStatusToApplyStatus = (customerStatus: CustomerStatus): number | undefined => {
switch (customerStatus) {
case 'pending':
return 10; // 跟进中
case 'signed':
return 20; // 已签约
case 'cancelled':
return 30; // 已取消
case 'all':
return undefined; // 全部,不筛选
default:
return undefined;
}
};
/**
* 临时函数:生成随机状态(实际项目中应该删除,从数据库获取真实状态)
*/
export const getRandomStatus = (): CustomerStatus => {
const statuses: CustomerStatus[] = ['pending', 'signed', 'cancelled'];
return statuses[Math.floor(Math.random() * statuses.length)];
};

View File

@@ -1,126 +0,0 @@
/**
* 日期格式化工具函数
* 用于处理各种日期格式转换
*/
/**
* 格式化日期为数据库格式 YYYY-MM-DD HH:mm:ss
* @param dateStr 输入的日期字符串,支持多种格式
* @returns 数据库格式的日期字符串
*/
export const formatDateForDatabase = (dateStr: string): string => {
if (!dateStr) return ''
let parts: string[] = []
// 处理不同的日期格式
if (dateStr.includes('/')) {
// 处理 YYYY/MM/DD 或 YYYY/M/D 格式
parts = dateStr.split('/')
} else if (dateStr.includes('-')) {
// 处理 YYYY-MM-DD 或 YYYY-M-D 格式
parts = dateStr.split('-')
} else {
return dateStr
}
if (parts.length !== 3) return dateStr
const year = parts[0]
const month = parts[1].padStart(2, '0')
const day = parts[2].padStart(2, '0')
return `${year}-${month}-${day} 00:00:00`
}
/**
* 从数据库格式提取日期部分用于Calendar组件显示
* @param dateTimeStr 数据库格式的日期时间字符串
* @returns Calendar组件需要的格式 (YYYY-M-D)
*/
export const extractDateForCalendar = (dateTimeStr: string): string => {
if (!dateTimeStr) return ''
// 处理不同的输入格式
let dateStr = ''
if (dateTimeStr.includes(' ')) {
// 从 "YYYY-MM-DD HH:mm:ss" 格式中提取日期部分
dateStr = dateTimeStr.split(' ')[0]
} else {
dateStr = dateTimeStr
}
// 转换为Calendar组件需要的格式 (YYYY-M-D)
if (dateStr.includes('-')) {
const parts = dateStr.split('-')
if (parts.length === 3) {
const year = parts[0]
const month = parseInt(parts[1]).toString() // 去掉前导0
const day = parseInt(parts[2]).toString() // 去掉前导0
return `${year}-${month}-${day}`
}
}
return dateStr
}
/**
* 格式化日期为用户友好的显示格式 YYYY-MM-DD
* @param dateStr 输入的日期字符串
* @returns 用户友好的日期格式
*/
export const formatDateForDisplay = (dateStr: string): string => {
if (!dateStr) return ''
// 如果是数据库格式,先提取日期部分
let dateOnly = dateStr
if (dateStr.includes(' ')) {
dateOnly = dateStr.split(' ')[0]
}
// 如果已经是标准格式,直接返回
if (/^\d{4}-\d{2}-\d{2}$/.test(dateOnly)) {
return dateOnly
}
// 处理其他格式
let parts: string[] = []
if (dateOnly.includes('/')) {
parts = dateOnly.split('/')
} else if (dateOnly.includes('-')) {
parts = dateOnly.split('-')
} else {
return dateStr
}
if (parts.length !== 3) return dateStr
const year = parts[0]
const month = parts[1].padStart(2, '0')
const day = parts[2].padStart(2, '0')
return `${year}-${month}-${day}`
}
/**
* 获取当前日期的字符串格式
* @param format 'database' | 'display' | 'calendar'
* @returns 格式化的当前日期
*/
export const getCurrentDate = (format: 'database' | 'display' | 'calendar' = 'display'): string => {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
switch (format) {
case 'database':
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} 00:00:00`
case 'display':
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
case 'calendar':
return `${year}-${month}-${day}`
default:
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
}
}

143
src/utils/geofence.ts Normal file
View File

@@ -0,0 +1,143 @@
export type LngLat = { lng: number; lat: number };
function normalizeLngLat(a: number, b: number): LngLat | null {
if (!Number.isFinite(a) || !Number.isFinite(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;
}
function parsePointLike(v: any): LngLat | null {
if (!v) return null;
if (Array.isArray(v) && v.length >= 2) {
return normalizeLngLat(Number(v[0]), Number(v[1]));
}
if (typeof v === 'object') {
// Try common field names from map libs / backends.
const a = v.lng ?? v.lon ?? v.longitude ?? v.x;
const b = v.lat ?? v.latitude ?? v.y;
if (a !== undefined && b !== undefined) {
return normalizeLngLat(Number(a), Number(b));
}
}
if (typeof v === 'string') {
return parseLngLatFromText(v);
}
return null;
}
export function parseLngLatFromText(raw: string | undefined): LngLat | null {
const text = (raw || '').trim();
if (!text) return null;
const parts = text.split(/[,\s]+/).filter(Boolean);
if (parts.length < 2) return null;
const a = parts[0];
const b = parts[1];
if (!a || !b) return null;
return normalizeLngLat(parseFloat(a), parseFloat(b));
}
/**
* Parse fence "points" into a polygon point list.
*
* Supported formats (best-effort):
* - JSON: [[lng,lat], ...] or [{lng,lat}, ...]
* - Delimited: "lng,lat;lng,lat;..." or "lng,lat|lng,lat|..."
* - Flat numbers: "lng,lat,lng,lat,..." (even count)
*/
export function parseFencePoints(pointsRaw: string | undefined): LngLat[] {
const text = (pointsRaw || '').trim();
if (!text) return [];
// 1) JSON-like.
if (text.startsWith('[') || text.startsWith('{')) {
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) {
const list = parsed.map(parsePointLike).filter(Boolean) as LngLat[];
if (list.length) return list;
// Some systems wrap coordinates like [[[lng,lat],...]].
if (Array.isArray(parsed[0])) {
const inner = (parsed[0] as any[]).map(parsePointLike).filter(Boolean) as LngLat[];
if (inner.length) return inner;
}
}
} catch (_e) {
// fall through
}
}
// 2) Split by common point separators.
const segments = text.split(/[;|\n\r]+/).map(s => s.trim()).filter(Boolean);
if (segments.length > 1) {
const list = segments.map(seg => {
const nums = seg.match(/-?\d+(\.\d+)?/g) || [];
if (nums.length < 2) return null;
const a = nums[0];
const b = nums[1];
if (!a || !b) return null;
return normalizeLngLat(parseFloat(a), parseFloat(b));
}).filter(Boolean) as LngLat[];
if (list.length) return list;
}
// 3) Fallback: grab all numbers and pair them.
const nums = text.match(/-?\d+(\.\d+)?/g) || [];
if (nums.length >= 6 && nums.length % 2 === 0) {
const list: LngLat[] = [];
for (let i = 0; i < nums.length; i += 2) {
const a = nums[i];
const b = nums[i + 1];
if (!a || !b) continue;
const p = normalizeLngLat(parseFloat(a), parseFloat(b));
if (p) list.push(p);
}
if (list.length) return list;
}
return [];
}
function pointOnSegment(p: LngLat, a: LngLat, b: LngLat, eps = 1e-9): boolean {
// Cross product must be near 0 and dot product within [0, |ab|^2]
const cross = (b.lat - a.lat) * (p.lng - a.lng) - (b.lng - a.lng) * (p.lat - a.lat);
if (Math.abs(cross) > eps) return false;
const dot = (p.lng - a.lng) * (b.lng - a.lng) + (p.lat - a.lat) * (b.lat - a.lat);
if (dot < -eps) return false;
const lenSq = (b.lng - a.lng) ** 2 + (b.lat - a.lat) ** 2;
return dot <= lenSq + eps;
}
// Ray-casting with boundary check; treats points on edges as inside.
export function pointInPolygon(p: LngLat, polygon: LngLat[]): boolean {
if (!polygon || polygon.length < 3) return false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const a = polygon[j];
const b = polygon[i];
if (pointOnSegment(p, a, b)) return true;
}
let inside = false;
const x = p.lng;
const y = p.lat;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].lng;
const yi = polygon[i].lat;
const xj = polygon[j].lng;
const yj = polygon[j].lat;
const intersect = (yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
export function pointInAnyPolygon(p: LngLat, polygons: LngLat[][]): boolean {
for (const poly of polygons) {
if (pointInPolygon(p, poly)) return true;
}
return false;
}

View File

@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro'
import {bindRefereeRelation} from "@/api/invite";
import { bindRefereeRelation } from '@/api/invite'
/**
* 邀请参数接口
@@ -15,108 +15,76 @@ export interface InviteParams {
*/
export function parseInviteParams(options: any): InviteParams | null {
try {
console.log('解析邀请参数:', options)
// 优先从 query.scene 中解析邀请信息
// 优先从 query.scene 参数中解析邀请信息
let sceneStr = null
if (options.query && options.query.scene) {
const sceneStr = typeof options.query.scene === 'string' ? options.query.scene : String(options.query.scene)
console.log('从 query.scene 解析:', sceneStr)
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)
}
// 处理 uid_xxxxx 格式的参数
// 从 scene 参数中解析邀请信息
if (sceneStr) {
// 处理 uid_xxx 格式的邀请码
if (sceneStr.startsWith('uid_')) {
const uid = sceneStr.replace('uid_', '')
if (uid) {
const inviterId = sceneStr.replace('uid_', '')
if (inviterId && !isNaN(parseInt(inviterId))) {
return {
inviter: uid,
inviter: inviterId,
source: 'qrcode',
t: String(Date.now())
t: Date.now().toString()
}
}
}
// 处理 key=value&key=value 格式的参数
if (sceneStr.includes('=')) {
const params: InviteParams = {}
const pairs = sceneStr.split('&')
// 处理传统的 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':
case 'uid':
params.inviter = decodeURIComponent(value)
break
case 'source':
params.source = decodeURIComponent(value)
break
case 't':
params.t = decodeURIComponent(value)
break
}
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
}
})
if (params.inviter) {
return params
}
}
// 从 scene 参数中解析邀请信息(兼容旧版本
if (options.scene) {
const sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene)
console.log('从 scene 解析:', sceneStr)
// 处理 uid_xxxxx 格式的参数
if (sceneStr.startsWith('uid_')) {
const uid = sceneStr.replace('uid_', '')
if (uid) {
return {
inviter: uid,
source: 'qrcode',
t: String(Date.now())
}
// 从 query 参数中解析邀请信息(处理首页分享链接
if (options.query) {
const query = options.query
if (query.inviter) {
return {
inviter: query.inviter,
source: query.source || 'share',
t: query.t
}
}
// 处理 key=value&key=value 格式的参数
if (sceneStr.includes('=')) {
const params: InviteParams = {}
const pairs = sceneStr.split('&')
pairs.forEach((pair: string) => {
const [key, value] = pair.split('=')
if (key && value) {
switch (key) {
case 'inviter':
case 'uid':
params.inviter = decodeURIComponent(value)
break
case 'source':
params.source = decodeURIComponent(value)
break
case 't':
params.t = decodeURIComponent(value)
break
}
}
})
if (params.inviter) {
return params
// 兼容旧版本
if (query.referrer) {
return {
inviter: query.referrer,
source: 'link'
}
}
}
// 从 query 参数中解析邀请信息(兼容旧版本)
if (options.referrer) {
return {
inviter: options.referrer,
source: 'link'
}
}
return null
} catch (error) {
console.error('解析邀请参数失败:', error)
@@ -129,11 +97,12 @@ export function parseInviteParams(options: any): InviteParams | null {
*/
export function saveInviteParams(params: InviteParams) {
try {
Taro.setStorageSync('invite_params', {
const saveData = {
...params,
timestamp: Date.now()
})
console.log('邀请参数已保存:', params)
}
Taro.setStorageSync('invite_params', saveData)
} catch (error) {
console.error('保存邀请参数失败:', error)
}
@@ -145,6 +114,7 @@ export function saveInviteParams(params: InviteParams) {
export function getStoredInviteParams(): InviteParams | null {
try {
const stored = Taro.getStorageSync('invite_params')
if (stored && stored.inviter) {
// 检查是否过期24小时
const now = Date.now()
@@ -174,7 +144,6 @@ export function getStoredInviteParams(): InviteParams | null {
export function clearInviteParams() {
try {
Taro.removeStorageSync('invite_params')
console.log('邀请参数已清除')
} catch (error) {
console.error('清除邀请参数失败:', error)
}
@@ -197,21 +166,55 @@ export async function handleInviteRelation(userId: number): Promise<boolean> {
return false
}
// 建立邀请关系
await bindRefereeRelation({
// 防重复检查:检查是否已经处理过这个邀请关系
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 || 'unknown',
scene: `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}`
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()
console.log(`邀请关系建立成功: ${inviterId} -> ${userId}`)
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
}
}
@@ -232,6 +235,7 @@ export function getSourceDisplayName(source: string): string {
'qrcode': '小程序码',
'link': '分享链接',
'share': '好友分享',
'goods_share': '商品分享',
'poster': '海报分享',
'unknown': '未知来源'
}
@@ -294,3 +298,188 @@ export function trackInviteSource(source: string, inviterId?: number) {
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
}
}

53
src/utils/location.ts Normal file
View File

@@ -0,0 +1,53 @@
import Taro from '@tarojs/taro'
export type LngLat = { lng: string; lat: string }
const isLocationDenied = (e: any) => {
const msg = String(e?.errMsg || e?.message || e || '')
return (
msg.includes('auth deny') ||
msg.includes('authorize') ||
msg.includes('permission') ||
msg.includes('denied') ||
msg.includes('scope.userLocation')
)
}
/**
* Best-effort: tries to fetch current GPS location (gcj02).
* - Returns null on failure.
* - If denied, it prompts user to open settings.
*/
export async function getCurrentLngLat(purpose = '保存地址需要获取您的定位信息,请在设置中开启定位权限后重试。'): Promise<LngLat | null> {
try {
const r = await Taro.getLocation({ type: 'gcj02' })
return { lng: String(r.longitude), lat: String(r.latitude) }
} catch (e: any) {
console.warn('获取定位失败:', e)
if (isLocationDenied(e)) {
try {
const modal = await Taro.showModal({
title: '需要定位权限',
content: purpose,
confirmText: '去设置'
})
if (modal.confirm) {
await Taro.openSetting()
// User may have toggled permission; try once again.
const r = await Taro.getLocation({ type: 'gcj02' })
return { lng: String(r.longitude), lat: String(r.latitude) }
}
} catch (_e) {
// ignore
}
return null
}
try {
await Taro.showToast({ title: '获取定位失败', icon: 'none' })
} catch (_e) {
// ignore
}
return null
}
}

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
}));
};

View File

@@ -1,6 +1,10 @@
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 { ShopStoreWarehouse } from '@/api/shop/shopStoreWarehouse/model';
import request from '@/utils/request';
/**
* 支付类型枚举
@@ -24,6 +28,9 @@ export interface PaymentCallback {
* 统一支付处理类
*/
export class PaymentHandler {
// 简单缓存,避免频繁请求(小程序单次运行生命周期内有效)
private static storeRidersCache = new Map<number, ShopStoreRider[]>();
private static warehousesCache: ShopStoreWarehouse[] | null = null;
/**
* 执行支付
@@ -39,6 +46,36 @@ export class PaymentHandler {
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;
@@ -119,6 +156,126 @@ export class PaymentHandler {
}
}
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<ShopStoreWarehouse[]> {
if (this.warehousesCache) return this.warehousesCache;
const list = await this.listByCompatEndpoint<ShopStoreWarehouse>(
['/shop/shop-store-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 [];
}
/**
* 处理微信支付
*/
@@ -215,6 +372,17 @@ export class PaymentHandler {
const message = error.message;
// 配送范围/电子围栏相关错误(优先于“地址信息有误”的兜底)
if (
message.includes('不在配送范围') ||
message.includes('配送范围') ||
message.includes('电子围栏') ||
message.includes('围栏')
) {
// Toast 文案尽量短(小程序 showToast 标题长度有限),更详细的引导可在业务页面用 Modal 呈现。
return '暂不支持配送';
}
// 余额相关错误
if (message.includes('余额不足') || message.includes('balance')) {
return '账户余额不足,请充值后重试';
@@ -296,6 +464,7 @@ export function buildSingleGoodsOrder(
skuId?: number;
specInfo?: string;
buyerRemarks?: string;
sendStartTime?: string;
}
): OrderCreateRequest {
return {
@@ -310,6 +479,7 @@ export function buildSingleGoodsOrder(
addressId,
payType: PaymentType.WECHAT, // 默认微信支付会被PaymentHandler覆盖
comments: options?.buyerRemarks || options?.comments || '',
sendStartTime: options?.sendStartTime,
deliveryType: options?.deliveryType || 0,
couponId: options?.couponId,
selfTakeMerchantId: options?.selfTakeMerchantId

View File

@@ -53,7 +53,7 @@ const DEFAULT_CONFIG = {
showError: true
};
let baseUrl = Taro.getStorageSync('ApiUrl') || BaseUrl;
let baseUrl = BaseUrl;
// 开发环境配置
if (process.env.NODE_ENV === 'development') {
@@ -94,6 +94,11 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
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(
@@ -105,7 +110,10 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
}
// 如果没有数据,抛出错误
if (!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,
@@ -115,7 +123,7 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
}
// 业务状态码检查
if (typeof data === 'object' && 'code' in data) {
if (typeof data === 'object' && data !== null && 'code' in data) {
const apiResponse = data as ApiResponse<T>;
// 成功响应
@@ -140,6 +148,9 @@ const responseInterceptor = <T>(response: any, config: RequestConfig): T => {
}
// 业务错误
if (process.env.NODE_ENV === 'development') {
console.error('API业务错误:', { code: apiResponse.code, message: apiResponse.message });
}
throw new RequestError(
apiResponse.message || '请求失败',
ErrorType.BUSINESS_ERROR,
@@ -171,7 +182,7 @@ const handleAuthError = () => {
icon: 'none',
duration: 2000
});
//
// setTimeout(() => {
// Taro.reLaunch({ url: '/passport/login' });
// }, 2000);

View File

@@ -16,5 +16,7 @@ export function saveStorageByLoginUser(token: string, user: User) {
Taro.setStorageSync('access_token', token)
Taro.setStorageSync('UserId', user.userId)
Taro.setStorageSync('Phone', user.phone)
Taro.setStorageSync('WxNickName', user.nickname);
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('🎉 所有测试完成')
}