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 @@
@@ -1,49 +1,295 @@
|
||||
/** |
||||
|
||||
* 客户信息相关 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, 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 获取客户信息(含最新余额、会员类型等) |
||||
* 登录后或页面刷新时调用,比 getCustomerBySn 更通用(无需 sn) |
||||
* @param openId 微信 OpenId |
||||
|
||||
* 根据 OpenId 获取客户信息 |
||||
|
||||
*/ |
||||
export const getCustomerByOpenId = (openId: string) => { |
||||
return get<CustomerDto>(`/Customer/${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>) } |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* 根据客户 SN 获取客户详情(含最新余额、会员类型等) |
||||
* @param sn 客户编码(CustomerDto.sn) |
||||
|
||||
* 查询客户在指定门店的余额 |
||||
|
||||
*/ |
||||
export const getCustomerBySn = (sn: string) => { |
||||
return get<CustomerDto>(`/Customer/sn/${sn}`) |
||||
|
||||
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 } |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* 更新客户信息(昵称、头像、手机号、微信号等) |
||||
* @param customerId 客户 ID(CustomerDto.id) |
||||
* @param dto 要更新的字段 |
||||
|
||||
* 查询客户在所有已消费门店的余额列表(含积分/存杆柜数量/VIP到期时间) |
||||
|
||||
* 对应接口:GET /customer/balancelist/{openId} |
||||
|
||||
* 用于 pagesOrder/accountBalance 的"账户余额" Tab(BalanceList 组件) |
||||
|
||||
*/ |
||||
export const updateCustomer = (customerId: number, dto: UpdateCustomerDto) => { |
||||
return put<void>(`/Customer/${customerId}`, dto) |
||||
|
||||
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>[]) } |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
* 读取本地图片并转为 Base64 字符串 |
||||
* 用于将选择的头像图片编码后传给更新接口 |
||||
* @param filePath 本地临时文件路径(如 wx.chooseImage 返回的路径) |
||||
* @returns Base64 字符串(不含 data:image/xxx;base64, 前缀) |
||||
|
||||
* 刷新用户资料 + 余额(有 bpSn 时) |
||||
|
||||
*/ |
||||
|
||||
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 |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
|
||||
* multipart 上传头像(小程序 uploadFile 仅支持 POST) |
||||
|
||||
*/ |
||||
|
||||
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, |
||||
|
||||
}) |
||||
|
||||
}) |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
|
||||
* 更新客户信息(昵称、头像、手机号等) |
||||
|
||||
*/ |
||||
|
||||
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) } |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
/** |
||||
|
||||
* @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 |
||||
|
||||
|
||||
@ -1,150 +0,0 @@
@@ -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 @@
@@ -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,58 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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