Browse Source

对接部分接口

v2.0
DU 3 months ago
parent
commit
3282fd7bbd
  1. 96
      src/api/basicInfo.ts
  2. 264
      src/api/customer.ts
  3. 26
      src/api/login.ts
  4. 66
      src/api/magdev.ts
  5. 3
      src/api/order.ts
  6. 60
      src/api/payment.ts
  7. 147
      src/api/recharge.ts
  8. 6
      src/api/startDevice.ts
  9. 82
      src/app.ts
  10. 79
      src/pageScan/closeTable/components/FooterButtons.tsx
  11. 7
      src/pageScan/closeTable/index.tsx
  12. 72
      src/pageScan/couponStart/index.tsx
  13. 63
      src/pageScan/renewFee/index.tsx
  14. 78
      src/pageScan/selfStart/index.tsx
  15. 88
      src/pageScan/startTable/index.tsx
  16. 69
      src/pageScan/timedStart/index.tsx
  17. 67
      src/pageScan/turnOffRestart/index.tsx
  18. 17
      src/pages/index/index.tsx
  19. 4
      src/pages/store/index.tsx
  20. 7
      src/pages/user/index.tsx
  21. 89
      src/pagesOrder/recharge/index.tsx
  22. 29
      src/pagesUser/editProfile/index.tsx
  23. 34
      src/types/login.ts
  24. 91
      src/types/magdev.ts
  25. 82
      src/types/recharge.ts
  26. 4
      src/types/startDevice.ts
  27. 24
      src/utils/apiNormalize.ts
  28. 15
      src/utils/callShopPhone.ts
  29. 40
      src/utils/customerNormalize.ts
  30. 135
      src/utils/operateNormalize.ts
  31. 69
      src/utils/rechargeNormalize.ts
  32. 117
      src/utils/scanNormalize.ts
  33. 41
      src/utils/storeNormalize.ts

96
src/api/basicInfo.ts

@ -1,8 +1,10 @@
/** /**
* 基础信息相关 API * 基础信息相关 API
* 接口地址:/api/v2/BasicInfo/ * 新接口路径:/api/v2/basicinfo/
*/ */
import { get } from '../services/request' import { get } from '../services/request'
import { normalizeStoreList } from '../utils/storeNormalize'
import { pickField, toNumber } from '../utils/apiNormalize'
/** /**
* 门店(场所)信息 * 门店(场所)信息
@ -28,9 +30,95 @@ export interface StoreInfoDto {
createTime: string createTime: string
} }
export interface InstInfoDto {
name: string
sn: string
address: string
phone: string
city: string
longitude: number
latitude: number
}
export interface BpDevInfoDto {
name: string
sn: string
price: number
type: number
class: string
status: number
wstatus: number
share: number
}
export interface BpDevQueryResultDto {
name: string
devs: BpDevInfoDto[]
}
export interface GetStoreListParams {
sn?: string
/** 0=店SN,1=机构SN */
type?: 0 | 1
}
const normalizeInstItem = (raw: Record<string, unknown>): InstInfoDto => ({
name: String(pickField(raw, 'name') ?? ''),
sn: String(pickField(raw, 'sn', 'instsn') ?? ''),
address: String(pickField(raw, 'address', 'add') ?? ''),
phone: String(pickField(raw, 'phone') ?? ''),
city: String(pickField(raw, 'city') ?? ''),
longitude: toNumber(pickField(raw, 'longitude', 'longItude')),
latitude: toNumber(pickField(raw, 'latitude', 'latItude')),
})
const normalizeDevItem = (raw: Record<string, unknown>): BpDevInfoDto => ({
name: String(pickField(raw, 'name') ?? ''),
sn: String(pickField(raw, 'sn') ?? ''),
price: toNumber(pickField(raw, 'price')),
type: toNumber(pickField(raw, 'type')),
class: String(pickField(raw, 'class') ?? ''),
status: toNumber(pickField(raw, 'status')),
wstatus: toNumber(pickField(raw, 'wstatus')),
share: toNumber(pickField(raw, 'share')),
})
/**
* 获取门店列表
*/
export const getStoreList = async (params?: GetStoreListParams) => {
const query: Record<string, string | number> = {}
if (params?.sn) query.sn = params.sn
if (params?.type !== undefined) query.type = params.type
const res = await get<Record<string, unknown>[]>('/basicinfo/getallbpinfo', query)
return { ...res, data: normalizeStoreList(res.data as Record<string, unknown>[]) }
}
/**
* 获取机构列表
*/
export const getInstList = async (instSn?: string) => {
const res = await get<Record<string, unknown>[]>(
'/basicinfo/getinst',
instSn ? { instsn: instSn } : undefined
)
const list = Array.isArray(res.data) ? res.data : []
return { ...res, data: list.map(normalizeInstItem) }
}
/** /**
* 获取所有场所(门店)列表 * 查询门店下营运设备与价格
* @param sn 球台 SN 或店 SN
* @param type 0=球台SN,1=店SN
*/ */
export const getStoreList = () => { export const queryBpDevices = async (sn: string, type: 0 | 1) => {
return get<StoreInfoDto[]>('/BasicInfo/GetAllBpInfo') const res = await get<Record<string, unknown>>('/basicinfo/query', { sn, type })
const src = res.data || {}
const devs = pickField<Record<string, unknown>[]>(src as Record<string, unknown>, 'devs') || []
const data: BpDevQueryResultDto = {
name: String(pickField(src as Record<string, unknown>, 'name') ?? ''),
devs: Array.isArray(devs) ? devs.map(normalizeDevItem) : [],
}
return { ...res, data }
} }

264
src/api/customer.ts

@ -1,49 +1,271 @@
/** /**
* 客户信息相关 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 } 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) * 刷新用户资料 + 余额(有 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),
}) })
}) })
} }

26
src/api/login.ts

@ -3,17 +3,27 @@
*/ */
import request from '../services/request' import request from '../services/request'
import { WechatLoginRequest, WechatAuthResponse } from '../types/login' import { WechatLoginRequest, WechatAuthResponse } from '../types/login'
import { normalizeLoginData } from '../utils/customerNormalize'
/** /**
* 微信登录 * 微信登录:code → openId + 是否新用户(资料建议再调 byopenid)
* @param code 微信登录凭证
*/ */
export const wechatLogin = (code: string): Promise<WechatAuthResponse> => { export const wechatLogin = async (code: string): Promise<WechatAuthResponse> => {
const data: WechatLoginRequest = { code } const data: WechatLoginRequest = { code }
return request({ const res = await request({
url: '/WechatAuth/login-wechat', url: '/wechatauth/login-wechat',
method: 'POST', method: 'POST',
data data,
}) })
}
const normalized = normalizeLoginData(res.data as Record<string, any>)
return {
...res,
data: {
customer: normalized.customer,
isNewCustomer: normalized.isNewCustomer,
message: normalized.message,
},
}
}

66
src/api/magdev.ts

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

3
src/api/order.ts

