33 changed files with 1625 additions and 446 deletions
@ -1,49 +1,271 @@
@@ -1,49 +1,271 @@
|
||||
/** |
||||
|
||||
* 客户信息相关 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 { normalizeCustomer } 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 } |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* 根据 OpenId 获取客户信息(含最新余额、会员类型等) |
||||
* 登录后或页面刷新时调用,比 getCustomerBySn 更通用(无需 sn) |
||||
* @param openId 微信 OpenId |
||||
|
||||
* 刷新用户资料 + 余额(有 bpSn 时) |
||||
|
||||
*/ |
||||
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 字符串 |
||||
* 用于将选择的头像图片编码后传给更新接口 |
||||
* @param filePath 本地临时文件路径(如 wx.chooseImage 返回的路径) |
||||
* @returns Base64 字符串(不含 data:image/xxx;base64, 前缀) |
||||
|
||||
* @deprecated 新接口使用 multipart avatarFilePath |
||||
|
||||
*/ |
||||
|
||||
export const readImageAsBase64 = (filePath: string): Promise<string> => { |
||||
return new Promise((resolve, reject) => { |
||||
const fs = wx.getFileSystemManager() |
||||
const fs = Taro.getFileSystemManager() |
||||
fs.readFile({ |
||||
filePath, |
||||
encoding: 'base64', |
||||
success: (res) => resolve(res.data as string), |
||||
fail: (err) => reject(err), |
||||
fail: (err: TaroGeneral.CallbackResult) => reject(err), |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
@@ -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 @@
@@ -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 @@
@@ -1,66 +1,126 @@
|
||||
import { PropsWithChildren } from 'react' |
||||
|
||||
import { useLaunch } from '@tarojs/taro' |
||||
|
||||
import Taro from '@tarojs/taro' |
||||
|
||||
import { userStore } from './store' |
||||
import { getCustomerByOpenId } from './api/customer' |
||||
|
||||
import { refreshCustomerProfile } from './api/customer' |
||||
|
||||
import { wechatLogin } from './api/login' |
||||
|
||||
import './app.scss' |
||||
|
||||
|
||||
|
||||
function App({ children }: PropsWithChildren<any>) { |
||||
|
||||
useLaunch(() => { |
||||
|
||||
console.log('台球馆小程序启动') |
||||
|
||||
|
||||
|
||||
// 先从本地缓存恢复用户信息(快速恢复登录态)
|
||||
|
||||
try { |
||||
|
||||
const savedUserInfo = Taro.getStorageSync('userInfo') |
||||
|
||||
if (savedUserInfo) { |
||||
|
||||
userStore.setUserInfo(savedUserInfo) |
||||
|
||||
} |
||||
|
||||
} catch (error) { |
||||
|
||||
console.warn('初始化用户信息失败:', error) |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') |
||||
|
||||
|
||||
|
||||
if (openId) { |
||||
|
||||
// 已有 openId:后台静默刷新最新用户信息(余额、会员等),不阻塞启动
|
||||
getCustomerByOpenId(openId) |
||||
.then(res => { |
||||
if (res.data) { |
||||
userStore.setUserInfoFromWechat(res.data) |
||||
} |
||||
}) |
||||
|
||||
refreshCustomerProfile(openId) |
||||
|
||||
.catch(e => console.warn('启动刷新用户信息失败:', e)) |
||||
|
||||
.finally(() => userStore.setLoginReady()) |
||||
|
||||
} else { |
||||
|
||||
// 无本地 openId:执行无感登录,wx.login() 不需要用户任何操作
|
||||
|
||||
Taro.login({ |
||||
|
||||
success: async ({ code }) => { |
||||
|
||||
try { |
||||
|
||||
const res = await wechatLogin(code) |
||||
if (res.success && res.data) { |
||||
|
||||
if (res.success && res.data?.customer) { |
||||
|
||||
userStore.setUserInfoFromWechat(res.data.customer) |
||||
// 新用户:触发头像/昵称补全弹窗
|
||||
|
||||
if (res.data.isNewCustomer) { |
||||
|
||||
userStore.setIsNewUser(true) |
||||
|
||||
} |
||||
|
||||
const wxOpenId = res.data.customer.wxOpenId |
||||
|
||||
if (wxOpenId) { |
||||
|
||||
await refreshCustomerProfile(wxOpenId) |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} catch (e) { |
||||
|
||||
console.warn('无感登录失败:', e) |
||||
|
||||
} finally { |
||||
// 无论成功失败,都标记登录流程结束,让 qrHandler 继续处理
|
||||
|
||||
userStore.setLoginReady() |
||||
|
||||
} |
||||
|
||||
}, |
||||
|
||||
fail: (err) => { |
||||
|
||||
console.warn('wx.login 调用失败:', err) |
||||
// 登录失败也要标记结束,避免 qrHandler 永久等待
|
||||
|
||||
userStore.setLoginReady() |
||||
|
||||
} |
||||
|
||||
}) |
||||
|
||||
} |
||||
|
||||
}) |
||||
|
||||
|
||||
|
||||
return children |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
export default App |
||||
|
||||
|
||||
@ -0,0 +1,91 @@
@@ -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 @@
@@ -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 @@
@@ -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,40 @@
@@ -0,0 +1,40 @@
|
||||
/** |
||||
* 客户字段归一化(新接口全小写 / 旧 camelCase) |
||||
*/ |
||||
import { CustomerDto } 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'), |
||||
} |
||||
} |
||||
|
||||
/** 登录 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 @@
@@ -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,69 @@
@@ -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,117 @@
@@ -0,0 +1,117 @@
|
||||
/** |
||||
* 扫码(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', |
||||
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 @@
@@ -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