Compare commits
3 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c2dd83c5c8 | 3 months ago |
|
|
e05547602e | 3 months ago |
|
|
3282fd7bbd | 3 months ago |
45 changed files with 2425 additions and 1278 deletions
@ -1,49 +1,295 @@ |
|||||||
/** |
/** |
||||||
|
|
||||||
* 客户信息相关 API |
* 客户信息相关 API |
||||||
|
|
||||||
*/ |
*/ |
||||||
import { get, put } from '../services/request' |
|
||||||
|
import Taro from '@tarojs/taro' |
||||||
|
|
||||||
|
import config from '../config' |
||||||
|
|
||||||
|
import { get, post } from '../services/request' |
||||||
|
|
||||||
import { CustomerDto, UpdateCustomerDto } from '../types/login' |
import { CustomerDto, UpdateCustomerDto } from '../types/login' |
||||||
|
|
||||||
|
import { normalizeCustomer, normalizeBalanceList } from '../utils/customerNormalize' |
||||||
|
|
||||||
|
import { pickField, toNumber } from '../utils/apiNormalize' |
||||||
|
|
||||||
|
import { userStore } from '../store' |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface CustomerBalanceResult { |
||||||
|
|
||||||
|
amount: number |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface ResponseResult<T = unknown> { |
||||||
|
|
||||||
|
message: string |
||||||
|
|
||||||
|
code: number |
||||||
|
|
||||||
|
success: boolean |
||||||
|
|
||||||
|
data: T |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
|
||||||
|
* 根据 OpenId 获取客户信息 |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
export const getCustomerByOpenId = async (openId: string) => { |
||||||
|
|
||||||
|
const res = await get<Record<string, unknown>>( |
||||||
|
|
||||||
|
`/customer/byopenid/${encodeURIComponent(openId)}` |
||||||
|
|
||||||
|
) |
||||||
|
|
||||||
|
return { ...res, data: normalizeCustomer(res.data as Record<string, unknown>) } |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
|
||||||
|
* 查询客户在指定门店的余额 |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
export const getCustomerBalance = async (openId: string, bpSn: string) => { |
||||||
|
|
||||||
|
const res = await get<Record<string, unknown>>('/customer/balance', { |
||||||
|
|
||||||
|
openid: openId, |
||||||
|
|
||||||
|
bpsn: bpSn, |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
const amount = toNumber(pickField(res.data as Record<string, unknown>, 'amount')) |
||||||
|
|
||||||
|
return { ...res, data: { amount } as CustomerBalanceResult } |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
|
||||||
|
* 查询客户在所有已消费门店的余额列表(含积分/存杆柜数量/VIP到期时间) |
||||||
|
|
||||||
|
* 对应接口:GET /customer/balancelist/{openId} |
||||||
|
|
||||||
|
* 用于 pagesOrder/accountBalance 的"账户余额" Tab(BalanceList 组件) |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
export const getCustomerBalanceList = async (openId: string) => { |
||||||
|
|
||||||
|
const res = await get<Record<string, unknown>>( |
||||||
|
|
||||||
|
`/customer/balancelist/${encodeURIComponent(openId)}` |
||||||
|
|
||||||
|
) |
||||||
|
|
||||||
|
return { ...res, data: normalizeBalanceList(res.data as unknown as Record<string, unknown>[]) } |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** |
/** |
||||||
* 根据 OpenId 获取客户信息(含最新余额、会员类型等) |
|
||||||
* 登录后或页面刷新时调用,比 getCustomerBySn 更通用(无需 sn) |
* 刷新用户资料 + 余额(有 bpSn 时) |
||||||
* @param openId 微信 OpenId |
|
||||||
*/ |
*/ |
||||||
export const getCustomerByOpenId = (openId: string) => { |
|
||||||
return get<CustomerDto>(`/Customer/${openId}`) |
export const refreshCustomerProfile = async (openId: string, bpSn?: string) => { |
||||||
|
|
||||||
|
const profileRes = await getCustomerByOpenId(openId) |
||||||
|
|
||||||
|
if (profileRes.data) { |
||||||
|
|
||||||
|
userStore.setUserInfoFromWechat(profileRes.data) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const storeSn = bpSn || profileRes.data?.bpSn || userStore.userInfo?.bpSn |
||||||
|
|
||||||
|
if (storeSn) { |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
const balanceRes = await getCustomerBalance(openId, storeSn) |
||||||
|
|
||||||
|
userStore.setBalance(balanceRes.data.amount) |
||||||
|
|
||||||
|
if (userStore.userInfo) { |
||||||
|
|
||||||
|
userStore.setUserInfo({ |
||||||
|
|
||||||
|
...userStore.userInfo, |
||||||
|
|
||||||
|
amount: balanceRes.data.amount, |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} catch (e) { |
||||||
|
|
||||||
|
console.warn('刷新余额失败:', e) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return profileRes |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** |
/** |
||||||
* 根据客户 SN 获取客户详情(含最新余额、会员类型等) |
|
||||||
* @param sn 客户编码(CustomerDto.sn) |
* multipart 上传头像(小程序 uploadFile 仅支持 POST) |
||||||
|
|
||||||
*/ |
*/ |
||||||
export const getCustomerBySn = (sn: string) => { |
|
||||||
return get<CustomerDto>(`/Customer/sn/${sn}`) |
const uploadCustomerWithAvatar = ( |
||||||
|
|
||||||
|
customerId: number, |
||||||
|
|
||||||
|
dto: UpdateCustomerDto |
||||||
|
|
||||||
|
): Promise<ResponseResult<CustomerDto>> => { |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
|
||||||
|
const openId = Taro.getStorageSync('openId') as string |
||||||
|
|
||||||
|
const formData: Record<string, string> = { id: String(customerId) } |
||||||
|
|
||||||
|
if (dto.name) formData.name = dto.name |
||||||
|
|
||||||
|
if (dto.phone) formData.phone = dto.phone |
||||||
|
|
||||||
|
if (dto.birthday) formData.birthday = dto.birthday |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Taro.uploadFile({ |
||||||
|
|
||||||
|
url: `${config.apiPrefix}/customer/update`, |
||||||
|
|
||||||
|
filePath: dto.avatarFilePath!, |
||||||
|
|
||||||
|
name: 'avatarimage', |
||||||
|
|
||||||
|
formData, |
||||||
|
|
||||||
|
header: openId ? { 'X-OpenId': openId } : {}, |
||||||
|
|
||||||
|
success: (res) => { |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
const body = JSON.parse(res.data) as ResponseResult<Record<string, unknown>> |
||||||
|
|
||||||
|
if (body.success) { |
||||||
|
|
||||||
|
resolve({ ...body, data: normalizeCustomer(body.data) }) |
||||||
|
|
||||||
|
} else { |
||||||
|
|
||||||
|
reject(body) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} catch (err) { |
||||||
|
|
||||||
|
reject(err) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
fail: reject, |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** |
/** |
||||||
* 更新客户信息(昵称、头像、手机号、微信号等) |
|
||||||
* @param customerId 客户 ID(CustomerDto.id) |
* 更新客户信息(昵称、头像、手机号等) |
||||||
* @param dto 要更新的字段 |
|
||||||
*/ |
*/ |
||||||
export const updateCustomer = (customerId: number, dto: UpdateCustomerDto) => { |
|
||||||
return put<void>(`/Customer/${customerId}`, dto) |
export const updateCustomer = async (customerId: number, dto: UpdateCustomerDto) => { |
||||||
|
|
||||||
|
if (dto.avatarFilePath) { |
||||||
|
|
||||||
|
return uploadCustomerWithAvatar(customerId, dto) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const body: Record<string, string> = { id: String(customerId) } |
||||||
|
|
||||||
|
if (dto.name !== undefined) body.name = dto.name |
||||||
|
|
||||||
|
if (dto.phone !== undefined) body.phone = dto.phone |
||||||
|
|
||||||
|
if (dto.birthday !== undefined) body.birthday = dto.birthday |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const res = await post<Record<string, unknown>>('/customer/update', body) |
||||||
|
|
||||||
|
return { ...res, data: normalizeCustomer(res.data) } |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** |
/** |
||||||
* 读取本地图片并转为 Base64 字符串 |
|
||||||
* 用于将选择的头像图片编码后传给更新接口 |
* @deprecated 新接口使用 multipart avatarFilePath |
||||||
* @param filePath 本地临时文件路径(如 wx.chooseImage 返回的路径) |
|
||||||
* @returns Base64 字符串(不含 data:image/xxx;base64, 前缀) |
|
||||||
*/ |
*/ |
||||||
|
|
||||||
export const readImageAsBase64 = (filePath: string): Promise<string> => { |
export const readImageAsBase64 = (filePath: string): Promise<string> => { |
||||||
return new Promise((resolve, reject) => { |
return new Promise((resolve, reject) => { |
||||||
const fs = wx.getFileSystemManager() |
const fs = Taro.getFileSystemManager() |
||||||
fs.readFile({ |
fs.readFile({ |
||||||
filePath, |
filePath, |
||||||
encoding: 'base64', |
encoding: 'base64', |
||||||
success: (res) => resolve(res.data as string), |
success: (res) => resolve(res.data as string), |
||||||
fail: (err) => reject(err), |
fail: (err: TaroGeneral.CallbackResult) => reject(err), |
||||||
}) |
}) |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,66 @@ |
|||||||
|
/** |
||||||
|
* magdev 统一操作接口 |
||||||
|
* peroperate:/api/v2/magdev/peroperate — 去开台(预检) |
||||||
|
* operate: /api/v2/magdev/operate — 开台/关台/续费/关灯重开/分享等 |
||||||
|
* getopstatus:/api/v2/magdev/getopstatus — 查询开台结果(付款后查一次) |
||||||
|
*/ |
||||||
|
import { get, post } from '../services/request' |
||||||
|
import { |
||||||
|
normalizePreOperateResponse, |
||||||
|
normalizeOperateResponse, |
||||||
|
normalizeGetOpStatusResponse, |
||||||
|
} from '../utils/operateNormalize' |
||||||
|
import { OperateRequestDto } from '../types/magdev' |
||||||
|
import { CtrlType, PayType } from '../types/startDevice' |
||||||
|
|
||||||
|
/** 小程序 JSAPI 支付固定交易类型 */ |
||||||
|
const MINI_PROGRAM_TRADE_TYPE = 1 |
||||||
|
|
||||||
|
/** |
||||||
|
* 去开台(预开台) |
||||||
|
* @param type 开台类型(1-押金 2-定时 3-信用 4-团购券) |
||||||
|
* @param grpSn 设备编码 |
||||||
|
* @param openId 用户 openId |
||||||
|
*/ |
||||||
|
export const preOperate = async (type: number, grpSn: string, openId: string) => { |
||||||
|
const res = await post<Record<string, unknown>>( |
||||||
|
'/magdev/peroperate', |
||||||
|
{ type, grpsn: grpSn, openid: openId }, |
||||||
|
{ showLoading: true, loadingText: '处理中...' } |
||||||
|
) |
||||||
|
return { ...res, data: normalizePreOperateResponse(res.data as Record<string, unknown>) } |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 统一操作接口(开台/关台/续费/关灯重开/分享等) |
||||||
|
* @param dto 操作请求参数 |
||||||
|
*/ |
||||||
|
export const operate = async (dto: OperateRequestDto) => { |
||||||
|
const res = await post<Record<string, unknown>>( |
||||||
|
'/magdev/operate', |
||||||
|
{ |
||||||
|
grpsn: dto.grpSn, |
||||||
|
openid: dto.openId, |
||||||
|
ctrltype: dto.ctrlType, |
||||||
|
amount: dto.amount, |
||||||
|
paytype: dto.payType, |
||||||
|
tradetype: dto.tradeType ?? MINI_PROGRAM_TRADE_TYPE, |
||||||
|
couponcode: dto.couponCode ?? '', |
||||||
|
remark: dto.remark ?? '', |
||||||
|
attach: dto.attach ?? '', |
||||||
|
}, |
||||||
|
{ showLoading: true, loadingText: '处理中...' } |
||||||
|
) |
||||||
|
return { ...res, data: normalizeOperateResponse(res.data as Record<string, unknown>) } |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 查询开台状态(微信支付成功后调用一次,不轮询) |
||||||
|
* @param sn 设备编码(grpSn) |
||||||
|
*/ |
||||||
|
export const getOpStatus = async (sn: string) => { |
||||||
|
const res = await get<Record<string, unknown>>('/magdev/getopstatus', { sn }) |
||||||
|
return { ...res, data: normalizeGetOpStatusResponse(res.data as Record<string, unknown>) } |
||||||
|
} |
||||||
|
|
||||||
|
export { CtrlType, PayType } |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
/** |
||||||
|
* 支付渠道相关 API |
||||||
|
*/ |
||||||
|
import { get } from '../services/request' |
||||||
|
import { pickField, toNumber } from '../utils/apiNormalize' |
||||||
|
|
||||||
|
export interface PaymentChannelItem { |
||||||
|
/** 渠道编码:1-微信 */ |
||||||
|
code: number |
||||||
|
name: string |
||||||
|
enabled: boolean |
||||||
|
} |
||||||
|
|
||||||
|
/** 微信支付渠道码 */ |
||||||
|
export const PAY_CHANNEL_WECHAT = 1 |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取机构支持的支付方式 |
||||||
|
* 文档暂无响应体定义,按常见结构归一化;仅微信时使用 code=1 |
||||||
|
*/ |
||||||
|
export const getPaymentChannels = async (instSn: string) => { |
||||||
|
const res = await get<unknown>('/payment/channels', { instsn: instSn }, { showError: false }) |
||||||
|
|
||||||
|
let list: PaymentChannelItem[] = [] |
||||||
|
const raw = res.data |
||||||
|
|
||||||
|
if (Array.isArray(raw)) { |
||||||
|
list = raw.map((item) => normalizeChannelItem(item as Record<string, unknown>)) |
||||||
|
} else if (raw && typeof raw === 'object') { |
||||||
|
const channels = pickField<Record<string, unknown>[]>( |
||||||
|
raw as Record<string, unknown>, |
||||||
|
'channels', |
||||||
|
'data' |
||||||
|
) |
||||||
|
if (Array.isArray(channels)) { |
||||||
|
list = channels.map(normalizeChannelItem) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { ...res, data: list } |
||||||
|
} |
||||||
|
|
||||||
|
const normalizeChannelItem = (raw: Record<string, unknown>): PaymentChannelItem => ({ |
||||||
|
code: toNumber(pickField(raw, 'code', 'paytype', 'channel')), |
||||||
|
name: String(pickField(raw, 'name', 'channelName', 'channelname') ?? ''), |
||||||
|
enabled: pickField(raw, 'enabled', 'available') !== false, |
||||||
|
}) |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否支持微信支付(本期仅对接微信) |
||||||
|
*/ |
||||||
|
export const isWechatPaySupported = async (instSn: string): Promise<boolean> => { |
||||||
|
try { |
||||||
|
const res = await getPaymentChannels(instSn) |
||||||
|
if (!res.data.length) return true |
||||||
|
return res.data.some((item) => item.code === PAY_CHANNEL_WECHAT && item.enabled) |
||||||
|
} catch { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
@ -1,66 +1,126 @@ |
|||||||
import { PropsWithChildren } from 'react' |
import { PropsWithChildren } from 'react' |
||||||
|
|
||||||
import { useLaunch } from '@tarojs/taro' |
import { useLaunch } from '@tarojs/taro' |
||||||
|
|
||||||
import Taro from '@tarojs/taro' |
import Taro from '@tarojs/taro' |
||||||
|
|
||||||
import { userStore } from './store' |
import { userStore } from './store' |
||||||
import { getCustomerByOpenId } from './api/customer' |
|
||||||
|
import { refreshCustomerProfile } from './api/customer' |
||||||
|
|
||||||
import { wechatLogin } from './api/login' |
import { wechatLogin } from './api/login' |
||||||
|
|
||||||
import './app.scss' |
import './app.scss' |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App({ children }: PropsWithChildren<any>) { |
function App({ children }: PropsWithChildren<any>) { |
||||||
|
|
||||||
useLaunch(() => { |
useLaunch(() => { |
||||||
|
|
||||||
console.log('台球馆小程序启动') |
console.log('台球馆小程序启动') |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 先从本地缓存恢复用户信息(快速恢复登录态)
|
// 先从本地缓存恢复用户信息(快速恢复登录态)
|
||||||
|
|
||||||
try { |
try { |
||||||
|
|
||||||
const savedUserInfo = Taro.getStorageSync('userInfo') |
const savedUserInfo = Taro.getStorageSync('userInfo') |
||||||
|
|
||||||
if (savedUserInfo) { |
if (savedUserInfo) { |
||||||
|
|
||||||
userStore.setUserInfo(savedUserInfo) |
userStore.setUserInfo(savedUserInfo) |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
} catch (error) { |
} catch (error) { |
||||||
|
|
||||||
console.warn('初始化用户信息失败:', error) |
console.warn('初始化用户信息失败:', error) |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') |
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (openId) { |
if (openId) { |
||||||
|
|
||||||
// 已有 openId:后台静默刷新最新用户信息(余额、会员等),不阻塞启动
|
// 已有 openId:后台静默刷新最新用户信息(余额、会员等),不阻塞启动
|
||||||
getCustomerByOpenId(openId) |
|
||||||
.then(res => { |
refreshCustomerProfile(openId) |
||||||
if (res.data) { |
|
||||||
userStore.setUserInfoFromWechat(res.data) |
|
||||||
} |
|
||||||
}) |
|
||||||
.catch(e => console.warn('启动刷新用户信息失败:', e)) |
.catch(e => console.warn('启动刷新用户信息失败:', e)) |
||||||
|
|
||||||
|
.finally(() => userStore.setLoginReady()) |
||||||
|
|
||||||
} else { |
} else { |
||||||
|
|
||||||
// 无本地 openId:执行无感登录,wx.login() 不需要用户任何操作
|
// 无本地 openId:执行无感登录,wx.login() 不需要用户任何操作
|
||||||
|
|
||||||
Taro.login({ |
Taro.login({ |
||||||
|
|
||||||
success: async ({ code }) => { |
success: async ({ code }) => { |
||||||
|
|
||||||
try { |
try { |
||||||
|
|
||||||
const res = await wechatLogin(code) |
const res = await wechatLogin(code) |
||||||
if (res.success && res.data) { |
|
||||||
|
if (res.success && res.data?.customer) { |
||||||
|
|
||||||
userStore.setUserInfoFromWechat(res.data.customer) |
userStore.setUserInfoFromWechat(res.data.customer) |
||||||
// 新用户:触发头像/昵称补全弹窗
|
|
||||||
if (res.data.isNewCustomer) { |
if (res.data.isNewCustomer) { |
||||||
|
|
||||||
userStore.setIsNewUser(true) |
userStore.setIsNewUser(true) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
const wxOpenId = res.data.customer.wxOpenId |
||||||
|
|
||||||
|
if (wxOpenId) { |
||||||
|
|
||||||
|
await refreshCustomerProfile(wxOpenId) |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
} catch (e) { |
} catch (e) { |
||||||
|
|
||||||
console.warn('无感登录失败:', e) |
console.warn('无感登录失败:', e) |
||||||
|
|
||||||
} finally { |
} finally { |
||||||
// 无论成功失败,都标记登录流程结束,让 qrHandler 继续处理
|
|
||||||
userStore.setLoginReady() |
userStore.setLoginReady() |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
}, |
}, |
||||||
|
|
||||||
fail: (err) => { |
fail: (err) => { |
||||||
|
|
||||||
console.warn('wx.login 调用失败:', err) |
console.warn('wx.login 调用失败:', err) |
||||||
// 登录失败也要标记结束,避免 qrHandler 永久等待
|
|
||||||
userStore.setLoginReady() |
userStore.setLoginReady() |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
}) |
}) |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
}) |
}) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return children |
return children |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default App |
export default App |
||||||
|
|
||||||
|
|||||||
@ -1,150 +0,0 @@ |
|||||||
/** |
|
||||||
* 球台相关 API |
|
||||||
* 包含开台、关台、续费等接口 |
|
||||||
*/ |
|
||||||
import { post } from './request' |
|
||||||
|
|
||||||
/** |
|
||||||
* 开台操作类型 |
|
||||||
*/ |
|
||||||
export enum TableOperationType { |
|
||||||
/** 押金开台 */ |
|
||||||
SelfServiceDeposit = 1, |
|
||||||
/** 余额开台 */ |
|
||||||
SelfServiceBalance = 2, |
|
||||||
/** 定时开台 */ |
|
||||||
Timed = 3, |
|
||||||
/** 信用开台 */ |
|
||||||
Credit = 4, |
|
||||||
/** 团购券开台 */ |
|
||||||
GroupBuy = 5, |
|
||||||
/** 立即关台 */ |
|
||||||
ImmediateClose = 6, |
|
||||||
/** 续费 */ |
|
||||||
Renew = 7, |
|
||||||
/** 关灯重开 */ |
|
||||||
ReopenAfterClose = 8, |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 支付渠道 |
|
||||||
*/ |
|
||||||
export enum PaymentChannel { |
|
||||||
/** 微信支付 */ |
|
||||||
WechatPay = 1, |
|
||||||
/** 支付宝 */ |
|
||||||
Alipay = 2, |
|
||||||
/** 银联支付 */ |
|
||||||
UnionPay = 3, |
|
||||||
/** 余额支付 */ |
|
||||||
Balance = 4, |
|
||||||
/** 银行卡支付 */ |
|
||||||
BankCard = 5, |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 开台请求参数 |
|
||||||
*/ |
|
||||||
export interface StartTableRequest { |
|
||||||
/** 设备编码 */ |
|
||||||
grpSn: string |
|
||||||
/** 用户 OpenId */ |
|
||||||
openId: string |
|
||||||
/** 开台操作类型 */ |
|
||||||
ctrlType: TableOperationType |
|
||||||
/** 消费金额 */ |
|
||||||
amount?: number |
|
||||||
/** 支付渠道 */ |
|
||||||
payType?: PaymentChannel |
|
||||||
/** 开台时长(小时),定时开台使用 */ |
|
||||||
durationHours?: number |
|
||||||
/** 团购券码,团购券开台使用 */ |
|
||||||
couponCode?: string |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 开台响应数据 |
|
||||||
*/ |
|
||||||
export interface StartTableResponse { |
|
||||||
/** 机构编码 */ |
|
||||||
instSn: string |
|
||||||
/** 店铺编码 */ |
|
||||||
bpSn: string |
|
||||||
/** 球台编码 */ |
|
||||||
grpSn: string |
|
||||||
/** 开台设备id */ |
|
||||||
operationId: number |
|
||||||
/** 开始时间 */ |
|
||||||
startTime: string |
|
||||||
/** 结束时间 */ |
|
||||||
expectedEndTime: string |
|
||||||
/** 开台操作详情id */ |
|
||||||
detailIds: number[] |
|
||||||
/** 控制id */ |
|
||||||
controlMessageId: number |
|
||||||
/** 商户订单号 */ |
|
||||||
outTradeNo: string |
|
||||||
/** 支付参数(用于调起微信支付) */ |
|
||||||
payParameters: WechatPayParameters | null |
|
||||||
/** 支付URL(用于扫码支付) */ |
|
||||||
payUrl: string |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 微信支付参数 |
|
||||||
*/ |
|
||||||
export interface WechatPayParameters { |
|
||||||
/** 时间戳 */ |
|
||||||
timeStamp: string |
|
||||||
/** 随机字符串 */ |
|
||||||
nonceStr: string |
|
||||||
/** 统一下单接口返回的 prepay_id */ |
|
||||||
package: string |
|
||||||
/** 签名方式 */ |
|
||||||
signType: string |
|
||||||
/** 签名 */ |
|
||||||
paySign: string |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 开台 API |
|
||||||
* @param data 开台请求参数 |
|
||||||
* @returns 开台响应 |
|
||||||
*/ |
|
||||||
export const startTable = (data: StartTableRequest) => { |
|
||||||
return post<StartTableResponse>('/Tables/start', data, { |
|
||||||
showLoading: true, |
|
||||||
loadingText: '开台中...', |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 调起微信支付 |
|
||||||
* @param payParams 支付参数 |
|
||||||
* @returns Promise |
|
||||||
*/ |
|
||||||
export const invokeWechatPay = (payParams: WechatPayParameters): Promise<void> => { |
|
||||||
return new Promise((resolve, reject) => { |
|
||||||
// 使用 wx.requestPayment 调起微信支付
|
|
||||||
wx.requestPayment({ |
|
||||||
timeStamp: payParams.timeStamp, |
|
||||||
nonceStr: payParams.nonceStr, |
|
||||||
package: payParams.package, |
|
||||||
signType: payParams.signType as 'MD5' | 'HMAC-SHA256' | 'RSA', |
|
||||||
paySign: payParams.paySign, |
|
||||||
success: () => { |
|
||||||
resolve() |
|
||||||
}, |
|
||||||
fail: (err) => { |
|
||||||
reject(err) |
|
||||||
}, |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
startTable, |
|
||||||
invokeWechatPay, |
|
||||||
TableOperationType, |
|
||||||
PaymentChannel, |
|
||||||
} |
|
||||||
@ -0,0 +1,91 @@ |
|||||||
|
/** |
||||||
|
* magdev(开台/关台)接口相关类型定义 |
||||||
|
* 对应接口:/api/v2/magdev/peroperate、/api/v2/magdev/operate、/api/v2/magdev/getopstatus |
||||||
|
*/ |
||||||
|
import { PriceInfoDto, CtrlType, PayType, WechatPayParameters } from './startDevice' |
||||||
|
|
||||||
|
/** 去开台(预开台)请求参数 */ |
||||||
|
export interface PreOperateRequestDto { |
||||||
|
type: number |
||||||
|
grpSn: string |
||||||
|
openId: string |
||||||
|
} |
||||||
|
|
||||||
|
/** 支付方式DTO(peroperate 返回) */ |
||||||
|
export interface PaymentMethodDto { |
||||||
|
type: number |
||||||
|
name: string |
||||||
|
available: boolean |
||||||
|
balanceDisplay: string | null |
||||||
|
} |
||||||
|
|
||||||
|
/** 设备简要信息(peroperate 返回) */ |
||||||
|
export interface DevInfoDto { |
||||||
|
id: number |
||||||
|
sn: string |
||||||
|
name: string |
||||||
|
number: string |
||||||
|
wstatus: number |
||||||
|
status: number |
||||||
|
shared: number |
||||||
|
} |
||||||
|
|
||||||
|
/** 去开台响应数据(对应 data 字段) */ |
||||||
|
export interface PreOperateResponseData { |
||||||
|
/** 余额充足时直接开台返回订单号,为空表示需进子页支付 */ |
||||||
|
outTradeNo: string |
||||||
|
message: string |
||||||
|
isMember: boolean |
||||||
|
balance: number |
||||||
|
bpName: string |
||||||
|
bpSn: string |
||||||
|
instSn: string |
||||||
|
devType: number |
||||||
|
devInfo: DevInfoDto |
||||||
|
price: PriceInfoDto |
||||||
|
paymentMethods: PaymentMethodDto[] |
||||||
|
} |
||||||
|
|
||||||
|
/** operate 统一操作请求参数 */ |
||||||
|
export interface OperateRequestDto { |
||||||
|
grpSn: string |
||||||
|
openId: string |
||||||
|
ctrlType: CtrlType |
||||||
|
amount: number |
||||||
|
payType: PayType |
||||||
|
tradeType?: number |
||||||
|
couponCode?: string |
||||||
|
remark?: string |
||||||
|
attach?: string |
||||||
|
} |
||||||
|
|
||||||
|
/** operate 响应中的支付信息 */ |
||||||
|
export interface OperatePaymentResponseDto { |
||||||
|
outTradeNo: string |
||||||
|
channel: number |
||||||
|
payParameters: WechatPayParameters | null |
||||||
|
payUrl: string |
||||||
|
} |
||||||
|
|
||||||
|
/** operate 响应数据(对应 data 字段) */ |
||||||
|
export interface OperateResponseData { |
||||||
|
consumptionId: number |
||||||
|
errorMessage: string |
||||||
|
paymentResponseDto: OperatePaymentResponseDto | null |
||||||
|
} |
||||||
|
|
||||||
|
/** getopstatus 响应中的操作选项 */ |
||||||
|
export interface OpenOptionItem { |
||||||
|
title: string |
||||||
|
type: number |
||||||
|
desc: string |
||||||
|
available: boolean |
||||||
|
} |
||||||
|
|
||||||
|
/** getopstatus 响应数据(对应 data 字段) */ |
||||||
|
export interface GetOpStatusResponseData { |
||||||
|
isOpened: boolean |
||||||
|
openOptions: OpenOptionItem[] |
||||||
|
} |
||||||
|
|
||||||
|
export { CtrlType, PayType } |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
/** |
||||||
|
* 新接口字段大小写兼容:Word/Swagger 多为全小写,现网前端用 camelCase |
||||||
|
*/ |
||||||
|
export const pickField = <T = unknown>( |
||||||
|
source: Record<string, any> | null | undefined, |
||||||
|
...keys: string[] |
||||||
|
): T | undefined => { |
||||||
|
if (!source) return undefined |
||||||
|
for (const key of keys) { |
||||||
|
if (source[key] !== undefined && source[key] !== null) { |
||||||
|
return source[key] as T |
||||||
|
} |
||||||
|
} |
||||||
|
return undefined |
||||||
|
} |
||||||
|
|
||||||
|
export const toNumber = (value: unknown, fallback = 0): number => { |
||||||
|
if (typeof value === 'number' && !Number.isNaN(value)) return value |
||||||
|
if (typeof value === 'string' && value.trim() !== '') { |
||||||
|
const n = Number(value) |
||||||
|
return Number.isNaN(n) ? fallback : n |
||||||
|
} |
||||||
|
return fallback |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
/** |
||||||
|
* 拨打门店/店长电话(来自扫码 qrcode.phone) |
||||||
|
*/ |
||||||
|
import Taro from '@tarojs/taro' |
||||||
|
|
||||||
|
export const callShopPhone = (phone?: string | null): void => { |
||||||
|
const phoneNumber = (phone || '').trim() |
||||||
|
if (!phoneNumber) { |
||||||
|
Taro.showToast({ title: '暂无店长电话', icon: 'none', duration: 2000 }) |
||||||
|
return |
||||||
|
} |
||||||
|
Taro.makePhoneCall({ phoneNumber }).catch((err) => { |
||||||
|
console.error('拨打电话失败:', err) |
||||||
|
}) |
||||||
|
} |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
/** |
||||||
|
* 客户字段归一化(新接口全小写 / 旧 camelCase) |
||||||
|
*/ |
||||||
|
import { CustomerDto, CustomerBalanceListItem } from '../types/login' |
||||||
|
import { pickField, toNumber } from '../utils/apiNormalize' |
||||||
|
|
||||||
|
export const normalizeCustomer = (raw: Record<string, any> | null | undefined): CustomerDto => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
id: toNumber(pickField(src, 'id')), |
||||||
|
sn: String(pickField(src, 'sn') ?? ''), |
||||||
|
wxOpenId: String(pickField(src, 'wxOpenId', 'wxopenid') ?? ''), |
||||||
|
name: String(pickField(src, 'name') ?? ''), |
||||||
|
type: toNumber(pickField(src, 'type')), |
||||||
|
typeName: String(pickField(src, 'typeName', 'typename') ?? ''), |
||||||
|
amount: toNumber(pickField(src, 'amount')), |
||||||
|
wchart: String(pickField(src, 'wchart') ?? ''), |
||||||
|
phone: String(pickField(src, 'phone') ?? ''), |
||||||
|
bpSn: String(pickField(src, 'bpSn', 'bpsn') ?? ''), |
||||||
|
bpInfoName: String(pickField(src, 'bpInfoName', 'bpinfoname', 'bpname') ?? ''), |
||||||
|
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''), |
||||||
|
ownerName: String(pickField(src, 'ownerName', 'ownername') ?? ''), |
||||||
|
createTime: String(pickField(src, 'createTime', 'createtime') ?? ''), |
||||||
|
lastLoginTime: String(pickField(src, 'lastLoginTime', 'lastlogintime') ?? ''), |
||||||
|
wxNickName: String(pickField(src, 'wxNickName', 'wxnickname', 'name') ?? ''), |
||||||
|
wxAvatarUrl: String(pickField(src, 'wxAvatarUrl', 'wxavatarurl') ?? ''), |
||||||
|
birthday: pickField<string>(src, 'birthday'), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 归一化单条门店余额(balancelist 数组项) */ |
||||||
|
export const normalizeBalanceListItem = (raw: Record<string, any>): CustomerBalanceListItem => ({ |
||||||
|
bpSn: String(pickField(raw, 'bpSn', 'bpsn') ?? ''), |
||||||
|
bpName: String(pickField(raw, 'bpName', 'bpname') ?? ''), |
||||||
|
amount: toNumber(pickField(raw, 'amount')), |
||||||
|
points: toNumber(pickField(raw, 'points')), |
||||||
|
lockerCount: toNumber(pickField(raw, 'lockerCount', 'lockercount')), |
||||||
|
vipEndTime: pickField<string | null>(raw, 'vipEndTime', 'vipendtime') ?? null, |
||||||
|
}) |
||||||
|
|
||||||
|
/** 归一化客户跨店余额列表 */ |
||||||
|
export const normalizeBalanceList = ( |
||||||
|
raw: Record<string, any>[] | null | undefined |
||||||
|
): CustomerBalanceListItem[] => { |
||||||
|
if (!Array.isArray(raw)) return [] |
||||||
|
return raw.map(normalizeBalanceListItem) |
||||||
|
} |
||||||
|
|
||||||
|
/** 登录 data:扁平(Word)或嵌套 customer(旧) */ |
||||||
|
export const normalizeLoginData = (data: Record<string, any> | null | undefined) => { |
||||||
|
const src = data || {} |
||||||
|
const nested = pickField<Record<string, any>>(src, 'customer') |
||||||
|
const customer = normalizeCustomer(nested || src) |
||||||
|
const isNewCustomer = Boolean( |
||||||
|
pickField(src, 'isNewCustomer', 'isnewcustomer') ?? false |
||||||
|
) |
||||||
|
return { customer, isNewCustomer, message: String(pickField(src, 'message') ?? '') } |
||||||
|
} |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
/** |
||||||
|
* magdev operate / peroperate / getopstatus 响应字段归一化 |
||||||
|
*/ |
||||||
|
import { pickField, toNumber } from './apiNormalize' |
||||||
|
import { normalizeWechatPayParams } from './rechargeNormalize' |
||||||
|
import { PriceInfoDto, TimePriceDto } from '../types/startDevice' |
||||||
|
import { |
||||||
|
OperateResponseData, |
||||||
|
OperatePaymentResponseDto, |
||||||
|
PreOperateResponseData, |
||||||
|
PaymentMethodDto, |
||||||
|
DevInfoDto, |
||||||
|
GetOpStatusResponseData, |
||||||
|
OpenOptionItem, |
||||||
|
} from '../types/magdev' |
||||||
|
|
||||||
|
const normalizeTimePrice = (raw: Record<string, any>): TimePriceDto => ({ |
||||||
|
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''), |
||||||
|
endTime: String(pickField(raw, 'endTime', 'endtime') ?? ''), |
||||||
|
price: toNumber(pickField(raw, 'price')), |
||||||
|
description: String(pickField(raw, 'description') ?? ''), |
||||||
|
}) |
||||||
|
|
||||||
|
const normalizePrice = (raw: Record<string, any> | null | undefined): PriceInfoDto => { |
||||||
|
const src = raw || {} |
||||||
|
const timePrices = pickField<Record<string, any>[]>(src, 'timePrices', 'timeprices') || [] |
||||||
|
return { |
||||||
|
standard: toNumber(pickField(src, 'standard')), |
||||||
|
memberPrice: toNumber(pickField(src, 'memberPrice', 'memberprice')), |
||||||
|
unit: String(pickField(src, 'unit') ?? ''), |
||||||
|
deposit: toNumber(pickField(src, 'deposit')), |
||||||
|
pricePerMinute: toNumber(pickField(src, 'pricePerMinute', 'pricePerminute', 'priceperminute')), |
||||||
|
memberPricePerMinute: toNumber(pickField(src, 'memberPricePerMinute', 'memberpriceperminute')), |
||||||
|
currentPrice: toNumber(pickField(src, 'currentPrice', 'currentprice')), |
||||||
|
descript: String(pickField(src, 'descript') ?? ''), |
||||||
|
pricePeriod: String(pickField(src, 'pricePeriod', 'priceperiod') ?? ''), |
||||||
|
timePrices: Array.isArray(timePrices) ? timePrices.map(normalizeTimePrice) : [], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const normalizeDevInfo = (raw: Record<string, any> | null | undefined): DevInfoDto => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
id: toNumber(pickField(src, 'id')), |
||||||
|
sn: String(pickField(src, 'sn') ?? ''), |
||||||
|
name: String(pickField(src, 'name') ?? ''), |
||||||
|
number: String(pickField(src, 'number') ?? ''), |
||||||
|
wstatus: toNumber(pickField(src, 'wstatus')), |
||||||
|
status: toNumber(pickField(src, 'status')), |
||||||
|
shared: toNumber(pickField(src, 'shared')), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const normalizePaymentMethods = (raw: any[]): PaymentMethodDto[] => { |
||||||
|
if (!Array.isArray(raw)) return [] |
||||||
|
return raw.map(item => ({ |
||||||
|
type: toNumber(pickField(item, 'type')), |
||||||
|
name: String(pickField(item, 'name') ?? ''), |
||||||
|
available: !!pickField(item, 'available'), |
||||||
|
balanceDisplay: pickField<string>(item, 'balanceDisplay', 'balancedisplay') ?? null, |
||||||
|
})) |
||||||
|
} |
||||||
|
|
||||||
|
const normalizeOperatePaymentResponse = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): OperatePaymentResponseDto | null => { |
||||||
|
if (!raw) return null |
||||||
|
const payParams = normalizeWechatPayParams( |
||||||
|
pickField<Record<string, any>>(raw, 'payParameters', 'payparameters') |
||||||
|
) |
||||||
|
if (!payParams && !pickField(raw, 'outTradeNo', 'outtradeno')) return null |
||||||
|
return { |
||||||
|
outTradeNo: String(pickField(raw, 'outTradeNo', 'outtradeno') ?? ''), |
||||||
|
channel: toNumber(pickField(raw, 'channel')), |
||||||
|
payParameters: payParams, |
||||||
|
payUrl: String(pickField(raw, 'payUrl', 'payurl') ?? ''), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 归一化 operate(开台/关台/续费)响应 data */ |
||||||
|
export const normalizeOperateResponse = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): OperateResponseData => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
consumptionId: toNumber(pickField(src, 'consumptionId', 'consumptionid')), |
||||||
|
errorMessage: String(pickField(src, 'errorMessage', 'errormessage') ?? ''), |
||||||
|
paymentResponseDto: normalizeOperatePaymentResponse( |
||||||
|
pickField<Record<string, any>>(src, 'paymentResponseDto', 'paymentresponsedto') |
||||||
|
), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 归一化 peroperate(去开台)响应 data */ |
||||||
|
export const normalizePreOperateResponse = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): PreOperateResponseData => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
message: String(pickField(src, 'message') ?? ''), |
||||||
|
outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''), |
||||||
|
isMember: !!pickField(src, 'isMember', 'ismember'), |
||||||
|
balance: toNumber(pickField(src, 'balance')), |
||||||
|
bpName: String(pickField(src, 'bpName', 'bpname') ?? ''), |
||||||
|
bpSn: String(pickField(src, 'bpSn', 'bpsn') ?? ''), |
||||||
|
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''), |
||||||
|
devType: toNumber(pickField(src, 'devType', 'devtype')), |
||||||
|
devInfo: normalizeDevInfo( |
||||||
|
pickField<Record<string, any>>(src, 'devInfo', 'devinfo') |
||||||
|
), |
||||||
|
price: normalizePrice(pickField<Record<string, any>>(src, 'price')), |
||||||
|
paymentMethods: normalizePaymentMethods( |
||||||
|
pickField<any[]>(src, 'paymentMethods', 'paymentmethods') ?? [] |
||||||
|
), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 归一化 getopstatus 响应 data */ |
||||||
|
export const normalizeGetOpStatusResponse = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): GetOpStatusResponseData => { |
||||||
|
const src = raw || {} |
||||||
|
const opts = pickField<any[]>(src, 'openOptions', 'openoptions') ?? [] |
||||||
|
return { |
||||||
|
isOpened: !!pickField(src, 'isOpened', 'isopened'), |
||||||
|
openOptions: Array.isArray(opts) |
||||||
|
? opts.map((item): OpenOptionItem => ({ |
||||||
|
title: String(pickField(item, 'title') ?? ''), |
||||||
|
type: toNumber(pickField(item, 'type')), |
||||||
|
desc: String(pickField(item, 'desc', 'description') ?? ''), |
||||||
|
available: !!pickField(item, 'available'), |
||||||
|
})) |
||||||
|
: [], |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
/** |
||||||
|
* 订单记录 / 订单详情 字段归一化 |
||||||
|
* 对应接口:/api/v2/order/records、/api/v2/order/detail/{id} |
||||||
|
*/ |
||||||
|
import { pickField, toNumber } from './apiNormalize' |
||||||
|
import { OrderRecordItem, OrderRecordPagedResult, OrderDetailDto } from '../types/order' |
||||||
|
|
||||||
|
/** 归一化单条订单记录(列表项) */ |
||||||
|
export const normalizeOrderRecordItem = (raw: Record<string, any>): OrderRecordItem => ({ |
||||||
|
id: toNumber(pickField(raw, 'id')), |
||||||
|
title: String(pickField(raw, 'title') ?? ''), |
||||||
|
statusText: String(pickField(raw, 'statusText', 'statustext') ?? ''), |
||||||
|
bpName: String(pickField(raw, 'bpName', 'bpname') ?? ''), |
||||||
|
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''), |
||||||
|
durationText: String(pickField(raw, 'durationText', 'durationtext') ?? ''), |
||||||
|
actualAmount: toNumber(pickField(raw, 'actualAmount', 'actualamount')), |
||||||
|
}) |
||||||
|
|
||||||
|
/** 归一化订单记录分页响应 */ |
||||||
|
export const normalizeOrderRecordPagedResult = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): OrderRecordPagedResult => { |
||||||
|
const src = raw || {} |
||||||
|
const list = pickField<Record<string, any>[]>(src, 'data') || [] |
||||||
|
return { |
||||||
|
data: Array.isArray(list) ? list.map(normalizeOrderRecordItem) : [], |
||||||
|
totalCount: toNumber(pickField(src, 'totalCount', 'totalcount')), |
||||||
|
pageIndex: toNumber(pickField(src, 'pageIndex', 'pageindex'), 1), |
||||||
|
pageSize: toNumber(pickField(src, 'pageSize', 'pagesize'), 10), |
||||||
|
totalPages: toNumber(pickField(src, 'totalPages', 'totalpages')), |
||||||
|
hasPrevious: Boolean(pickField(src, 'hasPrevious', 'hasprevious') ?? false), |
||||||
|
hasNext: Boolean(pickField(src, 'hasNext', 'hasnext') ?? false), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 归一化订单详情 */ |
||||||
|
export const normalizeOrderDetail = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): OrderDetailDto => { |
||||||
|
const src = raw || {} |
||||||
|
const memberPrice = pickField<number | null>(src, 'memberPrice', 'memberprice') |
||||||
|
const couponDeduction = pickField<number | null>(src, 'couponDeduction', 'coupondeduction') |
||||||
|
const couponDescription = pickField<string | null>(src, 'couponDescription', 'coupondescription') |
||||||
|
const actualAmount = pickField<number | null>(src, 'actualAmount', 'actualamount') |
||||||
|
return { |
||||||
|
id: toNumber(pickField(src, 'id')), |
||||||
|
projectName: String(pickField(src, 'projectName', 'projectname') ?? ''), |
||||||
|
statusText: String(pickField(src, 'statusText', 'statustext') ?? ''), |
||||||
|
storePhone: String(pickField(src, 'storePhone', 'storephone') ?? ''), |
||||||
|
durationText: String(pickField(src, 'durationText', 'durationtext') ?? ''), |
||||||
|
actualAmount: actualAmount === null || actualAmount === undefined ? null : toNumber(actualAmount), |
||||||
|
pricePerHour: toNumber(pickField(src, 'pricePerHour', 'priceperhour')), |
||||||
|
discountPricePerHour: toNumber(pickField(src, 'discountPricePerHour', 'discountpriceperhour')), |
||||||
|
onlineTotalPayment: toNumber(pickField(src, 'onlineTotalPayment', 'onlinetotalpayment')), |
||||||
|
deposit: toNumber(pickField(src, 'deposit')), |
||||||
|
originalAmount: toNumber(pickField(src, 'originalAmount', 'originalamount')), |
||||||
|
memberPrice: memberPrice === null || memberPrice === undefined ? null : toNumber(memberPrice), |
||||||
|
couponDeduction: |
||||||
|
couponDeduction === null || couponDeduction === undefined ? null : toNumber(couponDeduction), |
||||||
|
couponDescription: couponDescription === null || couponDescription === undefined ? null : String(couponDescription), |
||||||
|
bpName: String(pickField(src, 'bpName', 'bpname') ?? ''), |
||||||
|
bpAddress: String(pickField(src, 'bpAddress', 'bpaddress') ?? ''), |
||||||
|
tableName: String(pickField(src, 'tableName', 'tablename') ?? ''), |
||||||
|
startTime: String(pickField(src, 'startTime', 'starttime') ?? ''), |
||||||
|
stopTime: String(pickField(src, 'stopTime', 'stoptime') ?? ''), |
||||||
|
outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''), |
||||||
|
chargingStandard: String(pickField(src, 'chargingStandard', 'chargingstandard') ?? ''), |
||||||
|
payTypeName: String(pickField(src, 'payTypeName', 'paytypename') ?? ''), |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
/** |
||||||
|
* 充值相关字段归一化 |
||||||
|
*/ |
||||||
|
import { |
||||||
|
RechargePaymentResponseDto, |
||||||
|
RechargeWechatPayParameters, |
||||||
|
RechargeStatusResult, |
||||||
|
RechargeRuleItem, |
||||||
|
RechargeRuleQueryResult, |
||||||
|
} from '../types/recharge' |
||||||
|
import { pickField, toNumber } from './apiNormalize' |
||||||
|
|
||||||
|
export const normalizeWechatPayParams = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): RechargeWechatPayParameters | null => { |
||||||
|
if (!raw) return null |
||||||
|
const timeStamp = pickField<string>(raw, 'timeStamp', 'timestamp') |
||||||
|
if (!timeStamp) return null |
||||||
|
return { |
||||||
|
timeStamp: String(timeStamp), |
||||||
|
nonceStr: String(pickField(raw, 'nonceStr', 'noncestr') ?? ''), |
||||||
|
package: String(pickField(raw, 'package') ?? ''), |
||||||
|
signType: String(pickField(raw, 'signType', 'signtype') ?? 'RSA'), |
||||||
|
paySign: String(pickField(raw, 'paySign', 'paysign') ?? ''), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const normalizeRechargePaymentResponse = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): RechargePaymentResponseDto => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''), |
||||||
|
channel: toNumber(pickField(src, 'channel')), |
||||||
|
payParameters: normalizeWechatPayParams( |
||||||
|
pickField<Record<string, any>>(src, 'payParameters', 'payparameters') |
||||||
|
), |
||||||
|
payUrl: String(pickField(src, 'payUrl', 'payurl') ?? ''), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const normalizeRechargeStatus = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): RechargeStatusResult => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
amount: toNumber(pickField(src, 'amount')), |
||||||
|
giftAmount: toNumber(pickField(src, 'giftAmount', 'giftamount')), |
||||||
|
balance: toNumber(pickField(src, 'balance')), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const normalizeRechargeRuleItem = (raw: Record<string, any>): RechargeRuleItem => ({ |
||||||
|
recharge: toNumber(pickField(raw, 'recharge')), |
||||||
|
gift: toNumber(pickField(raw, 'gift')), |
||||||
|
desc: String(pickField(raw, 'desc', 'description') ?? ''), |
||||||
|
}) |
||||||
|
|
||||||
|
export const normalizeRechargeRules = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): RechargeRuleQueryResult => { |
||||||
|
const src = raw || {} |
||||||
|
const rules = pickField<Record<string, any>[]>(src, 'rules') || [] |
||||||
|
return { |
||||||
|
instName: String(pickField(src, 'instName', 'instname') ?? ''), |
||||||
|
bpName: String(pickField(src, 'bpName', 'bpname') ?? ''), |
||||||
|
rules: Array.isArray(rules) ? rules.map(normalizeRechargeRuleItem) : [], |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,111 @@ |
|||||||
|
/** |
||||||
|
* 充值记录:月份区间、状态文案、列表展示映射 |
||||||
|
*/ |
||||||
|
import { getRechargeRecords } from '../api/recharge' |
||||||
|
import { RechargeRecordItem } from '../types/recharge' |
||||||
|
import { userStore } from '../store' |
||||||
|
|
||||||
|
/** 列表项(页面展示结构) */ |
||||||
|
export interface RechargeRecordViewItem { |
||||||
|
id: string |
||||||
|
storeName: string |
||||||
|
rechargeTime: string |
||||||
|
rechargeType: string |
||||||
|
amount: number |
||||||
|
status: string |
||||||
|
} |
||||||
|
|
||||||
|
/** 当前年月 YYYY-MM */ |
||||||
|
export const getCurrentYearMonth = (): string => { |
||||||
|
const now = new Date() |
||||||
|
const year = now.getFullYear() |
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0') |
||||||
|
return `${year}-${month}` |
||||||
|
} |
||||||
|
|
||||||
|
/** 某月起止时间(供 records 接口过滤) */ |
||||||
|
export const getMonthDateRange = (yearMonth: string): { startDate: string; endDate: string } => { |
||||||
|
const [yStr, mStr] = yearMonth.split('-') |
||||||
|
const year = Number(yStr) |
||||||
|
const month = Number(mStr) |
||||||
|
const lastDay = new Date(year, month, 0).getDate() |
||||||
|
const day = String(lastDay).padStart(2, '0') |
||||||
|
return { |
||||||
|
startDate: `${yearMonth}-01 00:00:00`, |
||||||
|
endDate: `${yearMonth}-${day} 23:59:59`, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 支付状态(后端枚举,适用于 recharge/records.status 等) |
||||||
|
* 来源:后端「支付状态定义」 |
||||||
|
*/ |
||||||
|
export enum RechargePaymentStatus { |
||||||
|
Invalid = 0, |
||||||
|
Pending = 1, |
||||||
|
Initialized = 2, |
||||||
|
Paying = 3, |
||||||
|
Success = 4, |
||||||
|
Failed = 5, |
||||||
|
Cancelled = 6, |
||||||
|
Refunding = 7, |
||||||
|
Refunded = 8, |
||||||
|
RefundFailed = 9, |
||||||
|
Closed = 10, |
||||||
|
} |
||||||
|
|
||||||
|
export const RECHARGE_PAYMENT_STATUS_LABEL: Record<number, string> = { |
||||||
|
[RechargePaymentStatus.Invalid]: '无效', |
||||||
|
[RechargePaymentStatus.Pending]: '待支付', |
||||||
|
[RechargePaymentStatus.Initialized]: '已初始化', |
||||||
|
[RechargePaymentStatus.Paying]: '支付中', |
||||||
|
[RechargePaymentStatus.Success]: '支付成功', |
||||||
|
[RechargePaymentStatus.Failed]: '支付失败', |
||||||
|
[RechargePaymentStatus.Cancelled]: '已取消', |
||||||
|
[RechargePaymentStatus.Refunding]: '退款中', |
||||||
|
[RechargePaymentStatus.Refunded]: '已退款', |
||||||
|
[RechargePaymentStatus.RefundFailed]: '退款失败', |
||||||
|
[RechargePaymentStatus.Closed]: '已关闭', |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 充值状态文案(后端支付状态定义) |
||||||
|
*/ |
||||||
|
export const formatRechargeStatus = (status: number): string => { |
||||||
|
if (RECHARGE_PAYMENT_STATUS_LABEL[status] !== undefined) { |
||||||
|
return RECHARGE_PAYMENT_STATUS_LABEL[status] |
||||||
|
} |
||||||
|
return '未知状态' |
||||||
|
} |
||||||
|
|
||||||
|
export const mapRechargeRecordToView = (item: RechargeRecordItem): RechargeRecordViewItem => ({ |
||||||
|
id: String(item.id || item.outTradeNo || item.logTime), |
||||||
|
storeName: item.bpName || '--', |
||||||
|
rechargeTime: item.logTime || '--', |
||||||
|
rechargeType: item.rcgTypeName || '账户充值', |
||||||
|
amount: item.amount, |
||||||
|
status: formatRechargeStatus(item.status), |
||||||
|
}) |
||||||
|
|
||||||
|
/** |
||||||
|
* 拉取指定月份充值记录(需已登录且有客户 sn) |
||||||
|
*/ |
||||||
|
export const fetchRechargeRecordsByMonth = async ( |
||||||
|
yearMonth: string |
||||||
|
): Promise<RechargeRecordViewItem[]> => { |
||||||
|
const customerSn = userStore.userInfo?.sn |
||||||
|
if (!customerSn) { |
||||||
|
throw new Error('NO_CUSTOMER_SN') |
||||||
|
} |
||||||
|
|
||||||
|
const { startDate, endDate } = getMonthDateRange(yearMonth) |
||||||
|
const res = await getRechargeRecords({ |
||||||
|
customerSn, |
||||||
|
startDate, |
||||||
|
endDate, |
||||||
|
pageIndex: 1, |
||||||
|
pageSize: 100, |
||||||
|
}) |
||||||
|
|
||||||
|
return (res.data?.data || []).map(mapRechargeRecordToView) |
||||||
|
} |
||||||
@ -0,0 +1,119 @@ |
|||||||
|
/** |
||||||
|
* 扫码(qrcode)响应字段归一化 |
||||||
|
*/ |
||||||
|
import { |
||||||
|
TableScanResponseDto, |
||||||
|
TableInfoDto, |
||||||
|
PriceInfoDto, |
||||||
|
TimePriceDto, |
||||||
|
OptionDto, |
||||||
|
OpInfoDto, |
||||||
|
} from '../types/startDevice' |
||||||
|
import { pickField, toNumber } from './apiNormalize' |
||||||
|
|
||||||
|
const normalizeTimePrice = (raw: Record<string, any>): TimePriceDto => ({ |
||||||
|
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''), |
||||||
|
endTime: String(pickField(raw, 'endTime', 'endtime') ?? ''), |
||||||
|
price: toNumber(pickField(raw, 'price')), |
||||||
|
description: String(pickField(raw, 'description') ?? ''), |
||||||
|
}) |
||||||
|
|
||||||
|
const normalizePrice = (raw: Record<string, any> | null | undefined): PriceInfoDto => { |
||||||
|
const src = raw || {} |
||||||
|
const timePrices = pickField<Record<string, any>[]>(src, 'timePrices', 'timeprices') || [] |
||||||
|
return { |
||||||
|
standard: toNumber(pickField(src, 'standard')), |
||||||
|
memberPrice: toNumber(pickField(src, 'memberPrice', 'memberprice')), |
||||||
|
unit: String(pickField(src, 'unit') ?? ''), |
||||||
|
deposit: toNumber(pickField(src, 'deposit')), |
||||||
|
pricePerMinute: toNumber(pickField(src, 'pricePerMinute', 'pricePerminute', 'priceperminute')), |
||||||
|
memberPricePerMinute: toNumber( |
||||||
|
pickField(src, 'memberPricePerMinute', 'memberpriceperminute') |
||||||
|
), |
||||||
|
currentPrice: toNumber(pickField(src, 'currentPrice', 'currentprice')), |
||||||
|
descript: String(pickField(src, 'descript') ?? ''), |
||||||
|
pricePeriod: String(pickField(src, 'pricePeriod', 'priceperiod') ?? ''), |
||||||
|
timePrices: Array.isArray(timePrices) ? timePrices.map(normalizeTimePrice) : [], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const normalizeTableInfo = (raw: Record<string, any> | null | undefined): TableInfoDto => { |
||||||
|
const src = raw || {} |
||||||
|
return { |
||||||
|
id: toNumber(pickField(src, 'id')), |
||||||
|
sn: String(pickField(src, 'sn', 'grpsn') ?? ''), |
||||||
|
name: String(pickField(src, 'name', 'tablename') ?? ''), |
||||||
|
tableNumber: String(pickField(src, 'tableNumber', 'tablenumber', 'number') ?? ''), |
||||||
|
status: toNumber(pickField(src, 'status', 'wstatus')), |
||||||
|
statusDescription: String( |
||||||
|
pickField(src, 'statusDescription', 'statusdescription', 'statusdesc') ?? '' |
||||||
|
), |
||||||
|
isShared: Boolean(pickField(src, 'isShared', 'isshared', 'share')), |
||||||
|
currentSession: String(pickField(src, 'currentSession', 'currentsession') ?? ''), |
||||||
|
area: String(pickField(src, 'area', 'class') ?? ''), |
||||||
|
chanNum: toNumber(pickField(src, 'chanNum', 'channum')), |
||||||
|
startChan: toNumber(pickField(src, 'startChan', 'startchan')), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const normalizeOpenOption = (raw: Record<string, any>): OptionDto => ({ |
||||||
|
title: String(pickField(raw, 'title') ?? ''), |
||||||
|
type: toNumber(pickField(raw, 'type')), |
||||||
|
description: String(pickField(raw, 'description', 'desc') ?? ''), |
||||||
|
actionText: String(pickField(raw, 'actionText', 'actiontext') ?? ''), |
||||||
|
refundPolicy: String(pickField(raw, 'refundPolicy', 'refundpolicy') ?? ''), |
||||||
|
available: pickField(raw, 'available') !== false, |
||||||
|
optionCode: toNumber(pickField(raw, 'optionCode', 'optioncode', 'type')), |
||||||
|
icon: String(pickField(raw, 'icon') ?? ''), |
||||||
|
condition: String(pickField(raw, 'condition') ?? ''), |
||||||
|
actionUrl: String(pickField(raw, 'actionUrl', 'actionurl') ?? ''), |
||||||
|
}) |
||||||
|
|
||||||
|
const normalizeOpInfo = (raw: Record<string, any> | null | undefined): OpInfoDto | null => { |
||||||
|
if (!raw) return null |
||||||
|
return { |
||||||
|
opId: toNumber(pickField(raw, 'opId', 'opid')), |
||||||
|
orderNo: String(pickField(raw, 'orderNo', 'orderno') ?? ''), |
||||||
|
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''), |
||||||
|
usedTime: String(pickField(raw, 'usedTime', 'usedtime') ?? ''), |
||||||
|
usedMinutes: toNumber(pickField(raw, 'usedMinutes', 'usedminutes')), |
||||||
|
ctrlType: toNumber(pickField(raw, 'ctrlType', 'ctrltype')), |
||||||
|
ctrlTypeDescription: String( |
||||||
|
pickField(raw, 'ctrlTypeDescription', 'ctrltypedescription') ?? '' |
||||||
|
), |
||||||
|
payType: toNumber(pickField(raw, 'payType', 'paytype')), |
||||||
|
payTypeDescription: String( |
||||||
|
pickField(raw, 'payTypeDescription', 'paytypedescription') ?? '' |
||||||
|
), |
||||||
|
currentAmount: toNumber(pickField(raw, 'currentAmount', 'currentamount')), |
||||||
|
deposit: toNumber(pickField(raw, 'deposit')), |
||||||
|
depositStatus: String(pickField(raw, 'depositStatus', 'depositstatus') ?? ''), |
||||||
|
discountAmount: toNumber(pickField(raw, 'discountAmount', 'discountamount')), |
||||||
|
payableAmount: toNumber(pickField(raw, 'payableAmount', 'payableamount')), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const normalizeTableScanResponse = ( |
||||||
|
raw: Record<string, any> | null | undefined |
||||||
|
): TableScanResponseDto => { |
||||||
|
const src = raw || {} |
||||||
|
const openOptions = pickField<Record<string, any>[]>(src, 'openOptions', 'openoptions') || [] |
||||||
|
const devInfo = pickField<Record<string, any>>(src, 'tableInfo', 'devinfo', 'tableinfo') |
||||||
|
|
||||||
|
return { |
||||||
|
scanType: (pickField<string>(src, 'scanType', 'scantype') || 'open') as 'open' | 'close', |
||||||
|
// 后端默认 true(本人开台),只有明确返回 false 时才判定为"别人开的台"
|
||||||
|
isOwner: pickField(src, 'isOwner', 'isowner') !== false, |
||||||
|
shopName: String(pickField(src, 'shopName', 'bpname', 'shopname') ?? ''), |
||||||
|
shopCode: String(pickField(src, 'shopCode', 'bpsn', 'shopcode') ?? ''), |
||||||
|
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''), |
||||||
|
phone: String(pickField(src, 'phone') ?? ''), |
||||||
|
tableInfo: normalizeTableInfo(devInfo), |
||||||
|
price: normalizePrice(pickField(src, 'price')), |
||||||
|
businessHours: String(pickField(src, 'businessHours', 'businesshours') ?? ''), |
||||||
|
openOptions: Array.isArray(openOptions) ? openOptions.map(normalizeOpenOption) : [], |
||||||
|
recommendedOption: String(pickField(src, 'recommendedOption', 'recommendedoption') ?? ''), |
||||||
|
scanTime: String(pickField(src, 'scanTime', 'scantime') ?? ''), |
||||||
|
opInfo: normalizeOpInfo(pickField(src, 'opInfo', 'opinfo')), |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
/** |
||||||
|
* 门店列表字段归一化(新接口 BpInfoSimpleDto → 现网 StoreInfoDto) |
||||||
|
*/ |
||||||
|
import { StoreInfoDto } from '../api/basicInfo' |
||||||
|
import { pickField, toNumber } from './apiNormalize' |
||||||
|
|
||||||
|
const hashSn = (sn: string): number => { |
||||||
|
let h = 0 |
||||||
|
for (let i = 0; i < sn.length; i++) { |
||||||
|
h = (Math.imul(31, h) + sn.charCodeAt(i)) | 0 |
||||||
|
} |
||||||
|
return Math.abs(h) |
||||||
|
} |
||||||
|
|
||||||
|
export const normalizeStoreItem = (raw: Record<string, any>): StoreInfoDto => { |
||||||
|
const inst = pickField<Record<string, any>>(raw, 'inst') || {} |
||||||
|
const sn = String(pickField(raw, 'sn') ?? '') |
||||||
|
return { |
||||||
|
id: hashSn(sn || String(pickField(raw, 'id') ?? '')), |
||||||
|
sn, |
||||||
|
name: String(pickField(raw, 'name') ?? ''), |
||||||
|
type: toNumber(pickField(raw, 'type')), |
||||||
|
typeName: String(pickField(raw, 'typeName', 'typename') ?? ''), |
||||||
|
isUnion: toNumber(pickField(raw, 'isUnion', 'isunion')), |
||||||
|
address: String(pickField(raw, 'address', 'add') ?? ''), |
||||||
|
manager: String(pickField(raw, 'manager') ?? ''), |
||||||
|
phone: String(pickField(raw, 'phone') ?? ''), |
||||||
|
inst_Sn: String(pickField(inst, 'sn', 'instsn') ?? pickField(raw, 'instSn', 'instsn') ?? ''), |
||||||
|
instName: String(pickField(inst, 'name', 'instname') ?? pickField(raw, 'instName', 'instname') ?? ''), |
||||||
|
rgn_Sn: String(pickField(raw, 'rgn_Sn', 'rgnsn') ?? ''), |
||||||
|
regionName: String(pickField(raw, 'regionName', 'regionname', 'city') ?? pickField(raw, 'city') ?? ''), |
||||||
|
longItude: toNumber(pickField(raw, 'longItude', 'longitude')), |
||||||
|
latItude: toNumber(pickField(raw, 'latItude', 'latitude')), |
||||||
|
createTime: String(pickField(raw, 'createTime', 'createtime') ?? ''), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const normalizeStoreList = (list: Record<string, any>[] | null | undefined): StoreInfoDto[] => { |
||||||
|
if (!Array.isArray(list)) return [] |
||||||
|
return list.map(normalizeStoreItem) |
||||||
|
} |
||||||
Loading…
Reference in new issue