@ -13,8 +13,7 @@ export const queryPaymentResult = (dto: QueryPaymentRequest) => {
} }
/** /**
* 关闭支付订单(用户取消或支付失败时调用,避免产生悬挂订单) * @deprecated 新接口不对接关单,取消支付由后端自行处理
* @param outTradeNo 商户订单号
*/ */
export const closeOrder = (outTradeNo: string) => { export const closeOrder = (outTradeNo: string) => {
return post<boolean>('/Order/close', { outTradeNo } as CloseOrderDto) return post<boolean>('/Order/close', { outTradeNo } as CloseOrderDto)

60
src/api/payment.ts

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

147
src/api/recharge.ts

@ -1,54 +1,153 @@
/** /**
* 充值相关 API * 充值相关 API
*/ */
import { get, post } from '../services/request' import request, { get, post } from '../services/request'
import { import {
CreateRechargePaymentDto, CreateRechargePaymentDto,
RechargePaymentResponseDto,
RechargeWechatPayParameters, RechargeWechatPayParameters,
PaymentChannel, RechargeStatusResult,
RechargeRecordQueryParams,
RechargeRecordPagedResult,
} from '../types/recharge' } from '../types/recharge'
import {
normalizeRechargePaymentResponse,
normalizeRechargeRules,
normalizeRechargeStatus,
normalizeWechatPayParams,
} from '../utils/rechargeNormalize'
import { pickField, toNumber } from '../utils/apiNormalize'
/** 小程序 JSAPI 支付固定交易类型 */ /** 小程序 JSAPI 支付 */
const MINI_PROGRAM_TRADE_TYPE = 1 const PAY_TYPE_WECHAT = 1
const TRADE_TYPE_JSAPI = 1
/** /**
* 创建充值订单 * 创建充值订单
* @param dto 充值请求参数
* @returns 充值支付响应(含微信支付参数)
*/ */
export const createRechargeOrder = (dto: CreateRechargePaymentDto) => { export const createRechargeOrder = async (dto: CreateRechargePaymentDto) => {
return post<RechargePaymentResponseDto>('/Recharge/create', { const body = {
tradeType: MINI_PROGRAM_TRADE_TYPE, openid: dto.openId,
...dto, bpsn: dto.bpSn,
}, { amount: dto.amount,
paytype: PAY_TYPE_WECHAT,
tradetype: TRADE_TYPE_JSAPI,
subject: dto.subject || '账户余额充值',
body: dto.body,
}
const res = await post<Record<string, unknown>>('/recharge/create', body, {
showLoading: true, showLoading: true,
loadingText: '创建订单中...', loadingText: '创建订单中...',
}) })
return {
...res,
data: normalizeRechargePaymentResponse(res.data as Record<string, unknown>),
}
}
export interface RechargePaymentQueryResult {
/** 是否充值到账 */
paid: boolean
/** 支付成功但充值未到账,需继续轮询 */
pending: boolean
data: RechargeStatusResult | null
}
/**
* 查询充值支付结果(getrcgstatus)
* code 200=到账,204=支付成功但未到账
*/
export const queryPaymentStatus = async (outTradeNo: string): Promise<RechargePaymentQueryResult> => {
try {
const res = await request<Record<string, unknown>>({
url: '/recharge/getrcgstatus',
method: 'GET',
data: { outtradeno: outTradeNo },
showError: false,
})
const data = normalizeRechargeStatus(res.data as Record<string, unknown>)
if (res.code === 200) {
return { paid: true, pending: false, data }
}
if (res.code === 204) {
return { paid: false, pending: true, data: null }
}
return { paid: false, pending: false, data: null }
} catch (error: unknown) {
const err = error as { code?: number }
if (err.code === 204) {
return { paid: false, pending: true, data: null }
}
throw error
}
} }
/** /**
* 查询支付状态 * 获取充值规则(套餐)
* @param outTradeNo 商户订单号(创建充值订单时返回)
* @param channel 支付渠道
* @returns 支付状态
*/ */
export const queryPaymentStatus = (outTradeNo: string, channel?: PaymentChannel) => { export const getRechargeRules = async (bpSn: string) => {
return get<boolean>(`/Recharge/query/${outTradeNo}`, channel ? { channel } : undefined) const res = await get<Record<string, unknown>>('/recharge/query', { bpsn: bpSn })
return {
...res,
data: normalizeRechargeRules(res.data as Record<string, unknown>),
}
}
/**
* 查询充值记录
*/
export const getRechargeRecords = async (params: RechargeRecordQueryParams) => {
const res = await get<Record<string, unknown>>('/recharge/records', {
customersn: params.customerSn,
startdate: params.startDate,
enddate: params.endDate,
paytype: params.payType,
bpsn: params.bpSn,
pageindex: params.pageIndex,
pagesize: params.pageSize,
})
const src = res.data || {}
const records = pickField<Record<string, unknown>[]>(src as Record<string, unknown>, 'data') || []
const data: RechargeRecordPagedResult = {
data: records.map((item) => ({
id: toNumber(pickField(item, 'id')),
customerSn: String(pickField(item, 'customerSn', 'customersn') ?? ''),
customerName: String(pickField(item, 'customerName', 'customername') ?? ''),
amount: toNumber(pickField(item, 'amount')),
giftAmount: toNumber(pickField(item, 'giftAmount', 'giftamount')),
payType: toNumber(pickField(item, 'payType', 'paytype')),
rcgType: toNumber(pickField(item, 'rcgType', 'rcgtype')),
rcgTypeName: String(pickField(item, 'rcgTypeName', 'rcgtypename') ?? ''),
bpSn: String(pickField(item, 'bpSn', 'bpsn') ?? ''),
bpName: String(pickField(item, 'bpName', 'bpname') ?? ''),
instSn: String(pickField(item, 'instSn', 'instsn') ?? ''),
instName: String(pickField(item, 'instName', 'instname') ?? ''),
logTime: String(pickField(item, 'logTime', 'logtime') ?? ''),
updateTime: String(pickField(item, 'updateTime', 'updatetime') ?? ''),
})),
totalCount: toNumber(pickField(src as Record<string, unknown>, 'totalCount', 'totalcount')),
pageIndex: toNumber(pickField(src as Record<string, unknown>, 'pageIndex', 'pageindex'), 1),
pageSize: toNumber(pickField(src as Record<string, unknown>, 'pageSize', 'pagesize'), 20),
}
return { ...res, data }
} }
/** /**
* 调起微信支付(充值场景) * 调起微信支付(充值场景)
* @param payParams 微信支付参数
*/ */
export const invokeWechatPayForRecharge = (payParams: RechargeWechatPayParameters): Promise<void> => { export const invokeWechatPayForRecharge = (payParams: RechargeWechatPayParameters): Promise<void> => {
const params = normalizeWechatPayParams(payParams as unknown as Record<string, unknown>) || payParams
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
wx.requestPayment({ wx.requestPayment({
timeStamp: payParams.timeStamp, timeStamp: params.timeStamp,
nonceStr: payParams.nonceStr, nonceStr: params.nonceStr,
package: payParams.package, package: params.package,
signType: payParams.signType as 'MD5' | 'HMAC-SHA256' | 'RSA', signType: params.signType as 'MD5' | 'HMAC-SHA256' | 'RSA',
paySign: payParams.paySign, paySign: params.paySign,
success: () => resolve(), success: () => resolve(),
fail: (err) => reject(err), fail: (err) => reject(err),
}) })

6
src/api/startDevice.ts

@ -4,6 +4,7 @@
* 关台/续费/关灯重开接口:/api/v2/TablesDev/stop * 关台/续费/关灯重开接口:/api/v2/TablesDev/stop
*/ */
import { get, post } from '../services/request' import { get, post } from '../services/request'
import { normalizeTableScanResponse } from '../utils/scanNormalize'
import { import {
StartGroupDevRequestDto, StartGroupDevRequestDto,
StartDeviceResponse, StartDeviceResponse,
@ -204,8 +205,9 @@ export const turnOffRestart = (grpSn: string, openId: string) => {
* @param openId 用户 OpenId * @param openId 用户 OpenId
* @param source 来源(可选) * @param source 来源(可选)
*/ */
export const getTableInfo = (sn: string, openId?: string, source?: string) => { export const getTableInfo = async (sn: string, openId?: string, source?: string) => {
return get<TableScanResponseDto>('/TablesDev/qrcode', { sn, openId, source }) const res = await get<Record<string, unknown>>('/magdev/qrcode', { sn, openId, source })
return { ...res, data: normalizeTableScanResponse(res.data as Record<string, unknown>) }
} }
/** /**

82
src/app.ts

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

79
src/pageScan/closeTable/components/FooterButtons.tsx

@ -4,15 +4,15 @@
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { immediateClose, invokeWechatPay, updateShareStatus } from '../../../api/startDevice' import { invokeWechatPay, PayType } from '../../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../../api/order' import { operate, CtrlType } from '../../../api/magdev'
import { userStore, tableStore } from '../../../store' import { userStore, tableStore } from '../../../store'
import './FooterButtons.scss' import './FooterButtons.scss'
interface FooterButtonsProps { interface FooterButtonsProps {
type: 'owner' | 'guest' type: 'owner' | 'guest'
grpSn: string grpSn: string
operationId: number operationId?: number
/** 是否从支付完成后跳转而来,true 时只显示「立即关台」 */ /** 是否从支付完成后跳转而来,true 时只显示「立即关台」 */
fromPayment?: boolean fromPayment?: boolean
} }
@ -57,42 +57,31 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer
} }
setLoading(true) setLoading(true)
try {
console.log('立即关台,设备编码:', grpSn, '操作记录ID:', operationId)
// 调用立即关台接口 try {
const result = await immediateClose(grpSn, openId) console.log('立即关台,设备编码:', grpSn)
const result = await operate({
grpSn,
openId,
ctrlType: CtrlType.ImmediateClose,
amount: 0,
payType: PayType.WechatPay,
})
console.log('关台接口返回:', result) console.log('关台接口返回:', result)
const { operationId: responseOperationId, outTradeNo, needPay, payParameters } = result.data const payParams = result.data.paymentResponseDto?.payParameters
// 判断是否需要支付(可能需要补差价) if (payParams) {
if (needPay && payParameters) { // 需要微信支付(补差价场景)
// 调起微信支付
try { try {
await invokeWechatPay(payParameters) await invokeWechatPay(payParams)
// 关台支付成功后无需额外查询,直接清场
// 轮询服务端确认支付结果(responseOperationId 对应后端 operationId,需确认与 consumptionId 语义一致) tableStore.clearAll()
Taro.showLoading({ title: '确认支付中...', mask: true }) Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 })
const paid = await pollTablePaymentStatus(responseOperationId) setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000)
Taro.hideLoading()
if (paid) {
Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000)
} else {
Taro.showModal({
title: '支付确认中',
content: '支付结果尚未到账,请稍后查看订单,如有疑问请联系店长。',
showCancel: false,
confirmText: '知道了',
})
}
} catch (payError: any) { } catch (payError: any) {
console.log('支付错误:', payError)
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) { if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else { } else {
@ -100,13 +89,13 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer
} }
} }
} else { } else {
// 无需支付(押金关台场景) // 无需支付(余额关台 / 押金退还场景)
tableStore.clearAll()
Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000) setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('关台失败:', error) console.error('关台失败:', error)
// 错误提示由 request 拦截器处理
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -140,19 +129,23 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer
}) })
} }
// 分享球台 // 分享球台(toggle:开启/取消分享)
const handleShareTable = async () => { const handleShareTable = async () => {
const orderNo = tableStore.scanInfo?.opInfo?.orderNo const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (!orderNo) { if (!openId || !grpSn) {
Taro.showToast({ title: '订单编号暂不可用,请稍后再试', icon: 'none', duration: 2000 }) Taro.showToast({ title: '信息异常,请重新扫码', icon: 'none', duration: 2000 })
return return
} }
try { try {
await updateShareStatus(orderNo) await operate({
Taro.showToast({ title: '已开启分享', icon: 'success', duration: 2000 }) grpSn,
} catch (_) { openId,
// 错误由 request 拦截器处理 ctrlType: CtrlType.Share,
} amount: 0,
payType: PayType.WechatPay,
})
Taro.showToast({ title: '操作成功', icon: 'success', duration: 2000 })
} catch (_) {}
} }
// 关灯重开 // 关灯重开

