refactor(passport): 移除统一扫码页面中的核销功能
- 移除扫码页面中的核销类型相关代码 - 移除核销成功后的提示弹窗逻辑 - 移除核销相关的标签显示 - 更新页面描述文案,移除核销相关内容 - 移除核销类型的历史记录展示逻辑 - 简化扫码结果处理流程 - 移除大量已删除的工具函数和组件文件 - 恢复开发环境API配置为本地地址 - 移除支付类型枚举和支付处理器类 - 移除订单商品数据标准化工具函数 - 移除商品列表和订单列表组件 - 移除优惠券数据转换和计算相关工具函数 - 移除支付方式API接口和模型定义 - 移除规格选择器组件
This commit is contained in:
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@nutui/nutui-react-taro';
|
||||
import { View } from '@tarojs/components';
|
||||
import { Scan } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
import { useUnifiedQRScan, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
|
||||
export interface UnifiedQRButtonProps {
|
||||
/** 按钮类型 */
|
||||
@@ -26,7 +26,7 @@ export interface UnifiedQRButtonProps {
|
||||
|
||||
/**
|
||||
* 统一扫码按钮组件
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
* 支持登录二维码扫描
|
||||
*/
|
||||
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
type = 'danger',
|
||||
@@ -61,22 +61,6 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
const scanResult = await startScan();
|
||||
if (scanResult) {
|
||||
onSuccess?.(scanResult);
|
||||
|
||||
// 根据扫码类型给出不同的后续提示
|
||||
if (scanResult.type === ScanType.VERIFICATION) {
|
||||
// 核销成功后可以继续扫码
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他水票/礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || '扫码失败');
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,6 @@ import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent
|
||||
} 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 {
|
||||
LOGIN = 'login', // 登录二维码
|
||||
VERIFICATION = 'verification', // 核销二维码
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
type VerificationBusinessType = 'gift' | 'ticket';
|
||||
|
||||
interface TicketVerificationPayload {
|
||||
userTicketId: number;
|
||||
qty?: number;
|
||||
userId?: number;
|
||||
t?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
@@ -50,10 +35,9 @@ export interface UnifiedScanResult {
|
||||
|
||||
/**
|
||||
* 统一扫码Hook
|
||||
* 可以处理登录和核销两种类型的二维码
|
||||
* 用于处理登录二维码
|
||||
*/
|
||||
export function useUnifiedQRScan() {
|
||||
const { isAdmin } = useUser();
|
||||
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<UnifiedScanResult | null>(null);
|
||||
@@ -80,29 +64,12 @@ export function useUnifiedQRScan() {
|
||||
*/
|
||||
const detectScanType = useCallback((scanResult: string): ScanType => {
|
||||
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);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (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:
|
||||
result = await handleLoginQR(scanResult);
|
||||
break;
|
||||
case ScanType.VERIFICATION:
|
||||
result = await handleVerificationQR(scanResult);
|
||||
break;
|
||||
default:
|
||||
throw new Error('未知的扫码类型');
|
||||
}
|
||||
@@ -351,7 +201,7 @@ export function useUnifiedQRScan() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
|
||||
}, [reset, detectScanType, handleLoginQR]);
|
||||
|
||||
/**
|
||||
* 取消扫码
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {SERVER_API_URL} from "@/utils/server";
|
||||
import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import navTo from "@/utils/common";
|
||||
@@ -177,7 +178,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: SERVER_API_URL + '/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
|
||||
@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro'
|
||||
import { Button, Radio } from '@nutui/nutui-react-taro'
|
||||
import { TenantId } from '@/config/app'
|
||||
import { getUserInfo, getWxOpenId } from '@/api/layout'
|
||||
import { saveStorageByLoginUser } from '@/utils/server'
|
||||
import {saveStorageByLoginUser, SERVER_API_URL} from '@/utils/server'
|
||||
import {
|
||||
getStoredInviteParams,
|
||||
parseInviteParams,
|
||||
@@ -171,7 +171,7 @@ const Register = () => {
|
||||
const wxLoginCode = await getWeappLoginCode()
|
||||
|
||||
const res = (await Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: SERVER_API_URL + '/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: phoneCode,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useU
|
||||
|
||||
/**
|
||||
* 统一扫码页面
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
* 支持登录二维码扫描
|
||||
*/
|
||||
const UnifiedQRPage: React.FC = () => {
|
||||
const [scanHistory, setScanHistory] = useState<any[]>([]);
|
||||
@@ -36,22 +36,6 @@ const UnifiedQRPage: React.FC = () => {
|
||||
success: true
|
||||
};
|
||||
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) {
|
||||
case ScanType.LOGIN:
|
||||
return <Tag type="success">登录</Tag>;
|
||||
case ScanType.VERIFICATION:
|
||||
return <Tag type="warning">核销</Tag>;
|
||||
default:
|
||||
return <Tag type="default">未知</Tag>;
|
||||
}
|
||||
@@ -119,7 +101,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
<View className="flex-1">
|
||||
<Text className="text-lg font-bold">统一扫码</Text>
|
||||
<Text className="text-sm text-gray-600 block">
|
||||
支持登录和核销功能
|
||||
支持扫码登录确认
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -135,7 +117,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
智能扫码
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
自动识别登录和核销二维码
|
||||
自动识别登录二维码
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -178,8 +160,7 @@ const UnifiedQRPage: React.FC = () => {
|
||||
处理中...
|
||||
</Text>
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{scanType === ScanType.LOGIN ? '正在确认登录' :
|
||||
scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'}
|
||||
{scanType === ScanType.LOGIN ? '正在确认登录' : '正在处理'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
@@ -190,33 +171,6 @@ const UnifiedQRPage: React.FC = () => {
|
||||
<Text className="text-lg font-medium text-green-600 mb-2 block">
|
||||
{result.message}
|
||||
</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">
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -295,16 +249,6 @@ const UnifiedQRPage: React.FC = () => {
|
||||
<Text className="text-sm text-gray-800">
|
||||
{record.success ? record.message : record.error}
|
||||
</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>
|
||||
@@ -325,11 +269,8 @@ const UnifiedQRPage: React.FC = () => {
|
||||
<Text className="text-xs text-blue-700 block mb-1">
|
||||
• 登录二维码:自动确认网页端登录
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block mb-1">
|
||||
• 核销二维码:核销用户水票/礼品卡
|
||||
</Text>
|
||||
<Text className="text-xs text-blue-700 block">
|
||||
• 系统会自动识别二维码类型并执行相应操作
|
||||
• 系统会自动识别二维码并执行操作
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {User} from "@/api/system/user/model";
|
||||
export const TEMPLATE_ID = '10579';
|
||||
// 服务接口 - 请根据实际情况修改
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user