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

@@ -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 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

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