7
src/pageScan/closeTable/index.tsx

@ -8,6 +8,7 @@ import { observer } from 'mobx-react'
import { ArrowDown } from '@taroify/icons' import { ArrowDown } from '@taroify/icons'
import FooterButtons from './components/FooterButtons' import FooterButtons from './components/FooterButtons'
import { tableStore } from '../../store' import { tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
// 使用本地图片资源 // 使用本地图片资源
@ -39,11 +40,7 @@ const CloseTable = observer(() => {
// 拨打电话 // 拨打电话
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ callShopPhone(scanInfo?.phone)
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
} }
// 切换展开/收起 // 切换展开/收起

72
src/pageScan/couponStart/index.tsx

@ -5,9 +5,10 @@ import { View, Text, Image, Input } from '@tarojs/components'
import Taro, { useLoad, useRouter } from '@tarojs/taro' import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { couponStart, invokeWechatPay } from '../../api/startDevice' import { invokeWechatPay, getTableInfo, PayType } from '../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { operate, getOpStatus, CtrlType } from '../../api/magdev'
import { userStore, tableStore } from '../../store' import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
// 使用本地图片资源 // 使用本地图片资源
@ -81,11 +82,7 @@ const CouponStart = observer(() => {
// 拨打电话 // 拨打电话
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ callShopPhone(scanInfo?.phone)
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
} }
// 扫描券码 // 扫描券码
@ -158,54 +155,58 @@ const CouponStart = observer(() => {
} }
setLoading(true) setLoading(true)
try { try {
// 使用输入的券码或选中的券
const finalCouponCode = couponCode || `COUPON_${selectedCouponId}` const finalCouponCode = couponCode || `COUPON_${selectedCouponId}`
console.log('团购券开台,券码:', finalCouponCode, '设备编码:', grpSn) console.log('团购券开台,券码:', finalCouponCode, '设备编码:', grpSn)
// 调用团购券开台接口 const result = await operate({
const result = await couponStart(grpSn, openId, finalCouponCode) grpSn,
openId,
ctrlType: CtrlType.CouponStart,
amount: 0,
payType: PayType.WechatPay,
couponCode: finalCouponCode,
})
console.log('开台接口返回:', result) console.log('开台接口返回:', result)
// 保存操作记录ID到 store
if (result.data.consumptionId) { if (result.data.consumptionId) {
tableStore.setCurrentOperationId(result.data.consumptionId) tableStore.setCurrentOperationId(result.data.consumptionId)
} }
const consumptionId = result.data.consumptionId const payParams = result.data.paymentResponseDto?.payParameters
const outTradeNo = result.data.paymentResponseDto?.outTradeNo
// 判断是否需要支付(团购券一般无需支付,但可能需要补差价) if (payParams) {
if (result.data.paymentResponseDto?.payParameters) {
// 调起微信支付
try { try {
await invokeWechatPay(result.data.paymentResponseDto.payParameters) await invokeWechatPay(payParams)
// 轮询服务端确认支付结果 Taro.showLoading({ title: '开台确认中...', mask: true })
Taro.showLoading({ title: '确认支付中...', mask: true }) let isOpened = false
const paid = await pollTablePaymentStatus(consumptionId) try {
const statusRes = await getOpStatus(grpSn)
isOpened = statusRes.data.isOpened
} catch (_) {}
Taro.hideLoading() Taro.hideLoading()
if (paid) { if (isOpened) {
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}&fromPayment=1` })
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1`
})
}, 2000) }, 2000)
} else { } else {
Taro.showModal({ Taro.showModal({
title: '支付确认中', title: '开台确认中',
content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', content: '台桌正在启动,请稍后重新扫码进入关台页,如有疑问请联系店长。',
showCancel: false, showCancel: false,
confirmText: '知道了', confirmText: '知道了',
}) })
} }
} catch (payError: any) { } catch (payError: any) {
console.log('支付错误:', payError)
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) { if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else { } else {
@ -213,17 +214,18 @@ const CouponStart = observer(() => {
} }
} }
} else { } else {
// 无需支付 // 无需支付(团购券直接开台)
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '兑换成功,开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '兑换成功,开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}&fromPayment=1` })
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1`
})
}, 2000) }, 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('兑换开台失败:', error) console.error('兑换开台失败:', error)
// 错误提示由 request 拦截器处理
} finally { } finally {
setLoading(false) setLoading(false)
} }

63
src/pageScan/renewFee/index.tsx

