feat(store): 添加门店管理功能和订单配送功能
- 在app.config.ts中添加门店相关路由配置 - 在config/app.ts中添加租户名称常量 - 在Header.tsx中实现门店选择功能,包括定位、距离计算和门店切换 - 更新ShopOrder模型,添加门店ID、门店名称、配送员ID和仓库ID字段 - 新增ShopStore相关API和服务,支持门店的增删改查 - 新增ShopStoreRider相关API和服务,支持配送员管理 - 新增ShopStoreUser相关API和服务,支持店员管理 - 新增ShopWarehouse相关API和服务,支持仓库管理 - 添加配送订单页面,支持订单状态管理和送达确认功能 - 优化经销商页面的样式布局
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { createOrder, WxPayResult } from '@/api/shop/shopOrder';
|
||||
import { OrderCreateRequest } from '@/api/shop/shopOrder/model';
|
||||
import { getSelectedStoreFromStorage, getSelectedStoreIdFromStorage } from '@/utils/storeSelection';
|
||||
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
|
||||
import type { ShopWarehouse } from '@/api/shop/shopWarehouse/model';
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 支付类型枚举
|
||||
@@ -24,6 +28,9 @@ export interface PaymentCallback {
|
||||
* 统一支付处理类
|
||||
*/
|
||||
export class PaymentHandler {
|
||||
// 简单缓存,避免频繁请求(小程序单次运行生命周期内有效)
|
||||
private static storeRidersCache = new Map<number, ShopStoreRider[]>();
|
||||
private static warehousesCache: ShopWarehouse[] | null = null;
|
||||
|
||||
/**
|
||||
* 执行支付
|
||||
@@ -39,6 +46,36 @@ export class PaymentHandler {
|
||||
Taro.showLoading({ title: '支付中...' });
|
||||
|
||||
try {
|
||||
// 若调用方未指定门店,则自动注入“已选门店”,用于订单门店归属/统计。
|
||||
if (orderData.storeId === undefined || orderData.storeId === null) {
|
||||
const storeId = getSelectedStoreIdFromStorage();
|
||||
if (storeId) {
|
||||
orderData.storeId = storeId;
|
||||
}
|
||||
}
|
||||
if (!orderData.storeName) {
|
||||
const store = getSelectedStoreFromStorage();
|
||||
if (store?.name) {
|
||||
orderData.storeName = store.name;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动派单:按门店骑手优先级(dispatchPriority)选择 riderId(不覆盖手动指定)
|
||||
if ((orderData.riderId === undefined || orderData.riderId === null) && orderData.storeId) {
|
||||
const riderUserId = await this.pickRiderUserIdForStore(orderData.storeId);
|
||||
if (riderUserId) {
|
||||
orderData.riderId = riderUserId;
|
||||
}
|
||||
}
|
||||
|
||||
// 仓库选择:若未指定 warehouseId,则按“离门店最近”兜底选择一个(不覆盖手动指定)
|
||||
if ((orderData.warehouseId === undefined || orderData.warehouseId === null) && orderData.storeId) {
|
||||
const warehouseId = await this.pickWarehouseIdForStore(orderData.storeId);
|
||||
if (warehouseId) {
|
||||
orderData.warehouseId = warehouseId;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置支付类型
|
||||
orderData.payType = paymentType;
|
||||
|
||||
@@ -119,6 +156,127 @@ export class PaymentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static parseLngLat(raw: string | undefined): { lng: number; lat: number } | null {
|
||||
const text = (raw || '').trim();
|
||||
if (!text) return null;
|
||||
const parts = text.split(/[,\s]+/).filter(Boolean);
|
||||
if (parts.length < 2) return null;
|
||||
const a = parseFloat(parts[0]);
|
||||
const b = parseFloat(parts[1]);
|
||||
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90;
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180;
|
||||
if (looksLikeLngLat) return { lng: a, lat: b };
|
||||
if (looksLikeLatLng) return { lng: b, lat: a };
|
||||
return null;
|
||||
}
|
||||
|
||||
private static distanceMeters(a: { lng: number; lat: number }, b: { lng: number; lat: number }) {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180;
|
||||
const R = 6371000;
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLng = toRad(b.lng - a.lng);
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
const sin1 = Math.sin(dLat / 2);
|
||||
const sin2 = Math.sin(dLng / 2);
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2;
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||
}
|
||||
|
||||
private static async getRidersForStore(storeId: number): Promise<ShopStoreRider[]> {
|
||||
const cached = this.storeRidersCache.get(storeId);
|
||||
if (cached) return cached;
|
||||
|
||||
// 后端字段可能叫 dealerId 或 storeId,这里都带上,服务端忽略未知字段即可。
|
||||
// 这里做一次路径兼容(camel vs kebab),避免接口路径不一致导致整单失败。
|
||||
const list = await this.listByCompatEndpoint<ShopStoreRider>(
|
||||
['/shop/shopStoreRider', '/shop/shop-store-rider'],
|
||||
{
|
||||
dealerId: storeId,
|
||||
storeId: storeId,
|
||||
status: 1
|
||||
}
|
||||
);
|
||||
|
||||
const usable = (list || []).filter(r => r?.isDelete !== 1 && (r.status === undefined || r.status === 1));
|
||||
this.storeRidersCache.set(storeId, usable);
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static async pickRiderUserIdForStore(storeId: number): Promise<number | undefined> {
|
||||
const riders = await this.getRidersForStore(storeId);
|
||||
if (!riders.length) return undefined;
|
||||
|
||||
// 优先:启用 + 在线 + 自动派单,再按 dispatchPriority 由高到低
|
||||
const score = (r: ShopStoreRider) => {
|
||||
const enabled = (r.status === undefined || r.status === 1) ? 1 : 0;
|
||||
const online = r.workStatus === 1 ? 1 : 0;
|
||||
const auto = r.autoDispatchEnabled === 1 ? 1 : 0;
|
||||
const p = typeof r.dispatchPriority === 'number' ? r.dispatchPriority : 0;
|
||||
return enabled * 1000 + online * 100 + auto * 10 + p;
|
||||
};
|
||||
|
||||
const sorted = [...riders].sort((a, b) => score(b) - score(a));
|
||||
return sorted[0]?.userId;
|
||||
}
|
||||
|
||||
private static async getWarehouses(): Promise<ShopWarehouse[]> {
|
||||
if (this.warehousesCache) return this.warehousesCache;
|
||||
const list = await this.listByCompatEndpoint<ShopWarehouse>(
|
||||
['/shop/shopWarehouse', '/shop/shop-warehouse'],
|
||||
{}
|
||||
);
|
||||
const usable = (list || []).filter(w => w?.isDelete !== 1 && (w.status === undefined || w.status === 1));
|
||||
this.warehousesCache = usable;
|
||||
return usable;
|
||||
}
|
||||
|
||||
private static async pickWarehouseIdForStore(storeId: number): Promise<number | undefined> {
|
||||
const store = getSelectedStoreFromStorage();
|
||||
if (!store?.id || store.id !== storeId) return undefined;
|
||||
|
||||
// 一门店一默认仓库:优先使用门店自带的 warehouseId
|
||||
if (store.warehouseId) return store.warehouseId;
|
||||
|
||||
const storeCoords = this.parseLngLat(store.lngAndLat || store.location);
|
||||
if (!storeCoords) return undefined;
|
||||
|
||||
const warehouses = await this.getWarehouses();
|
||||
if (!warehouses.length) return undefined;
|
||||
|
||||
// 优先选择“门店仓”,否则选最近的任意仓库
|
||||
const candidates = warehouses.filter(w => w.type?.includes('门店') || w.type?.includes('门店仓'));
|
||||
const list = candidates.length ? candidates : warehouses;
|
||||
|
||||
const withDistance = list
|
||||
.map(w => {
|
||||
const coords = this.parseLngLat(w.lngAndLat);
|
||||
if (!coords) return { w, d: Number.POSITIVE_INFINITY };
|
||||
return { w, d: this.distanceMeters(storeCoords, coords) };
|
||||
})
|
||||
.sort((a, b) => a.d - b.d);
|
||||
|
||||
return withDistance[0]?.w?.id;
|
||||
}
|
||||
|
||||
private static async listByCompatEndpoint<T>(
|
||||
urls: string[],
|
||||
params: Record<string, any>
|
||||
): Promise<T[]> {
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res: any = await (request as any).get(url, params, { showError: false });
|
||||
if (res?.code === 0 && Array.isArray(res?.data)) {
|
||||
return res.data as T[];
|
||||
}
|
||||
} catch (_e) {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信支付
|
||||
*/
|
||||
|
||||
27
src/utils/storeSelection.ts
Normal file
27
src/utils/storeSelection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import type { ShopStore } from '@/api/shop/shopStore/model';
|
||||
|
||||
export const SELECTED_STORE_STORAGE_KEY = 'SelectedStore';
|
||||
|
||||
export function getSelectedStoreFromStorage(): ShopStore | null {
|
||||
try {
|
||||
const raw = Taro.getStorageSync(SELECTED_STORE_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
return (typeof raw === 'string' ? JSON.parse(raw) : raw) as ShopStore;
|
||||
} catch (_e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSelectedStoreToStorage(store: ShopStore | null) {
|
||||
if (!store) {
|
||||
Taro.removeStorageSync(SELECTED_STORE_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
Taro.setStorageSync(SELECTED_STORE_STORAGE_KEY, store);
|
||||
}
|
||||
|
||||
export function getSelectedStoreIdFromStorage(): number | undefined {
|
||||
return getSelectedStoreFromStorage()?.id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user