refactor(passport): 移除统一扫码页面中的核销功能

- 移除扫码页面中的核销类型相关代码
- 移除核销成功后的提示弹窗逻辑
- 移除核销相关的标签显示
- 更新页面描述文案,移除核销相关内容
- 移除核销类型的历史记录展示逻辑
- 简化扫码结果处理流程
- 移除大量已删除的工具函数和组件文件
- 恢复开发环境API配置为本地地址
- 移除支付类型枚举和支付处理器类
- 移除订单商品数据标准化工具函数
- 移除商品列表和订单列表组件
- 移除优惠券数据转换和计算相关工具函数
- 移除支付方式API接口和模型定义
- 移除规格选择器组件
This commit is contained in:
2026-03-18 10:57:22 +08:00
parent c3b29d4d76
commit addb53f9c5
19 changed files with 17 additions and 1916 deletions

View File

@@ -2,8 +2,8 @@
export const ENV_CONFIG = { export const ENV_CONFIG = {
// 开发环境 // 开发环境
development: { development: {
// API_BASE_URL: 'http://127.0.0.1:9200/api', API_BASE_URL: 'http://127.0.0.1:9200/api',
API_BASE_URL: 'https://ysb-api.websoft.top/api', // API_BASE_URL: 'https://ysb-api.websoft.top/api',
APP_NAME: '开发环境', APP_NAME: '开发环境',
DEBUG: 'true', DEBUG: 'true',
}, },

View File

@@ -1,145 +0,0 @@
import request from '@/utils/request';
import type {ApiResult, PageResult} from '@/api/index';
import type {Payment, PaymentParam} from './model';
import type {ShopOrder} from '@/api/shop/shopOrder/model';
import {SERVER_API_URL} from "@/utils/server";
/**
* 分页查询支付方式
*/
export async function pagePayment(params: PaymentParam) {
const res = await request.get<ApiResult<PageResult<Payment>>>(
SERVER_API_URL + '/system/payment/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询支付方式列表
*/
export async function listPayment(params?: PaymentParam) {
const res = await request.get<ApiResult<Payment[]>>(
SERVER_API_URL + '/system/payment',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加支付方式
*/
export async function addPayment(data: Payment) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/system/payment',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改支付方式
*/
export async function updatePayment(data: Payment) {
const res = await request.put<ApiResult<unknown>>(
SERVER_API_URL + '/system/payment',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除支付方式
*/
export async function removePayment(id?: number) {
const res = await request.del<ApiResult<unknown>>(
SERVER_API_URL + '/system/payment/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除支付方式
*/
export async function removeBatchPayment(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
SERVER_API_URL + '/system/payment/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询支付方式
*/
export async function getPayment(id: number) {
const res = await request.get<ApiResult<Payment>>(
SERVER_API_URL + '/system/payment/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 生成支付二维码(微信native)
*/
export async function getNativeCode(data: ShopOrder) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/system/wx-native-pay/codeUrl',
data
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 使用余额支付
*/
export async function payByBalance(data: ShopOrder) {
const res = await request.post<ApiResult<unknown>>(
SERVER_API_URL + '/system/payment/balancePay',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 选择支付方式
*/
export async function selectPayment(params?: PaymentParam) {
const res = await request.get<ApiResult<Payment[]>>(
SERVER_API_URL + '/system/payment/select',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,55 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 支付方式
*/
export interface Payment {
// ID
id?: number;
// 支付方式
name?: string;
// 支付类型
type?: number;
// 标识
code?: string;
// 支付图标
image?: string;
// 微信商户号类型 1普通商户2子商户
wechatType?: number;
// 应用ID
appId?: string;
// 商户号
mchId?: string;
// 设置APIv3密钥
apiKey?: string;
// 证书文件 (CERT)
apiclientCert?: string;
// 证书文件 (KEY)
apiclientKey?: string;
// 商户证书序列号
merchantSerialNumber?: string;
// 支付结果通过
notifyUrl?: string;
// 备注
comments?: string;
// 文章排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0启用, 1禁用
status?: boolean;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 支付方式搜索条件
*/
export interface PaymentParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -1,162 +0,0 @@
import {Avatar, Cell, Space, Tabs, Button, TabPane, Swiper} from '@nutui/nutui-react-taro'
import {useEffect, useState, CSSProperties, useRef} from "react";
import {BszxPay} from "@/api/bszx/bszxPay/model";
import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
import {pageShopOrder} from "@/api/shop/shopOrder";
import {ShopOrder} from "@/api/shop/shopOrder/model";
import {copyText} from "@/utils/common";
const InfiniteUlStyle: CSSProperties = {
marginTop: '84px',
height: '82vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
const tabs = [
{
index: 0,
key: '全部',
title: '全部'
},
{
index: 1,
key: '已上架',
title: '已上架'
},
{
index: 2,
key: '已下架',
title: '已下架'
},
{
index: 3,
key: '已售罄',
title: '已售罄'
},
{
index: 4,
key: '警戒库存',
title: '警戒库存'
},
{
index: 5,
key: '回收站',
title: '回收站'
},
]
function GoodsList(props: any) {
const [list, setList] = useState<ShopOrder[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const swiperRef = useRef<React.ElementRef<typeof Swiper> | null>(null)
const [tabIndex, setTabIndex] = useState<string | number>(0)
console.log(props.statusBarHeight, 'ppp')
const reload = async () => {
pageShopOrder({page}).then(res => {
let newList: BszxPay[] | undefined = []
if (res?.list && res?.list.length > 0) {
newList = list?.concat(res.list)
setHasMore(true)
} else {
newList = res?.list
setHasMore(false)
}
setList(newList || []);
})
}
const reloadMore = async () => {
setPage(page + 1)
reload().then();
}
useEffect(() => {
setPage(2)
reload().then()
}, [])
return (
<>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{ top: '84px'}}
value={tabIndex}
onChange={(page) => {
swiperRef.current?.to(page)
setTabIndex(page)
}}
>
{
tabs?.map((item, index) => {
return <TabPane key={index} title={item.title}></TabPane>
})
}
</Tabs>
<div style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
{list?.map(item => {
return (
<Cell style={{padding: '16px'}}>
<Space direction={'vertical'} className={'w-full flex flex-col'}>
<div className={'order-no flex justify-between'}>
<span className={'text-gray-700 font-bold text-sm'}
onClick={() => copyText(`${item.orderNo}`)}>{item.orderNo}</span>
<span className={'text-orange-500'}></span>
</div>
<div
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
<div className={'goods-info'}>
<div className={'flex items-center'}>
<div className={'flex items-center'}>
<Avatar
src='34'
size={'45'}
shape={'square'}
/>
<div className={'ml-2'}>{item.realName}</div>
</div>
<div className={'text-gray-400 text-xs'}>{item.totalNum}</div>
</div>
</div>
<div className={' w-full text-right'}>{item.payPrice}</div>
<Space className={'btn flex justify-end'}>
<Button size={'small'}></Button>
<Button size={'small'}></Button>
</Space>
</Space>
</Cell>
)
})}
</InfiniteLoading>
</div>
</>
)
}
export default GoodsList

View File

@@ -1,181 +0,0 @@
import {Avatar, Cell, Space, Tabs, Button, TabPane} from '@nutui/nutui-react-taro'
import {useEffect, useState, CSSProperties} from "react";
import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
import {pageShopOrder} from "@/api/shop/shopOrder";
import {ShopOrder} from "@/api/shop/shopOrder/model";
import {copyText} from "@/utils/common";
const InfiniteUlStyle: CSSProperties = {
marginTop: '84px',
height: '82vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
const tabs = [
{
index: 0,
key: '全部',
title: '全部'
},
{
index: 1,
key: '待付款',
title: '待付款'
},
{
index: 2,
key: '待发货',
title: '待发货'
},
{
index: 3,
key: '已收货',
title: '已收货'
},
{
index: 4,
key: '已完成',
title: '已完成'
}
]
function OrderList(props: any) {
const [list, setList] = useState<ShopOrder[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [tapIndex, setTapIndex] = useState<string | number>('0')
console.log(props.statusBarHeight, 'ppp')
const getOrderStatusParams = (index: string | number) => {
let params: { payStatus?: number; deliveryStatus?: number; orderStatus?: number } = {};
switch (index) {
case '1': // 待付款
params.payStatus = 0;
break;
case '2': // 待发货
params.payStatus = 1;
params.deliveryStatus = 10;
break;
case '3': // 已收货
params.deliveryStatus = 30;
break;
case '4': // 已完成
params.orderStatus = 1;
break;
case '0': // 全部
default:
break;
}
return params;
};
const reload = async (resetPage = false) => {
const currentPage = resetPage ? 1 : page;
const params = getOrderStatusParams(tapIndex);
pageShopOrder({ page: currentPage, ...params }).then(res => {
let newList: ShopOrder[] | undefined = [];
if (res?.list && res?.list.length > 0) {
newList = resetPage ? res.list : list?.concat(res.list);
setHasMore(true);
} else {
newList = res?.list;
setHasMore(false);
}
setList(newList || []);
setPage(currentPage);
});
};
const reloadMore = async () => {
setPage(page + 1);
reload();
};
useEffect(() => {
reload(true); // 首次加载或tab切换时重置页码
}, [tapIndex]); // 监听tapIndex变化
return (
<>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{ top: '84px'}}
tabStyle={{ backgroundColor: 'transparent'}}
value={tapIndex}
onChange={(paneKey) => {
setTapIndex(paneKey)
}}
>
{
tabs?.map((item, index) => {
return <TabPane key={index} title={item.title}></TabPane>
})
}
</Tabs>
<div style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
{list?.map(item => {
return (
<Cell style={{padding: '16px'}} onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}>
<div className={'order-no flex justify-between'}>
<span className={'text-gray-700 font-bold text-sm'}
onClick={(e) => {e.stopPropagation(); copyText(`${item.orderNo}`)}}>{item.orderNo}</span>
<span className={'text-orange-500'}></span> {/* 这里可以根据item.orderStatus显示不同的状态 */}
</div>
<div
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
<div className={'goods-info'}>
<div className={'flex items-center'}>
<div className={'flex items-center'}>
<Avatar
src='34'
size={'45'}
shape={'square'}
/>
<div className={'ml-2'}>{item.realName}</div>
</div>
<div className={'text-gray-400 text-xs'}>{item.totalNum}</div>
</div>
</div>
<div className={' w-full text-right'}>{item.payPrice}</div>
<Space className={'btn flex justify-end'}>
<Button size={'small'}></Button>
</Space>
</Space>
</Cell>
)
})}
</InfiniteLoading>
</div>
</>
)
}
export default OrderList

View File

@@ -1,176 +0,0 @@
import React, { useState, useEffect } from 'react';
import { View } from '@tarojs/components';
import { Popup, Button, Radio, Image, Space, Cell, CellGroup } from '@nutui/nutui-react-taro';
import { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model';
import { ShopGoodsSpec } from '@/api/shop/shopGoodsSpec/model';
import { ShopGoods } from '@/api/shop/shopGoods/model';
import './index.scss';
interface SpecSelectorProps {
visible?: boolean;
onClose: () => void;
goods: ShopGoods;
specs: ShopGoodsSpec[];
skus: ShopGoodsSku[];
onConfirm: (selectedSku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => void;
action?: 'cart' | 'buy';
}
interface SpecGroup {
specName: string;
values: string[];
}
const SpecSelector: React.FC<SpecSelectorProps> = ({
visible = true,
onClose,
goods,
specs,
skus,
onConfirm,
action = 'cart'
}) => {
const [selectedSpecs, setSelectedSpecs] = useState<Record<string, string>>({});
const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
const [quantity, setQuantity] = useState(1);
const [specGroups, setSpecGroups] = useState<SpecGroup[]>([]);
// 组织规格数据
useEffect(() => {
if (specs.length > 0) {
const groups: Record<string, Set<string>> = {};
specs.forEach(spec => {
if (spec.specName && spec.specValue) {
if (!groups[spec.specName]) {
groups[spec.specName] = new Set();
}
groups[spec.specName].add(spec.specValue);
}
});
const groupsArray = Object.entries(groups).map(([specName, values]) => ({
specName,
values: Array.from(values)
}));
setSpecGroups(groupsArray);
}
}, [specs]);
// 根据选中规格找到对应SKU
useEffect(() => {
if (Object.keys(selectedSpecs).length === specGroups.length && skus.length > 0) {
// 构建规格值字符串,按照规格名称排序确保一致性
const sortedSpecNames = specGroups.map(g => g.specName).sort();
const specValues = sortedSpecNames.map(name => selectedSpecs[name]).join('|');
const sku = skus.find(s => s.sku === specValues);
setSelectedSku(sku || null);
} else {
setSelectedSku(null);
}
}, [selectedSpecs, skus, specGroups]);
// 选择规格值
// const handleSpecSelect = (specName: string, specValue: string) => {
// setSelectedSpecs(prev => ({
// ...prev,
// [specName]: specValue
// }));
// };
// 确认选择
const handleConfirm = () => {
if (!selectedSku) {
return;
}
onConfirm(selectedSku, quantity, action);
};
// 检查规格值是否可选是否有对应的SKU且有库存
// const isSpecValueAvailable = (specName: string, specValue: string) => {
// const testSpecs = { ...selectedSpecs, [specName]: specValue };
//
// // 如果还有其他规格未选择,则认为可选
// if (Object.keys(testSpecs).length < specGroups.length) {
// return true;
// }
//
// // 构建规格值字符串
// const sortedSpecNames = specGroups.map(g => g.specName).sort();
// const specValues = sortedSpecNames.map(name => testSpecs[name]).join('|');
//
// const sku = skus.find(s => s.sku === specValues);
// return sku && sku.stock && sku.stock > 0 && sku.status === 0;
// };
return (
<Popup
visible={visible}
position="bottom"
onClose={onClose}
style={{ height: '60vh' }}
>
<View className="spec-selector">
{/* 商品信息 */}
<View className="spec-selector__header p-4">
<Space className="flex">
<Image
src={selectedSku?.image || goods.image || ''}
width="80"
height="80"
radius="8"
/>
<View className="goods-detail">
<View className="goods-name font-medium text-lg">{goods.name}</View>
<View className="text-red-500">
¥{selectedSku?.price || goods.price}
</View>
<View className="goods-stock text-gray-500">
{selectedSku?.stock || goods.stock}
</View>
</View>
</Space>
</View>
{/* 规格选择 */}
<CellGroup className="spec-selector__content">
<Cell>
<Space direction="vertical">
<View className={'title'}></View>
<Radio.Group defaultValue="1" direction="horizontal">
<Radio shape="button" value="1">
1
</Radio>
<Radio shape="button" value="2">
2
</Radio>
<Radio shape="button" value="3">
3
</Radio>
</Radio.Group>
</Space>
</Cell>
</CellGroup>
{/* 底部按钮 */}
<View className="fixed bottom-7 w-full">
<View className={'px-4'}>
<Button
type="success"
size="large"
className={'w-full'}
block
// disabled={!selectedSku || !selectedSku.stock || selectedSku.stock <= 0}
onClick={handleConfirm}
>
</Button>
</View>
</View>
</View>
</Popup>
);
};
export default SpecSelector;

View File

@@ -3,7 +3,7 @@ import { Button } from '@nutui/nutui-react-taro';
import { View } from '@tarojs/components'; import { View } from '@tarojs/components';
import { Scan } from '@nutui/icons-react-taro'; import { Scan } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan'; import { useUnifiedQRScan, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
export interface UnifiedQRButtonProps { export interface UnifiedQRButtonProps {
/** 按钮类型 */ /** 按钮类型 */
@@ -26,7 +26,7 @@ export interface UnifiedQRButtonProps {
/** /**
* 统一扫码按钮组件 * 统一扫码按钮组件
* 支持登录和核销两种类型的二维码扫描 * 支持登录二维码扫描
*/ */
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
type = 'danger', type = 'danger',
@@ -61,22 +61,6 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
const scanResult = await startScan(); const scanResult = await startScan();
if (scanResult) { if (scanResult) {
onSuccess?.(scanResult); onSuccess?.(scanResult);
// 根据扫码类型给出不同的后续提示
if (scanResult.type === ScanType.VERIFICATION) {
// 核销成功后可以继续扫码
setTimeout(() => {
Taro.showModal({
title: '核销成功',
content: '是否继续扫码核销其他水票/礼品卡?',
success: (res) => {
if (res.confirm) {
handleClick(); // 递归调用继续扫码
}
}
});
}, 2000);
}
} }
} catch (error: any) { } catch (error: any) {
onError?.(error.message || '扫码失败'); onError?.(error.message || '扫码失败');

View File

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

View File

@@ -1,102 +0,0 @@
import {useState, useEffect, useCallback} from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
// Hook 返回值接口
export interface UseDealerUserReturn {
// 经销商用户信息
dealerUser: ShopDealerUser | null
// 加载状态
loading: boolean
// 错误信息
error: string | null
// 刷新数据
refresh: () => Promise<void>
}
/**
* 经销商用户 Hook - 简化版本
* 只查询经销商用户信息和判断是否存在
*/
export const useDealerUser = (): UseDealerUserReturn => {
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
const rawUserId = Taro.getStorageSync('UserId')
const userId = Number(rawUserId)
const hasUser = Number.isFinite(userId) && userId > 0
// If user is logged in, start in loading state to avoid "click too fast" mis-routing.
const [loading, setLoading] = useState<boolean>(hasUser)
const [error, setError] = useState<string | null>(null)
// 获取经销商用户数据
const fetchDealerData = useCallback(async () => {
if (!hasUser) {
setDealerUser(null)
setLoading(false)
return
}
try {
setLoading(true)
setError(null)
// 查询当前用户的经销商信息
const dealer = await getShopDealerUser(userId)
if (dealer) {
setDealerUser(dealer)
} else {
setDealerUser(null)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
setError(errorMessage)
setDealerUser(null)
} finally {
setLoading(false)
}
}, [hasUser, userId])
// 刷新数据
const refresh = useCallback(async () => {
await fetchDealerData()
}, [fetchDealerData])
// 初始化加载数据
useEffect(() => {
if (hasUser) {
fetchDealerData()
} else {
setDealerUser(null)
setError(null)
setLoading(false)
}
}, [fetchDealerData, hasUser])
// 页面返回/切换到前台时刷新一次,避免“注册成为经销商后,页面不更新”
useDidShow(() => {
fetchDealerData()
})
// 允许业务侧通过事件主动触发刷新(例如:注册成功后触发)
useEffect(() => {
const handler = () => {
fetchDealerData()
}
// 事件名尽量语义化;后续可在注册成功处 trigger
Taro.eventCenter.on('dealerUser:changed', handler)
return () => {
Taro.eventCenter.off('dealerUser:changed', handler)
}
}, [fetchDealerData])
return {
dealerUser,
loading,
error,
refresh
}
}

View File

@@ -4,11 +4,6 @@ import {
confirmWechatQRLogin, confirmWechatQRLogin,
parseQRContent parseQRContent
} from '@/api/passport/qr-login'; } from '@/api/passport/qr-login';
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
import { useUser } from "@/hooks/useUser";
import { isValidJSON } from "@/utils/jsonUtils";
import dayjs from 'dayjs';
/** /**
* 统一扫码状态 * 统一扫码状态
@@ -26,19 +21,9 @@ export enum UnifiedScanState {
*/ */
export enum ScanType { export enum ScanType {
LOGIN = 'login', // 登录二维码 LOGIN = 'login', // 登录二维码
VERIFICATION = 'verification', // 核销二维码
UNKNOWN = 'unknown' // 未知类型 UNKNOWN = 'unknown' // 未知类型
} }
type VerificationBusinessType = 'gift' | 'ticket';
interface TicketVerificationPayload {
userTicketId: number;
qty?: number;
userId?: number;
t?: number;
}
/** /**
* 统一扫码结果 * 统一扫码结果
*/ */
@@ -50,10 +35,9 @@ export interface UnifiedScanResult {
/** /**
* 统一扫码Hook * 统一扫码Hook
* 可以处理登录和核销两种类型的二维码 * 用于处理登录二维码
*/ */
export function useUnifiedQRScan() { export function useUnifiedQRScan() {
const { isAdmin } = useUser();
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE); const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [result, setResult] = useState<UnifiedScanResult | null>(null); const [result, setResult] = useState<UnifiedScanResult | null>(null);
@@ -80,29 +64,12 @@ export function useUnifiedQRScan() {
*/ */
const detectScanType = useCallback((scanResult: string): ScanType => { const detectScanType = useCallback((scanResult: string): ScanType => {
try { try {
// 1. 检查是否为JSON格式核销二维码 // 检查是否为登录二维码
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
return ScanType.VERIFICATION;
}
// Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
if (json.userTicketId) {
return ScanType.VERIFICATION;
}
}
// 2. 检查是否为登录二维码
const loginToken = parseQRContent(scanResult); const loginToken = parseQRContent(scanResult);
if (loginToken) { if (loginToken) {
return ScanType.LOGIN; return ScanType.LOGIN;
} }
// 3. 检查是否为纯文本核销码6位数字
if (/^\d{6}$/.test(scanResult.trim())) {
return ScanType.VERIFICATION;
}
return ScanType.UNKNOWN; return ScanType.UNKNOWN;
} catch (error) { } catch (error) {
console.error('检测二维码类型失败:', error); console.error('检测二维码类型失败:', error);
@@ -136,120 +103,6 @@ export function useUnifiedQRScan() {
} }
}, []); }, []);
/**
* 处理核销二维码
*/
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
if (!isAdmin()) {
throw new Error('您没有核销权限');
}
let businessType: VerificationBusinessType = 'gift';
let decryptedOrRaw = '';
// 判断是否为加密的JSON格式
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
businessType = json.businessType;
// 解密获取核销内容
const decryptedData = await decryptQrData({
token: json.token,
encryptedData: json.data
});
if (decryptedData) {
decryptedOrRaw = decryptedData.toString();
} else {
throw new Error('解密失败');
}
} else if (json.userTicketId) {
businessType = 'ticket';
decryptedOrRaw = scanResult.trim();
}
} else {
// 直接使用扫码结果作为核销内容
decryptedOrRaw = scanResult.trim();
}
if (!decryptedOrRaw) {
throw new Error('无法获取有效的核销码');
}
if (businessType === 'ticket') {
if (!isValidJSON(decryptedOrRaw)) {
throw new Error('水票核销信息格式错误');
}
const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
const userTicketId = Number(payload.userTicketId);
const qty = Math.max(1, Number(payload.qty || 1));
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
throw new Error('水票核销信息无效');
}
const ticket = await getGltUserTicket(userTicketId);
if (!ticket) throw new Error('水票不存在');
if (ticket.status === 1) throw new Error('该水票已冻结');
const available = Number(ticket.availableQty || 0);
const used = Number(ticket.usedQty || 0);
if (available < qty) throw new Error('水票可用次数不足');
await updateGltUserTicket({
...ticket,
availableQty: available - qty,
usedQty: used + qty
});
return {
type: ScanType.VERIFICATION,
data: {
businessType: 'ticket',
ticket: {
...ticket,
availableQty: available - qty,
usedQty: used + qty
},
qty
},
message: `核销成功(已使用${qty}次)`
};
}
// 验证礼品卡核销码
const gift = await getShopGiftByCode(decryptedOrRaw);
if (!gift) {
throw new Error('核销码无效');
}
if (gift.status === 1) {
throw new Error('此礼品码已使用');
}
if (gift.status === 2) {
throw new Error('此礼品码已失效');
}
if (gift.userId === 0) {
throw new Error('此礼品码未认领');
}
// 执行核销
await updateShopGift({
...gift,
status: 1,
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
});
return {
type: ScanType.VERIFICATION,
data: { businessType: 'gift', gift },
message: '核销成功'
};
}, [isAdmin]);
/** /**
* 开始扫码 * 开始扫码
*/ */
@@ -306,9 +159,6 @@ export function useUnifiedQRScan() {
case ScanType.LOGIN: case ScanType.LOGIN:
result = await handleLoginQR(scanResult); result = await handleLoginQR(scanResult);
break; break;
case ScanType.VERIFICATION:
result = await handleVerificationQR(scanResult);
break;
default: default:
throw new Error('未知的扫码类型'); throw new Error('未知的扫码类型');
} }
@@ -351,7 +201,7 @@ export function useUnifiedQRScan() {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]); }, [reset, detectScanType, handleLoginQR]);
/** /**
* 取消扫码 * 取消扫码

View File

@@ -2,6 +2,7 @@ import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import {getUserInfo, getWxOpenId} from '@/api/layout'; import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import {SERVER_API_URL} from "@/utils/server";
import {useEffect, useState, forwardRef, useImperativeHandle} from "react"; import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
import {User} from "@/api/system/user/model"; import {User} from "@/api/system/user/model";
import navTo from "@/utils/common"; import navTo from "@/utils/common";
@@ -177,7 +178,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
success: function () { success: function () {
if (code) { if (code) {
Taro.request({ Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone', url: SERVER_API_URL + '/wx-login/loginByMpWxPhone',
method: 'POST', method: 'POST',
data: { data: {
code, code,

View File

@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro'
import { Button, Radio } from '@nutui/nutui-react-taro' import { Button, Radio } from '@nutui/nutui-react-taro'
import { TenantId } from '@/config/app' import { TenantId } from '@/config/app'
import { getUserInfo, getWxOpenId } from '@/api/layout' import { getUserInfo, getWxOpenId } from '@/api/layout'
import { saveStorageByLoginUser } from '@/utils/server' import {saveStorageByLoginUser, SERVER_API_URL} from '@/utils/server'
import { import {
getStoredInviteParams, getStoredInviteParams,
parseInviteParams, parseInviteParams,
@@ -171,7 +171,7 @@ const Register = () => {
const wxLoginCode = await getWeappLoginCode() const wxLoginCode = await getWeappLoginCode()
const res = (await Taro.request({ const res = (await Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone', url: SERVER_API_URL + '/wx-login/loginByMpWxPhone',
method: 'POST', method: 'POST',
data: { data: {
code: phoneCode, code: phoneCode,

View File

@@ -7,7 +7,7 @@ import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useU
/** /**
* 统一扫码页面 * 统一扫码页面
* 支持登录和核销两种类型的二维码扫描 * 支持登录二维码扫描
*/ */
const UnifiedQRPage: React.FC = () => { const UnifiedQRPage: React.FC = () => {
const [scanHistory, setScanHistory] = useState<any[]>([]); const [scanHistory, setScanHistory] = useState<any[]>([]);
@@ -36,22 +36,6 @@ const UnifiedQRPage: React.FC = () => {
success: true success: true
}; };
setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录
// 根据类型给出不同提示
if (result.type === ScanType.VERIFICATION) {
// 核销成功后询问是否继续扫码
setTimeout(() => {
Taro.showModal({
title: '核销成功',
content: '是否继续扫码核销其他水票/礼品卡?',
success: (res) => {
if (res.confirm) {
handleStartScan();
}
}
});
}, 2000);
}
}; };
// 处理扫码失败 // 处理扫码失败
@@ -100,8 +84,6 @@ const UnifiedQRPage: React.FC = () => {
switch (type) { switch (type) {
case ScanType.LOGIN: case ScanType.LOGIN:
return <Tag type="success"></Tag>; return <Tag type="success"></Tag>;
case ScanType.VERIFICATION:
return <Tag type="warning"></Tag>;
default: default:
return <Tag type="default"></Tag>; return <Tag type="default"></Tag>;
} }
@@ -119,7 +101,7 @@ const UnifiedQRPage: React.FC = () => {
<View className="flex-1"> <View className="flex-1">
<Text className="text-lg font-bold"></Text> <Text className="text-lg font-bold"></Text>
<Text className="text-sm text-gray-600 block"> <Text className="text-sm text-gray-600 block">
</Text> </Text>
</View> </View>
</View> </View>
@@ -135,7 +117,7 @@ const UnifiedQRPage: React.FC = () => {
</Text> </Text>
<Text className="text-gray-600 mb-6 block"> <Text className="text-gray-600 mb-6 block">
</Text> </Text>
<Button <Button
type="primary" type="primary"
@@ -178,8 +160,7 @@ const UnifiedQRPage: React.FC = () => {
... ...
</Text> </Text>
<Text className="text-gray-600 mb-6 block"> <Text className="text-gray-600 mb-6 block">
{scanType === ScanType.LOGIN ? '正在确认登录' : {scanType === ScanType.LOGIN ? '正在确认登录' : '正在处理'}
scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'}
</Text> </Text>
</> </>
)} )}
@@ -190,33 +171,6 @@ const UnifiedQRPage: React.FC = () => {
<Text className="text-lg font-medium text-green-600 mb-2 block"> <Text className="text-lg font-medium text-green-600 mb-2 block">
{result.message} {result.message}
</Text> </Text>
{result.type === ScanType.VERIFICATION && result.data && (
<View className="bg-green-50 rounded-lg p-3 mb-4">
{result.data.businessType === 'gift' && result.data.gift && (
<>
<Text className="text-sm text-green-800 block">
{result.data.gift.goodsName || result.data.gift.name || '未知'}
</Text>
<Text className="text-sm text-green-800 block">
¥{result.data.gift.faceValue}
</Text>
</>
)}
{result.data.businessType === 'ticket' && result.data.ticket && (
<>
<Text className="text-sm text-green-800 block">
{result.data.ticket.templateName || '水票'}
</Text>
<Text className="text-sm text-green-800 block">
{result.data.qty || 1}
</Text>
<Text className="text-sm text-green-800 block">
{result.data.ticket.availableQty ?? 0}
</Text>
</>
)}
</View>
)}
<View className="mt-2"> <View className="mt-2">
<Button <Button
type="primary" type="primary"
@@ -295,16 +249,6 @@ const UnifiedQRPage: React.FC = () => {
<Text className="text-sm text-gray-800"> <Text className="text-sm text-gray-800">
{record.success ? record.message : record.error} {record.success ? record.message : record.error}
</Text> </Text>
{record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'gift' && record.data?.gift && (
<Text className="text-xs text-gray-500">
{record.data.gift.goodsName || record.data.gift.name} - ¥{record.data.gift.faceValue}
</Text>
)}
{record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'ticket' && record.data?.ticket && (
<Text className="text-xs text-gray-500">
{record.data.ticket.templateName || '水票'} - {record.data.qty || 1}
</Text>
)}
</View> </View>
</View> </View>
</View> </View>
@@ -325,11 +269,8 @@ const UnifiedQRPage: React.FC = () => {
<Text className="text-xs text-blue-700 block mb-1"> <Text className="text-xs text-blue-700 block mb-1">
</Text> </Text>
<Text className="text-xs text-blue-700 block mb-1">
/
</Text>
<Text className="text-xs text-blue-700 block"> <Text className="text-xs text-blue-700 block">
</Text> </Text>
</View> </View>
</View> </View>

View File

@@ -1,200 +0,0 @@
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))
}

View File

@@ -1,32 +0,0 @@
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,514 +0,0 @@
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';
/**
* 支付类型枚举
*/
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: ShopStoreWarehouse[] | 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<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 [];
}
/**
* 处理微信支付
*/
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('配送范围') ||
message.includes('电子围栏') ||
message.includes('围栏')
) {
// Toast 文案尽量短(小程序 showToast 标题长度有限),更详细的引导可在业务页面用 Modal 呈现。
return '暂不支持配送';
}
// 余额相关错误
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;
sendStartTime?: string;
}
): OrderCreateRequest {
return {
goodsItems: [
{
goodsId,
quantity,
skuId: options?.skuId,
specInfo: options?.specInfo
}
],
addressId,
payType: PaymentType.WECHAT, // 默认微信支付会被PaymentHandler覆盖
comments: options?.buyerRemarks || options?.comments || '',
sendStartTime: options?.sendStartTime,
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
};
}

View File

@@ -5,7 +5,7 @@ import {User} from "@/api/system/user/model";
export const TEMPLATE_ID = '10579'; export const TEMPLATE_ID = '10579';
// 服务接口 - 请根据实际情况修改 // 服务接口 - 请根据实际情况修改
export const SERVER_API_URL = 'https://server.websoft.top/api'; export const SERVER_API_URL = 'https://server.websoft.top/api';
// export const SERVER_API_URL = 'http://127.0.0.1:8000/api'; // export const SERVER_API_URL = 'http://127.0.0.1:9200/api';
/** /**
* 保存用户信息到本地存储 * 保存用户信息到本地存储
* @param token * @param token

View File

@@ -1,27 +0,0 @@
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;
}