@ -6,9 +6,10 @@ import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { Stepper } from '@taroify/core' import { Stepper } from '@taroify/core'
import { renewFee, invokeWechatPay, PayType } from '../../api/startDevice' import { invokeWechatPay, getTableInfo, PayType } from '../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { operate, CtrlType } from '../../api/magdev'
import { userStore, tableStore } from '../../store' import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
// 使用本地图片资源 // 使用本地图片资源
@ -40,11 +41,7 @@ const RenewFee = observer(() => {
// 拨打电话 // 拨打电话
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ callShopPhone(scanInfo?.phone)
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
} }
// 续费金额变化 // 续费金额变化
@ -94,42 +91,33 @@ const RenewFee = observer(() => {
} }
setLoading(true) setLoading(true)
try { try {
console.log('立即续费,金额:', renewAmount, '设备编码:', grpSn) console.log('立即续费,金额:', renewAmount, '设备编码:', grpSn)
// 调用续费接口 const result = await operate({
const result = await renewFee(grpSn, openId, renewAmount, PayType.WechatPay) grpSn,
openId,
ctrlType: CtrlType.Renew,
amount: renewAmount,
payType: PayType.WechatPay,
})
console.log('续费接口返回:', result) console.log('续费接口返回:', result)
const { operationId: responseOperationId, outTradeNo, needPay, payParameters } = result.data const payParams = result.data.paymentResponseDto?.payParameters
// 判断是否需要支付 if (payParams) {
if (needPay && payParameters) {
// 调起微信支付
try { try {
await invokeWechatPay(payParameters) await invokeWechatPay(payParams)
// 续费支付成功,无需额外查询,可选刷新关台页数据
// 轮询服务端确认支付结果(responseOperationId 对应后端 operationId,需确认与 consumptionId 语义一致) try {
Taro.showLoading({ title: '确认支付中...', mask: true }) const freshInfo = await getTableInfo(grpSn, openId)
const paid = await pollTablePaymentStatus(responseOperationId) tableStore.setScanInfo(freshInfo.data)
Taro.hideLoading() } catch (_) {}
Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 })
if (paid) { setTimeout(() => Taro.navigateBack(), 2000)
Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.navigateBack(), 2000)
} else {
Taro.showModal({
title: '支付确认中',
content: '支付结果尚未到账,请稍后刷新页面查看,如有疑问请联系店长。',
showCancel: false,
confirmText: '知道了',
})
}
} catch (payError: any) { } catch (payError: any) {
console.log('支付错误:', payError)
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) { if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else { } else {
@ -137,13 +125,16 @@ const RenewFee = observer(() => {
} }
} }
} else { } else {
// 无需支付 // 余额续费,无需支付
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.navigateBack(), 2000) setTimeout(() => Taro.navigateBack(), 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('续费失败:', error) console.error('续费失败:', error)
// 错误提示由 request 拦截器处理
} finally { } finally {
setLoading(false) setLoading(false)
} }

78
src/pageScan/selfStart/index.tsx

@ -6,9 +6,10 @@ import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { Stepper } from '@taroify/core' import { Stepper } from '@taroify/core'
import { depositStart, balanceStart, invokeWechatPay, PayType, getTableInfo, pollCheckOpenStatus } from '../../api/startDevice' import { invokeWechatPay, getTableInfo, PayType } from '../../api/startDevice'
import { closeOrder } from '../../api/order' import { operate, getOpStatus, CtrlType } from '../../api/magdev'
import { userStore, tableStore } from '../../store' import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
// 使用本地图片资源 // 使用本地图片资源
@ -42,11 +43,7 @@ const SelfStart = observer(() => {
// 拨打电话 // 拨打电话
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ callShopPhone(scanInfo?.phone)
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
} }
// 押金变化 // 押金变化
@ -122,55 +119,51 @@ const SelfStart = observer(() => {
} }
setLoading(true) setLoading(true)
try { try {
console.log('立即开台,押金:', deposit, '设备编码:', grpSn, '支付方式:', paymentMethod) console.log('立即开台,押金:', deposit, '设备编码:', grpSn, '支付方式:', paymentMethod)
// 根据支付方式选择接口 const payType = paymentMethod === '余额支付' ? PayType.Balance : PayType.WechatPay
let result const result = await operate({
if (paymentMethod === '余额支付') { grpSn,
// 余额开台 openId,
result = await balanceStart(grpSn, openId, deposit) ctrlType: CtrlType.DepositStart,
} else { amount: deposit,
// 押金开台(微信支付) payType,
result = await depositStart(grpSn, openId, deposit, PayType.WechatPay) })
}
console.log('开台接口返回:', result) console.log('开台接口返回:', result)
// 保存操作记录ID到 store // 保存 consumptionId(兼容旧逻辑,新流程不依赖它查接口)
if (result.data.consumptionId) { if (result.data.consumptionId) {
tableStore.setCurrentOperationId(result.data.consumptionId) tableStore.setCurrentOperationId(result.data.consumptionId)
} }
const consumptionId = result.data.consumptionId const payParams = result.data.paymentResponseDto?.payParameters
const outTradeNo = result.data.paymentResponseDto?.outTradeNo
// 判断是否需要支付 if (payParams) {
if (result.data.paymentResponseDto?.payParameters) {
// 调起微信支付 // 调起微信支付
try { try {
await invokeWechatPay(result.data.paymentResponseDto.payParameters) await invokeWechatPay(payParams)
// 轮询台子是否真正开启(check-open-status),解决付款后仍显示 open 的问题 // 付款后查一次开台状态(getopstatus,不轮询)
Taro.showLoading({ title: '开台确认中...', mask: true }) Taro.showLoading({ title: '开台确认中...', mask: true })
const openResult = await pollCheckOpenStatus(outTradeNo!) let isOpened = false
try {
const statusRes = await getOpStatus(grpSn)
isOpened = statusRes.data.isOpened
} catch (_) {}
Taro.hideLoading() Taro.hideLoading()
if (openResult?.isOpened) { if (isOpened) {
// 用接口返回的 operationId(比 consumptionId 更准确) // 刷新 scanInfo,确保关台页有真实的 opInfo
const confirmedOperationId = openResult.operationId || consumptionId
// 刷新 scanInfo,关台页 opInfo 有真实数据
try { try {
const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') const freshInfo = await getTableInfo(grpSn, openId)
const freshInfo = await getTableInfo(grpSn, openId2)
tableStore.setScanInfo(freshInfo.data) tableStore.setScanInfo(freshInfo.data)
} catch (_) {} } catch (_) {}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}&fromPayment=1` })
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${confirmedOperationId}&fromPayment=1`
})
}, 2000) }, 2000)
} else { } else {
Taro.showModal({ Taro.showModal({
@ -181,9 +174,6 @@ const SelfStart = observer(() => {
}) })
} }
} catch (payError: any) { } catch (payError: any) {
console.log('支付错误:', payError)
// 关闭服务端未支付订单,避免产生悬挂订单
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) { if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else { } else {
@ -191,24 +181,18 @@ const SelfStart = observer(() => {
} }
} }
} else { } else {
// 无需支付(余额支付等场景) // 无需支付(余额支付场景)
try { try {
const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') const freshInfo = await getTableInfo(grpSn, openId)
const freshInfo = await getTableInfo(grpSn, openId2)
tableStore.setScanInfo(freshInfo.data) tableStore.setScanInfo(freshInfo.data)
} catch (_) { } catch (_) {}
// 刷新失败不阻断流程
}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}&fromPayment=1` })
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1`
})
}, 2000) }, 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('开台失败:', error) console.error('开台失败:', error)
// 错误提示由 request 拦截器处理
} finally { } finally {
setLoading(false) setLoading(false)
} }

88
src/pageScan/startTable/index.tsx

@ -3,9 +3,12 @@
*/ */
import { View, Text, Image } from '@tarojs/components' import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad, useRouter } from '@tarojs/taro' import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { CtrlType } from '../../api/startDevice' import { CtrlType, getTableInfo } from '../../api/startDevice'
import { tableStore } from '../../store' import { preOperate } from '../../api/magdev'
import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
const phoneIcon = require('./images/phone.png') const phoneIcon = require('./images/phone.png')
@ -22,6 +25,7 @@ const StartTable = observer(() => {
const router = useRouter() const router = useRouter()
const grpSn = router.params.grpSn || tableStore.currentGrpSn const grpSn = router.params.grpSn || tableStore.currentGrpSn
const scanInfo = tableStore.scanInfo const scanInfo = tableStore.scanInfo
const [loading, setLoading] = useState(false)
useLoad(() => { useLoad(() => {
console.log('开台页面加载, grpSn:', grpSn) console.log('开台页面加载, grpSn:', grpSn)
@ -29,23 +33,74 @@ const StartTable = observer(() => {
}) })
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ phoneNumber: '100-666-XXXX' }).catch(() => {}) callShopPhone(scanInfo?.phone)
} }
const handleStartTable = (option: { optionCode: number; type: number; title: string }, available: boolean) => { const handleStartTable = async (
option: { optionCode: number; type: number; title: string },
available: boolean
) => {
if (!available) { if (!available) {
Taro.showToast({ title: '当前条件不满足,无法使用该开台方式', icon: 'none', duration: 2000 }) Taro.showToast({ title: '当前条件不满足,无法使用该开台方式', icon: 'none', duration: 2000 })
return return
} }
// 先用 optionCode 匹配,匹配不到再用 type 匹配 if (loading) return
const url = OPTION_URL_MAP[option.optionCode] ?? OPTION_URL_MAP[option.type]
console.log(`[DEBUG] 点击开台方式 title=${option.title} optionCode=${option.optionCode} type=${option.type} → url=${url}`) const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (url) { if (!openId) {
Taro.navigateTo({ url: `${url}?grpSn=${grpSn}` }) Taro.showToast({ title: '请先登录', icon: 'none', duration: 2000 })
} else if (option.type === CtrlType.CreditStart) { return
Taro.showToast({ title: '信用开台功能开发中', icon: 'none', duration: 2000 }) }
} else { if (!grpSn) {
Taro.showToast({ title: '功能开发中', icon: 'none', duration: 2000 }) Taro.showToast({ title: '设备信息异常,请重新扫码', icon: 'none', duration: 2000 })
return
}
setLoading(true)
try {
const res = await preOperate(option.type, grpSn, openId)
const data = res.data
// 写入 bpsn / balance / 会员状态 到 userStore
if (data.bpSn && userStore.userInfo) {
userStore.setUserInfo({
...userStore.userInfo,
bpSn: data.bpSn,
bpInfoName: data.bpName,
instSn: data.instSn,
})
}
if (data.balance >= 0) {
userStore.setBalance(data.balance)
}
if (data.outTradeNo) {
// 余额充足,已直接开台 → 刷新 scanInfo 后跳转关台页
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 1500 })
setTimeout(() => {
Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}` })
}, 1500)
return
}
// 余额不足 → 进对应子页
const url = OPTION_URL_MAP[option.optionCode] ?? OPTION_URL_MAP[option.type]
console.log(`[DEBUG] 点击开台方式 title=${option.title} type=${option.type} → url=${url}`)
if (url) {
Taro.navigateTo({ url: `${url}?grpSn=${grpSn}` })
} else if (option.type === CtrlType.CreditStart) {
Taro.showToast({ title: '信用开台功能开发中', icon: 'none', duration: 2000 })
} else {
Taro.showToast({ title: '功能开发中', icon: 'none', duration: 2000 })
}
} catch (error: any) {
console.error('preOperate 失败:', error)
} finally {
setLoading(false)
} }
} }
@ -105,16 +160,17 @@ const StartTable = observer(() => {
</View> </View>
<View className="method-right"> <View className="method-right">
<View <View
className={`start-btn ${!option.available ? 'disabled' : ''}`} className={`start-btn ${(!option.available || loading) ? 'disabled' : ''}`}
onClick={() => handleStartTable(option, option.available)} onClick={() => handleStartTable(option, option.available)}
> >
<Text className="btn-text">{option.available ? option.actionText || '去开台' : '不可用'}</Text> <Text className="btn-text">
{loading ? '处理中...' : (option.available ? option.actionText || '去开台' : '不可用')}
</Text>
</View> </View>
</View> </View>
</View> </View>
)) ))
) : ( ) : (
// 接口数据未加载时显示占位
<View className="method-item"> <View className="method-item">
<Text className="method-desc">加载中...</Text> <Text className="method-desc">加载中...</Text>
</View> </View>

69
src/pageScan/timedStart/index.tsx

@ -5,9 +5,10 @@ import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad, useRouter } from '@tarojs/taro' import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { timedStart, invokeWechatPay, PayType } from '../../api/startDevice' import { invokeWechatPay, getTableInfo, PayType } from '../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { operate, getOpStatus, CtrlType } from '../../api/magdev'
import { userStore, tableStore } from '../../store' import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
// 使用本地图片资源 // 使用本地图片资源
@ -47,11 +48,7 @@ const TimedStart = observer(() => {
// 拨打电话 // 拨打电话
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ callShopPhone(scanInfo?.phone)
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
} }
// 选择时长 // 选择时长
@ -104,53 +101,58 @@ const TimedStart = observer(() => {
} }
setLoading(true) setLoading(true)
try { try {
const amount = parseFloat(calculateAmount()) const amount = parseFloat(calculateAmount())
console.log('定时开台,时长:', selectedDuration, '小时,金额:', amount, '设备编码:', grpSn) console.log('定时开台,时长:', selectedDuration, '小时,金额:', amount, '设备编码:', grpSn)
// 调用定时开台接口 const result = await operate({
const result = await timedStart(grpSn, openId, selectedDuration, amount, PayType.WechatPay) grpSn,
openId,
ctrlType: CtrlType.TimedStart,
amount,
payType: PayType.WechatPay,
})
console.log('开台接口返回:', result) console.log('开台接口返回:', result)
// 保存操作记录ID到 store
if (result.data.consumptionId) { if (result.data.consumptionId) {
tableStore.setCurrentOperationId(result.data.consumptionId) tableStore.setCurrentOperationId(result.data.consumptionId)
} }
const consumptionId = result.data.consumptionId const payParams = result.data.paymentResponseDto?.payParameters
const outTradeNo = result.data.paymentResponseDto?.outTradeNo
// 判断是否需要支付 if (payParams) {
if (result.data.paymentResponseDto?.payParameters) {
// 调起微信支付
try { try {
await invokeWechatPay(result.data.paymentResponseDto.payParameters) await invokeWechatPay(payParams)
// 轮询服务端确认支付结果 // 付款后查一次开台状态(不轮询)
Taro.showLoading({ title: '确认支付中...', mask: true }) Taro.showLoading({ title: '开台确认中...', mask: true })
const paid = await pollTablePaymentStatus(consumptionId) let isOpened = false
try {
const statusRes = await getOpStatus(grpSn)
isOpened = statusRes.data.isOpened
} catch (_) {}
Taro.hideLoading() Taro.hideLoading()
if (paid) { if (isOpened) {
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}&fromPayment=1` })
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1`
})
}, 2000) }, 2000)
} else { } else {
Taro.showModal({ Taro.showModal({
title: '支付确认中', title: '开台确认中',
content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', content: '台桌正在启动,请稍后重新扫码进入关台页,如有疑问请联系店长。',
showCancel: false, showCancel: false,
confirmText: '知道了', confirmText: '知道了',
}) })
} }
} catch (payError: any) { } catch (payError: any) {
console.log('支付错误:', payError)
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) { if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else { } else {
@ -158,17 +160,18 @@ const TimedStart = observer(() => {
} }
} }
} else { } else {
// 无需支付 // 无需支付(余额开台等)
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({ url: `/pageScan/closeTable/index?grpSn=${grpSn}&fromPayment=1` })
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1`
})
}, 2000) }, 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('开台失败:', error) console.error('开台失败:', error)
// 错误提示由 request 拦截器处理
} finally { } finally {
setLoading(false) setLoading(false)
} }

67
src/pageScan/turnOffRestart/index.tsx

@ -5,9 +5,10 @@ import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad, useRouter } from '@tarojs/taro' import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { turnOffRestart, invokeWechatPay } from '../../api/startDevice' import { invokeWechatPay, getTableInfo, PayType } from '../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { operate, CtrlType } from '../../api/magdev'
import { userStore, tableStore } from '../../store' import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss' import './index.scss'
// 使用本地图片资源 // 使用本地图片资源
@ -34,11 +35,7 @@ const TurnOffRestart = observer(() => {
// 拨打电话 // 拨打电话
const handlePhoneCall = () => { const handlePhoneCall = () => {
Taro.makePhoneCall({ callShopPhone(scanInfo?.phone)
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
} }
// 确认结算 // 确认结算
@ -78,42 +75,33 @@ const TurnOffRestart = observer(() => {
} }
setLoading(true) setLoading(true)
try {
console.log('关灯重开,设备编码:', grpSn, '操作记录ID:', operationId)
// 调用关灯重开接口 try {
const result = await turnOffRestart(grpSn, openId) console.log('关灯重开,设备编码:', grpSn)
const result = await operate({
grpSn,
openId,
ctrlType: CtrlType.TurnOffRestart,
amount: 0,
payType: PayType.WechatPay,
})
console.log('关灯重开接口返回:', result) console.log('关灯重开接口返回:', result)
const { operationId: responseOperationId, outTradeNo, needPay, payParameters } = result.data const payParams = result.data.paymentResponseDto?.payParameters
// 判断是否需要支付 if (payParams) {
if (needPay && payParameters) {
// 调起微信支付
try { try {
await invokeWechatPay(payParameters) await invokeWechatPay(payParams)
// 支付成功后刷新 scanInfo,关台页返回时显示新会话数据
// 轮询服务端确认支付结果(responseOperationId 对应后端 operationId,需确认与 consumptionId 语义一致) try {
Taro.showLoading({ title: '确认支付中...', mask: true }) const freshInfo = await getTableInfo(grpSn, openId)
const paid = await pollTablePaymentStatus(responseOperationId) tableStore.setScanInfo(freshInfo.data)
Taro.hideLoading() } catch (_) {}
Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 })
if (paid) { setTimeout(() => Taro.navigateBack(), 2000)
Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.navigateBack(), 2000)
} else {
Taro.showModal({
title: '支付确认中',
content: '支付结果尚未到账,请稍后刷新页面查看,如有疑问请联系店长。',
showCancel: false,
confirmText: '知道了',
})
}
} catch (payError: any) { } catch (payError: any) {
console.log('支付错误:', payError)
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) { if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else { } else {
@ -121,13 +109,16 @@ const TurnOffRestart = observer(() => {
} }
} }
} else { } else {
// 无需支付 // 余额结算,无需支付,刷新新会话数据
try {
const freshInfo = await getTableInfo(grpSn, openId)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.navigateBack(), 2000) setTimeout(() => Taro.navigateBack(), 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('关灯重开失败:', error) console.error('关灯重开失败:', error)
// 错误提示由 request 拦截器处理
} finally { } finally {
setLoading(false) setLoading(false)
} }

17
src/pages/index/index.tsx

@ -9,7 +9,7 @@ import { Swiper, Image as TaroifyImage } from '@taroify/core'
import { LocationOutlined } from '@taroify/icons' import { LocationOutlined } from '@taroify/icons'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { userStore } from '../../store' import { userStore } from '../../store'
import { updateCustomer, readImageAsBase64 } from '@/api/customer' import { updateCustomer } from '@/api/customer'
import { getBigImage } from '@/utils/imageHelper' import { getBigImage } from '@/utils/imageHelper'
import myAvatar from '@/assets/icon/my_avatar.png' import myAvatar from '@/assets/icon/my_avatar.png'
import './index.scss' import './index.scss'
@ -50,26 +50,17 @@ const Index = observer(() => {
setSaving(true) setSaving(true)
try { try {
let avatarImage: string | undefined
if (newAvatarPath) {
try {
avatarImage = await readImageAsBase64(newAvatarPath)
} catch (e) {
console.warn('头像转 Base64 失败,跳过头像更新:', e)
}
}
await updateCustomer(userId, { await updateCustomer(userId, {
id: userId, id: userId,
name: newNickname.trim() || undefined, name: newNickname.trim() || undefined,
avatarImage, avatarFilePath: newAvatarPath || undefined,
}) })
// 同步更新本地 store // 同步更新本地 store
userStore.setUserInfo({ userStore.setUserInfo({
...userStore.userInfo!, ...userStore.userInfo!,
name: newNickname.trim() || userStore.userInfo?.name, name: newNickname.trim() || userStore.userInfo?.name || '',
nickname: newNickname.trim() || userStore.userInfo?.nickname, nickname: newNickname.trim() || userStore.userInfo?.nickname || '微信用户',
avatar: newAvatarPath || userStore.userInfo?.avatar, avatar: newAvatarPath || userStore.userInfo?.avatar,
}) })

4
src/pages/store/index.tsx

@ -210,7 +210,7 @@ const StoreIndex = observer(() => {
// 跳转到店铺详情 // 跳转到店铺详情
const clickStoreItem = (store: StoreInfoDto) => { const clickStoreItem = (store: StoreInfoDto) => {
Taro.navigateTo({ Taro.navigateTo({
url: '/pageStore/storeDetail/index?id=' + store.id url: '/pageStore/storeDetail/index?sn=' + store.sn
}) })
} }
@ -269,7 +269,7 @@ const StoreIndex = observer(() => {
<View className="list-footer"><Text className="footer-text">暂无门店数据</Text></View> <View className="list-footer"><Text className="footer-text">暂无门店数据</Text></View>
) : ( ) : (
storeList.map((store) => ( storeList.map((store) => (
<View className="store-item" key={store.id} onClick={() => clickStoreItem(store)}> <View className="store-item" key={store.sn || store.id} onClick={() => clickStoreItem(store)}>
{/* 标题和类型 */} {/* 标题和类型 */}
<View className="store-header"> <View className="store-header">
<Text className="store-name">{store.name}</Text> <Text className="store-name">{store.name}</Text>

7
src/pages/user/index.tsx

@ -7,7 +7,7 @@ import Taro from '@tarojs/taro'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { Image as TaroifyImage } from '@taroify/core' import { Image as TaroifyImage } from '@taroify/core'
import { userStore } from '../../store' import { userStore } from '../../store'
import { getCustomerByOpenId } from '../../api/customer' import { refreshCustomerProfile } from '../../api/customer'
import { getBigImage } from '@/utils/imageHelper' import { getBigImage } from '@/utils/imageHelper'
import './index.scss' import './index.scss'
@ -32,10 +32,7 @@ const UserIndex = observer(() => {
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (userStore.isLoggedIn && openId) { if (userStore.isLoggedIn && openId) {
try { try {
const res = await getCustomerByOpenId(openId) await refreshCustomerProfile(openId)
if (res.data) {
userStore.setUserInfoFromWechat(res.data)
}
} catch (e) { } catch (e) {
console.warn('刷新用户信息失败:', e) console.warn('刷新用户信息失败:', e)
} }

89
src/pagesOrder/recharge/index.tsx

@ -6,18 +6,25 @@ import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { Arrow } from '@taroify/icons' import { Arrow } from '@taroify/icons'
import { createRechargeOrder, invokeWechatPayForRecharge, queryPaymentStatus } from '@/api/recharge' import {
createRechargeOrder,
invokeWechatPayForRecharge,
queryPaymentStatus,
getRechargeRules,
} from '@/api/recharge'
import { RechargeRuleItem } from '@/types/recharge'
import { userStore } from '@/store' import { userStore } from '@/store'
import './index.scss' import './index.scss'
// 充值套餐数据(套餐内容需与后端确认是否动态配置)
interface RechargePackage { interface RechargePackage {
id: number id: number
amount: number amount: number
bonus: number bonus: number
desc?: string
} }
const RechargePackages: RechargePackage[] = [ /** 接口不可用时的兜底套餐 */
const FALLBACK_PACKAGES: RechargePackage[] = [
{ id: 1, amount: 100, bonus: 50 }, { id: 1, amount: 100, bonus: 50 },
{ id: 2, amount: 200, bonus: 100 }, { id: 2, amount: 200, bonus: 100 },
{ id: 3, amount: 300, bonus: 200 }, { id: 3, amount: 300, bonus: 200 },
@ -29,36 +36,63 @@ const RechargePackages: RechargePackage[] = [
const RechargePage = observer(() => { const RechargePage = observer(() => {
const [selectedPackageId, setSelectedPackageId] = useState<number | null>(null) const [selectedPackageId, setSelectedPackageId] = useState<number | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [packages, setPackages] = useState<RechargePackage[]>(FALLBACK_PACKAGES)
const [storeName, setStoreName] = useState(userStore.userInfo?.bpInfoName || '--')
useLoad(() => { useLoad(async () => {
console.log('余额充值页面加载') const bpSn = userStore.userInfo?.bpSn
if (!bpSn) return
try {
const res = await getRechargeRules(bpSn)
if (res.data?.rules?.length) {
setPackages(
res.data.rules.map((rule: RechargeRuleItem, index: number) => ({
id: index + 1,
amount: rule.recharge,
bonus: rule.gift,
desc: rule.desc,
}))
)
if (res.data.bpName) {
setStoreName(res.data.bpName)
}
}
} catch (e) {
console.warn('获取充值规则失败,使用默认套餐:', e)
}
}) })
// 当前余额和门店名称从 userStore 读取
const currentBalance = userStore.balance const currentBalance = userStore.balance
const storeName = userStore.userInfo?.bpInfoName || '--'
// 选择套餐
const handleSelectPackage = (id: number) => { const handleSelectPackage = (id: number) => {
setSelectedPackageId(id) setSelectedPackageId(id)
} }
// 查看明细
const handleViewDetail = () => { const handleViewDetail = () => {
Taro.navigateTo({ Taro.navigateTo({
url: '/pagesOrder/rechargeDetail/index' url: '/pagesOrder/rechargeDetail/index'
}) })
} }
// 轮询查询支付状态,最多查 5 次,每次间隔 2 秒
const pollPaymentStatus = async (outTradeNo: string): Promise<boolean> => { const pollPaymentStatus = async (outTradeNo: string): Promise<boolean> => {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)) await new Promise(resolve => setTimeout(resolve, 2000))
try { try {
const res = await queryPaymentStatus(outTradeNo, 'WechatPay') const res = await queryPaymentStatus(outTradeNo)
if (res.data === true) { if (res.paid && res.data) {
userStore.setBalance(res.data.balance)
if (userStore.userInfo) {
userStore.setUserInfo({
...userStore.userInfo,
amount: res.data.balance,
})
}
return true return true
} }
if (!res.pending) {
return false
}
} catch (e) { } catch (e) {
console.warn('查询支付状态异常:', e) console.warn('查询支付状态异常:', e)
} }
@ -66,7 +100,6 @@ const RechargePage = observer(() => {
return false return false
} }
// 立即充值
const handleRecharge = async () => { const handleRecharge = async () => {
if (loading) return if (loading) return
@ -75,29 +108,29 @@ const RechargePage = observer(() => {
return return
} }
const selectedPackage = RechargePackages.find(pkg => pkg.id === selectedPackageId) const selectedPackage = packages.find(pkg => pkg.id === selectedPackageId)
if (!selectedPackage) return if (!selectedPackage) return
const openId = userStore.userInfo?.wxOpenId const openId = userStore.userInfo?.wxOpenId
const bpSn = userStore.userInfo?.bpSn
if (!openId) { if (!openId) {
Taro.showToast({ title: '请先登录', icon: 'none', duration: 2000 }) Taro.showToast({ title: '请先登录', icon: 'none', duration: 2000 })
return return
} }
if (!bpSn) {
Taro.showToast({ title: '未绑定门店,暂无法充值', icon: 'none', duration: 2000 })
return
}
setLoading(true) setLoading(true)
try { try {
// 创建充值订单
const result = await createRechargeOrder({ const result = await createRechargeOrder({
userId: String(userStore.userInfo?.id || ''),
customerSn: userStore.userInfo?.bpSn,
amount: selectedPackage.amount, amount: selectedPackage.amount,
giftAmount: selectedPackage.bonus, bpSn,
bpSn: userStore.userInfo?.bpSn,
instSn: userStore.userInfo?.instSn,
openId, openId,
subject: '账户余额充值', subject: '账户余额充值',
body: `充值 ¥${selectedPackage.amount},赠送 ¥${selectedPackage.bonus}`, body: selectedPackage.desc || `充值 ¥${selectedPackage.amount},赠送 ¥${selectedPackage.bonus}`,
}) })
const payData = result.data const payData = result.data
@ -107,20 +140,14 @@ const RechargePage = observer(() => {
return return
} }
// 调起微信支付
try { try {
await invokeWechatPayForRecharge(payData.payParameters) await invokeWechatPayForRecharge(payData.payParameters)
// 微信支付成功回调后,查询服务端支付状态确认
Taro.showLoading({ title: '确认支付中...', mask: true }) Taro.showLoading({ title: '确认支付中...', mask: true })
const paid = await pollPaymentStatus(payData.outTradeNo) const paid = await pollPaymentStatus(payData.outTradeNo)
Taro.hideLoading() Taro.hideLoading()
if (paid) { if (paid) {
// 更新本地余额(充值金额 + 赠送金额)
const newBalance = currentBalance + selectedPackage.amount + selectedPackage.bonus
userStore.setBalance(newBalance)
Taro.showToast({ title: '充值成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '充值成功', icon: 'success', duration: 2000 })
setSelectedPackageId(null) setSelectedPackageId(null)
} else { } else {
@ -141,7 +168,6 @@ const RechargePage = observer(() => {
} }
} catch (error: any) { } catch (error: any) {
console.error('创建充值订单失败:', error) console.error('创建充值订单失败:', error)
// 错误提示由 request 拦截器统一处理
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -149,7 +175,6 @@ const RechargePage = observer(() => {
return ( return (
<View className="recharge-page"> <View className="recharge-page">
{/* 上部分:余额卡片区域 */}
<View className="balance-card-area"> <View className="balance-card-area">
<Text className="store-name">{storeName}</Text> <Text className="store-name">{storeName}</Text>
@ -180,13 +205,11 @@ const RechargePage = observer(() => {
</View> </View>
</View> </View>
{/* 下部分:套餐和提示区域 */}
<View className="packages-area"> <View className="packages-area">
<Text className="section-title">余额充值</Text> <Text className="section-title">余额充值</Text>
{/* 充值套餐列表 */}
<View className="packages-grid"> <View className="packages-grid">
{RechargePackages.map(pkg => ( {packages.map(pkg => (
<View <View
key={pkg.id} key={pkg.id}
className={`package-item ${selectedPackageId === pkg.id ? 'selected' : ''}`} className={`package-item ${selectedPackageId === pkg.id ? 'selected' : ''}`}
@ -198,7 +221,6 @@ const RechargePage = observer(() => {
))} ))}
</View> </View>
{/* 温馨提示 */}
<View className="tips-section"> <View className="tips-section">
<Text className="tips-title">温馨提示:{'\n'}</Text> <Text className="tips-title">温馨提示:{'\n'}</Text>
<Text className="tips-content"> <Text className="tips-content">
@ -211,7 +233,6 @@ const RechargePage = observer(() => {
</View> </View>
</View> </View>
{/* 底部固定按钮 */}
<View className="footer-bar"> <View className="footer-bar">
<View className={`recharge-btn ${loading ? 'disabled' : ''}`} onClick={handleRecharge}> <View className={`recharge-btn ${loading ? 'disabled' : ''}`} onClick={handleRecharge}>
<Text className="recharge-btn-text">{loading ? '处理中...' : '立即充值'}</Text> <Text className="recharge-btn-text">{loading ? '处理中...' : '立即充值'}</Text>

29
src/pagesUser/editProfile/index.tsx

@ -6,7 +6,7 @@ import Taro, { useLoad } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { Image as TaroifyImage, Button, ActionSheet } from '@taroify/core' import { Image as TaroifyImage, Button, ActionSheet } from '@taroify/core'
import { updateCustomer, readImageAsBase64 } from '../../api/customer' import { updateCustomer } from '../../api/customer'
import { userStore } from '../../store' import { userStore } from '../../store'
import myAvatar from '../../assets/icon/my_avatar.png' import myAvatar from '../../assets/icon/my_avatar.png'
import './index.scss' import './index.scss'
@ -16,7 +16,6 @@ const EditProfile = observer(() => {
const [nickname, setNickname] = useState(info?.name || info?.nickname || '') const [nickname, setNickname] = useState(info?.name || info?.nickname || '')
const [phone, setPhone] = useState(info?.phone || '') const [phone, setPhone] = useState(info?.phone || '')
const [wchart, setWchart] = useState(info?.wchart || '')
const [avatarUrl, setAvatarUrl] = useState(info?.avatar || myAvatar) const [avatarUrl, setAvatarUrl] = useState(info?.avatar || myAvatar)
// 是否选了新的本地头像(需要上传) // 是否选了新的本地头像(需要上传)
const [newAvatarPath, setNewAvatarPath] = useState<string>('') const [newAvatarPath, setNewAvatarPath] = useState<string>('')
@ -88,32 +87,21 @@ const EditProfile = observer(() => {
setLoading(true) setLoading(true)
try { try {
// 如果选了新头像,先转成 Base64 const result = await updateCustomer(userId, {
let avatarImage: string | undefined
if (newAvatarPath) {
try {
avatarImage = await readImageAsBase64(newAvatarPath)
} catch (e) {
console.warn('头像转 Base64 失败,跳过头像更新:', e)
}
}
await updateCustomer(userId, {
id: userId, id: userId,
name: nickname.trim(), name: nickname.trim(),
phone: phone.trim() || undefined, phone: phone.trim() || undefined,
wchart: wchart.trim() || undefined, avatarFilePath: newAvatarPath || undefined,
avatarImage,
}) })
// 同步更新本地 store // 用服务端返回的数据更新 store(头像使用服务端 URL,非本地临时路径)
const serverAvatar = result.data?.wxAvatarUrl || newAvatarPath || userStore.userInfo?.avatar
userStore.setUserInfo({ userStore.setUserInfo({
...userStore.userInfo!, ...userStore.userInfo!,
name: nickname.trim(), name: nickname.trim(),
nickname: nickname.trim(), nickname: nickname.trim(),
phone: phone.trim() || userStore.userInfo?.phone, phone: phone.trim() || userStore.userInfo?.phone,
wchart: wchart.trim() || userStore.userInfo?.wchart, avatar: serverAvatar,
avatar: newAvatarPath || userStore.userInfo?.avatar,
}) })
Taro.showToast({ title: '保存成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '保存成功', icon: 'success', duration: 2000 })
@ -177,18 +165,17 @@ const EditProfile = observer(() => {
/> />
</View> </View>
{/* 微信号 */} {/* 微信号(接口已移除该字段,暂不展示)
<View className="form-item"> <View className="form-item">
<Text className="form-label">微信号</Text> <Text className="form-label">微信号</Text>
<Input <Input
className="form-input" className="form-input"
value={wchart}
onInput={(e) => setWchart(e.detail.value)}
placeholder="请输入微信号" placeholder="请输入微信号"
placeholderClass="form-placeholder" placeholderClass="form-placeholder"
maxlength={30} maxlength={30}
/> />
</View> </View>
*/}
</View> </View>
{/* 保存按钮 */} {/* 保存按钮 */}

34
src/types/login.ts

@ -2,12 +2,11 @@
* 微信登录相关类型定义 * 微信登录相关类型定义
*/ */
// 微信登录请求参数 - 根据接口文档更新
export interface WechatLoginRequest { export interface WechatLoginRequest {
code: string // 微信登录凭证code,通过 wx.login() 获取的临时登录凭证 code: string
} }
// 客户信息 /** 客户信息(兼容新旧接口;新接口可能缺会员/门店/余额等) */
export interface CustomerDto { export interface CustomerDto {
id: number id: number
sn: string sn: string
@ -26,16 +25,19 @@ export interface CustomerDto {
lastLoginTime: string lastLoginTime: string
wxNickName: string wxNickName: string
wxAvatarUrl: string wxAvatarUrl: string
birthday?: string
} }
// 微信登录结果数据 /** 登录 data:可能是扁平客户字段,也可能嵌套 customer */
export interface WechatAuthResultDto { export interface WechatAuthResultDto {
customer: CustomerDto /** 兼容旧结构 */
customer?: CustomerDto
isNewCustomer: boolean isNewCustomer: boolean
message: string message?: string
/** 扁平字段时 login 直接带在 data 上,由 normalize 填入 customer */
[key: string]: unknown
} }
// 微信登录响应
export interface WechatAuthResponse { export interface WechatAuthResponse {
message: string message: string
code: number code: number
@ -43,17 +45,17 @@ export interface WechatAuthResponse {
data: WechatAuthResultDto data: WechatAuthResultDto
} }
// 更新客户信息请求参数 /** 更新客户信息:新接口走 multipart,avatar 传本地文件路径 */
export interface UpdateCustomerDto { export interface UpdateCustomerDto {
id: number id: number
/** 头像(Base64 编码字符串) */
avatarImage?: string
/** 名称 */
name?: string name?: string
/** 客户类型 */ phone?: string
birthday?: string
/** 新接口:本地临时文件路径,走 uploadFile */
avatarFilePath?: string
/** 旧接口:Base64(保留兼容,优先用 avatarFilePath) */
avatarImage?: string
type?: number type?: number
/** 微信号 */ /** 新接口文档暂无此字段,传了也可能被忽略 */
wchart?: string wchart?: string
/** 手机号 */ }
phone?: string
}

91
src/types/magdev.ts

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

82
src/types/recharge.ts

@ -2,26 +2,11 @@
* 充值相关类型定义 * 充值相关类型定义
*/ */
/**
* 创建充值订单请求参数
*/
export interface CreateRechargePaymentDto { export interface CreateRechargePaymentDto {
/** 用户ID */
userId?: string
/** 客户编码(充值对象) */
customerSn?: string
/** 充值金额 */ /** 充值金额 */
amount?: number amount?: number
/** 赠送金额 */ /** 店铺编码(必填) */
giftAmount?: number
/** 充值类型 */
rcgType?: number
/** 交易类型:1-JSAPI/小程序 */
tradeType?: number
/** 店铺编码 */
bpSn?: string bpSn?: string
/** 机构编码 */
instSn?: string
/** OpenId(微信支付必传) */ /** OpenId(微信支付必传) */
openId?: string openId?: string
/** 支付标题 */ /** 支付标题 */
@ -30,23 +15,13 @@ export interface CreateRechargePaymentDto {
body?: string body?: string
} }
/**
* 创建充值订单响应(data 字段内容)
*/
export interface RechargePaymentResponseDto { export interface RechargePaymentResponseDto {
/** 商户订单号 */
outTradeNo: string outTradeNo: string
/** 支付渠道:1-微信、2-支付宝等 */
channel: number channel: number
/** 调起微信支付所需参数 */
payParameters: RechargeWechatPayParameters | null payParameters: RechargeWechatPayParameters | null
/** 支付链接(Native扫码支付用) */
payUrl: string payUrl: string
} }
/**
* 微信支付参数(充值场景)
*/
export interface RechargeWechatPayParameters { export interface RechargeWechatPayParameters {
timeStamp: string timeStamp: string
nonceStr: string nonceStr: string
@ -55,7 +30,54 @@ export interface RechargeWechatPayParameters {
paySign: string paySign: string
} }
/** export interface RechargeStatusResult {
* 查询支付状态的支付渠道枚举 amount: number
*/ giftAmount: number
export type PaymentChannel = 'WechatPay' | 'Alipay' | 'UnionPay' | 'Balance' | 'BankCard' balance: number
}
export interface RechargeRuleItem {
recharge: number
gift: number
desc: string
}
export interface RechargeRuleQueryResult {
instName: string
bpName: string
rules: RechargeRuleItem[]
}
export interface RechargeRecordQueryParams {
customerSn: string
startDate?: string
endDate?: string
payType?: number
bpSn?: string
pageIndex?: number
pageSize?: number
}
export interface RechargeRecordItem {
id: number
customerSn: string
customerName: string
amount: number
giftAmount: number
payType: number
rcgType: number
rcgTypeName: string
bpSn: string
bpName: string
instSn: string
instName: string
logTime: string
updateTime: string
}
export interface RechargeRecordPagedResult {
data: RechargeRecordItem[]
totalCount: number
pageIndex: number
pageSize: number
}

4
src/types/startDevice.ts

@ -8,6 +8,8 @@
* 注意:余额开台通过 ctrlType=DepositStart + payType=Balance 组合实现 * 注意:余额开台通过 ctrlType=DepositStart + payType=Balance 组合实现
*/ */
export enum CtrlType { export enum CtrlType {
/** 分享球台 / 取消分享(toggle) */
Share = 0,
/** 押金开台(余额开台时配合 payType=Balance 使用) */ /** 押金开台(余额开台时配合 payType=Balance 使用) */
DepositStart = 1, DepositStart = 1,
/** 定时开台 */ /** 定时开台 */
@ -277,6 +279,8 @@ export interface TableScanResponseDto {
shopName: string shopName: string
shopCode: string shopCode: string
instSn: string instSn: string
/** 店长/门店联系电话(来自 qrcode.phone) */
phone: string
tableInfo: TableInfoDto tableInfo: TableInfoDto
price: PriceInfoDto price: PriceInfoDto
businessHours: string businessHours: string

24
src/utils/apiNormalize.ts

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

15
src/utils/callShopPhone.ts

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

40
src/utils/customerNormalize.ts

@ -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') ?? '') }
}

135
src/utils/operateNormalize.ts

@ -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'),
}))
: [],
}
}

69
src/utils/rechargeNormalize.ts

@ -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) : [],
}
}

117
src/utils/scanNormalize.ts

@ -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')),
}
}

41
src/utils/storeNormalize.ts

@ -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…
Cancel
Save