Browse Source

对接部分接口

v2.0
DU 3 months ago
parent
commit
3282fd7bbd
  1. 96
      src/api/basicInfo.ts
  2. 264
      src/api/customer.ts
  3. 22
      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. 73
      src/pageScan/closeTable/components/FooterButtons.tsx
  11. 7
      src/pageScan/closeTable/index.tsx
  12. 68
      src/pageScan/couponStart/index.tsx
  13. 57
      src/pageScan/renewFee/index.tsx
  14. 76
      src/pageScan/selfStart/index.tsx
  15. 74
      src/pageScan/startTable/index.tsx
  16. 67
      src/pageScan/timedStart/index.tsx
  17. 61
      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. 32
      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 @@ @@ -1,8 +1,10 @@
/**
* 基础信息相关 API
* 接口地址:/api/v2/BasicInfo/
* 新接口路径:/api/v2/basicinfo/
*/
import { get } from '../services/request'
import { normalizeStoreList } from '../utils/storeNormalize'
import { pickField, toNumber } from '../utils/apiNormalize'
/**
* 门店(场所)信息
@ -28,9 +30,95 @@ export interface StoreInfoDto { @@ -28,9 +30,95 @@ export interface StoreInfoDto {
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 getStoreList = () => {
return get<StoreInfoDto[]>('/BasicInfo/GetAllBpInfo')
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 queryBpDevices = async (sn: string, type: 0 | 1) => {
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 @@ @@ -1,49 +1,271 @@
/**
* 客户信息相关 API
*/
import { get, put } from '../services/request'
import Taro from '@tarojs/taro'
import config from '../config'
import { get, post } from '../services/request'
import { CustomerDto, UpdateCustomerDto } from '../types/login'
import { normalizeCustomer } from '../utils/customerNormalize'
import { pickField, toNumber } from '../utils/apiNormalize'
import { userStore } from '../store'
export interface CustomerBalanceResult {
amount: number
}
interface ResponseResult<T = unknown> {
message: string
code: number
success: boolean
data: T
}
/**
* 根据 OpenId 获取客户信息
*/
export const getCustomerByOpenId = async (openId: string) => {
const res = await get<Record<string, unknown>>(
`/customer/byopenid/${encodeURIComponent(openId)}`
)
return { ...res, data: normalizeCustomer(res.data as Record<string, unknown>) }
}
/**
* 根据 OpenId 获取客户信息(含最新余额、会员类型等)
* 登录后或页面刷新时调用,比 getCustomerBySn 更通用(无需 sn)
* @param openId 微信 OpenId
* 查询客户在指定门店的余额
*/
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 }
}
/**
* 刷新用户资料 + 余额(有 bpSn 时)
*/
export const getCustomerByOpenId = (openId: string) => {
return get<CustomerDto>(`/Customer/${openId}`)
export const refreshCustomerProfile = async (openId: string, bpSn?: string) => {
const profileRes = await getCustomerByOpenId(openId)
if (profileRes.data) {
userStore.setUserInfoFromWechat(profileRes.data)
}
const storeSn = bpSn || profileRes.data?.bpSn || userStore.userInfo?.bpSn
if (storeSn) {
try {
const balanceRes = await getCustomerBalance(openId, storeSn)
userStore.setBalance(balanceRes.data.amount)
if (userStore.userInfo) {
userStore.setUserInfo({
...userStore.userInfo,
amount: balanceRes.data.amount,
})
}
} catch (e) {
console.warn('刷新余额失败:', e)
}
}
return profileRes
}
/**
* 根据客户 SN 获取客户详情(含最新余额、会员类型等)
* @param sn 客户编码(CustomerDto.sn)
* multipart 上传头像(小程序 uploadFile 仅支持 POST)
*/
export const getCustomerBySn = (sn: string) => {
return get<CustomerDto>(`/Customer/sn/${sn}`)
const uploadCustomerWithAvatar = (
customerId: number,
dto: UpdateCustomerDto
): Promise<ResponseResult<CustomerDto>> => {
return new Promise((resolve, reject) => {
const openId = Taro.getStorageSync('openId') as string
const formData: Record<string, string> = { id: String(customerId) }
if (dto.name) formData.name = dto.name
if (dto.phone) formData.phone = dto.phone
if (dto.birthday) formData.birthday = dto.birthday
Taro.uploadFile({
url: `${config.apiPrefix}/customer/update`,
filePath: dto.avatarFilePath!,
name: 'avatarimage',
formData,
header: openId ? { 'X-OpenId': openId } : {},
success: (res) => {
try {
const body = JSON.parse(res.data) as ResponseResult<Record<string, unknown>>
if (body.success) {
resolve({ ...body, data: normalizeCustomer(body.data) })
} else {
reject(body)
}
} catch (err) {
reject(err)
}
},
fail: reject,
})
})
}
/**
* 更新客户信息(昵称、头像、手机号、微信号等)
* @param customerId 客户 ID(CustomerDto.id)
* @param dto 要更新的字段
* 更新客户信息(昵称、头像、手机号等)
*/
export const updateCustomer = (customerId: number, dto: UpdateCustomerDto) => {
return put<void>(`/Customer/${customerId}`, dto)
export const updateCustomer = async (customerId: number, dto: UpdateCustomerDto) => {
if (dto.avatarFilePath) {
return uploadCustomerWithAvatar(customerId, dto)
}
const body: Record<string, string> = { id: String(customerId) }
if (dto.name !== undefined) body.name = dto.name
if (dto.phone !== undefined) body.phone = dto.phone
if (dto.birthday !== undefined) body.birthday = dto.birthday
const res = await post<Record<string, unknown>>('/customer/update', body)
return { ...res, data: normalizeCustomer(res.data) }
}
/**
* 读取本地图片并转为 Base64 字符串
* 用于将选择的头像图片编码后传给更新接口
* @param filePath 本地临时文件路径(如 wx.chooseImage 返回的路径)
* @returns Base64 字符串(不含 data:image/xxx;base64, 前缀)
* @deprecated 新接口使用 multipart avatarFilePath
*/
export const readImageAsBase64 = (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
const fs = wx.getFileSystemManager()
const fs = Taro.getFileSystemManager()
fs.readFile({
filePath,
encoding: 'base64',
success: (res) => resolve(res.data as string),
fail: (err) => reject(err),
fail: (err: TaroGeneral.CallbackResult) => reject(err),
})
})
}

22
src/api/login.ts

@ -3,17 +3,27 @@ @@ -3,17 +3,27 @@
*/
import request from '../services/request'
import { WechatLoginRequest, WechatAuthResponse } from '../types/login'
import { normalizeLoginData } from '../utils/customerNormalize'
/**
* 微信登录
* @param code 微信登录凭证
* 微信登录:code → openId + 是否新用户(资料建议再调 byopenid)
*/
export const wechatLogin = (code: string): Promise<WechatAuthResponse> => {
export const wechatLogin = async (code: string): Promise<WechatAuthResponse> => {
const data: WechatLoginRequest = { code }
return request({
url: '/WechatAuth/login-wechat',
const res = await request({
url: '/wechatauth/login-wechat',
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 @@ @@ -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) => { @@ -13,8 +13,7 @@ export const queryPaymentResult = (dto: QueryPaymentRequest) => {
}
/**
* 关闭支付订单(用户取消或支付失败时调用,避免产生悬挂订单)
* @param outTradeNo 商户订单号
* @deprecated 新接口不对接关单,取消支付由后端自行处理
*/
export const closeOrder = (outTradeNo: string) => {
return post<boolean>('/Order/close', { outTradeNo } as CloseOrderDto)

60
src/api/payment.ts

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/**
* 支付渠道相关 API
*/
import { get } from '../services/request'
import { pickField, toNumber } from '../utils/apiNormalize'
export interface PaymentChannelItem {
/** 渠道编码:1-微信 */
code: number
name: string
enabled: boolean
}
/** 微信支付渠道码 */
export const PAY_CHANNEL_WECHAT = 1
/**
* 获取机构支持的支付方式
* 文档暂无响应体定义,按常见结构归一化;仅微信时使用 code=1
*/
export const getPaymentChannels = async (instSn: string) => {
const res = await get<unknown>('/payment/channels', { instsn: instSn }, { showError: false })
let list: PaymentChannelItem[] = []
const raw = res.data
if (Array.isArray(raw)) {
list = raw.map((item) => normalizeChannelItem(item as Record<string, unknown>))
} else if (raw && typeof raw === 'object') {
const channels = pickField<Record<string, unknown>[]>(
raw as Record<string, unknown>,
'channels',
'data'
)
if (Array.isArray(channels)) {
list = channels.map(normalizeChannelItem)
}
}
return { ...res, data: list }
}
const normalizeChannelItem = (raw: Record<string, unknown>): PaymentChannelItem => ({
code: toNumber(pickField(raw, 'code', 'paytype', 'channel')),
name: String(pickField(raw, 'name', 'channelName', 'channelname') ?? ''),
enabled: pickField(raw, 'enabled', 'available') !== false,
})
/**
* 是否支持微信支付(本期仅对接微信)
*/
export const isWechatPaySupported = async (instSn: string): Promise<boolean> => {
try {
const res = await getPaymentChannels(instSn)
if (!res.data.length) return true
return res.data.some((item) => item.code === PAY_CHANNEL_WECHAT && item.enabled)
} catch {
return true
}
}

147
src/api/recharge.ts

@ -1,54 +1,153 @@ @@ -1,54 +1,153 @@
/**
* 充值相关 API
*/
import { get, post } from '../services/request'
import request, { get, post } from '../services/request'
import {
CreateRechargePaymentDto,
RechargePaymentResponseDto,
RechargeWechatPayParameters,
PaymentChannel,
RechargeStatusResult,
RechargeRecordQueryParams,
RechargeRecordPagedResult,
} from '../types/recharge'
import {
normalizeRechargePaymentResponse,
normalizeRechargeRules,
normalizeRechargeStatus,
normalizeWechatPayParams,
} from '../utils/rechargeNormalize'
import { pickField, toNumber } from '../utils/apiNormalize'
/** 小程序 JSAPI 支付固定交易类型 */
const MINI_PROGRAM_TRADE_TYPE = 1
/** 小程序 JSAPI 支付 */
const PAY_TYPE_WECHAT = 1
const TRADE_TYPE_JSAPI = 1
/**
* 创建充值订单
* @param dto 充值请求参数
* @returns 充值支付响应(含微信支付参数)
*/
export const createRechargeOrder = (dto: CreateRechargePaymentDto) => {
return post<RechargePaymentResponseDto>('/Recharge/create', {
tradeType: MINI_PROGRAM_TRADE_TYPE,
...dto,
}, {
export const createRechargeOrder = async (dto: CreateRechargePaymentDto) => {
const body = {
openid: dto.openId,
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,
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) => {
return get<boolean>(`/Recharge/query/${outTradeNo}`, channel ? { channel } : undefined)
export const getRechargeRules = async (bpSn: string) => {
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> => {
const params = normalizeWechatPayParams(payParams as unknown as Record<string, unknown>) || payParams
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType as 'MD5' | 'HMAC-SHA256' | 'RSA',
paySign: payParams.paySign,
timeStamp: params.timeStamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType as 'MD5' | 'HMAC-SHA256' | 'RSA',
paySign: params.paySign,
success: () => resolve(),
fail: (err) => reject(err),
})

6
src/api/startDevice.ts

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
* 关台/续费/关灯重开接口:/api/v2/TablesDev/stop
*/
import { get, post } from '../services/request'
import { normalizeTableScanResponse } from '../utils/scanNormalize'
import {
StartGroupDevRequestDto,
StartDeviceResponse,
@ -204,8 +205,9 @@ export const turnOffRestart = (grpSn: string, openId: string) => { @@ -204,8 +205,9 @@ export const turnOffRestart = (grpSn: string, openId: string) => {
* @param openId 用户 OpenId
* @param source 来源(可选)
*/
export const getTableInfo = (sn: string, openId?: string, source?: string) => {
return get<TableScanResponseDto>('/TablesDev/qrcode', { sn, openId, source })
export const getTableInfo = async (sn: string, openId?: string, source?: string) => {
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 @@ @@ -1,66 +1,126 @@
import { PropsWithChildren } from 'react'
import { useLaunch } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { userStore } from './store'
import { getCustomerByOpenId } from './api/customer'
import { refreshCustomerProfile } from './api/customer'
import { wechatLogin } from './api/login'
import './app.scss'
function App({ children }: PropsWithChildren<any>) {
useLaunch(() => {
console.log('台球馆小程序启动')
// 先从本地缓存恢复用户信息(快速恢复登录态)
try {
const savedUserInfo = Taro.getStorageSync('userInfo')
if (savedUserInfo) {
userStore.setUserInfo(savedUserInfo)
}
} catch (error) {
console.warn('初始化用户信息失败:', error)
}
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (openId) {
// 已有 openId:后台静默刷新最新用户信息(余额、会员等),不阻塞启动
getCustomerByOpenId(openId)
.then(res => {
if (res.data) {
userStore.setUserInfoFromWechat(res.data)
}
})
refreshCustomerProfile(openId)
.catch(e => console.warn('启动刷新用户信息失败:', e))
.finally(() => userStore.setLoginReady())
} else {
// 无本地 openId:执行无感登录,wx.login() 不需要用户任何操作
Taro.login({
success: async ({ code }) => {
try {
const res = await wechatLogin(code)
if (res.success && res.data) {
if (res.success && res.data?.customer) {
userStore.setUserInfoFromWechat(res.data.customer)
// 新用户:触发头像/昵称补全弹窗
if (res.data.isNewCustomer) {
userStore.setIsNewUser(true)
}
const wxOpenId = res.data.customer.wxOpenId
if (wxOpenId) {
await refreshCustomerProfile(wxOpenId)
}
}
} catch (e) {
console.warn('无感登录失败:', e)
} finally {
// 无论成功失败,都标记登录流程结束,让 qrHandler 继续处理
userStore.setLoginReady()
}
},
fail: (err) => {
console.warn('wx.login 调用失败:', err)
// 登录失败也要标记结束,避免 qrHandler 永久等待
userStore.setLoginReady()
}
})
}
})
return children
}
export default App

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

@ -4,15 +4,15 @@ @@ -4,15 +4,15 @@
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState } from 'react'
import { immediateClose, invokeWechatPay, updateShareStatus } from '../../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../../api/order'
import { invokeWechatPay, PayType } from '../../../api/startDevice'
import { operate, CtrlType } from '../../../api/magdev'
import { userStore, tableStore } from '../../../store'
import './FooterButtons.scss'
interface FooterButtonsProps {
type: 'owner' | 'guest'
grpSn: string
operationId: number
operationId?: number
/** 是否从支付完成后跳转而来,true 时只显示「立即关台」 */
fromPayment?: boolean
}
@ -59,40 +59,29 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer @@ -59,40 +59,29 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer
setLoading(true)
try {
console.log('立即关台,设备编码:', grpSn, '操作记录ID:', operationId)
// 调用立即关台接口
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)
const { operationId: responseOperationId, outTradeNo, needPay, payParameters } = result.data
const payParams = result.data.paymentResponseDto?.payParameters
// 判断是否需要支付(可能需要补差价)
if (needPay && payParameters) {
// 调起微信支付
if (payParams) {
// 需要微信支付(补差价场景)
try {
await invokeWechatPay(payParameters)
// 轮询服务端确认支付结果(responseOperationId 对应后端 operationId,需确认与 consumptionId 语义一致)
Taro.showLoading({ title: '确认支付中...', mask: true })
const paid = await pollTablePaymentStatus(responseOperationId)
Taro.hideLoading()
if (paid) {
await invokeWechatPay(payParams)
// 关台支付成功后无需额外查询,直接清场
tableStore.clearAll()
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) {
console.log('支付错误:', payError)
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else {
@ -100,13 +89,13 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer @@ -100,13 +89,13 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer
}
}
} else {
// 无需支付(押金关台场景)
// 无需支付(余额关台 / 押金退还场景)
tableStore.clearAll()
Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 })
setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000)
}
} catch (error: any) {
console.error('关台失败:', error)
// 错误提示由 request 拦截器处理
} finally {
setLoading(false)
}
@ -140,19 +129,23 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer @@ -140,19 +129,23 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer
})
}
// 分享球台
// 分享球台(toggle:开启/取消分享)
const handleShareTable = async () => {
const orderNo = tableStore.scanInfo?.opInfo?.orderNo
if (!orderNo) {
Taro.showToast({ title: '订单编号暂不可用,请稍后再试', icon: 'none', duration: 2000 })
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (!openId || !grpSn) {
Taro.showToast({ title: '信息异常,请重新扫码', icon: 'none', duration: 2000 })
return
}
try {
await updateShareStatus(orderNo)
Taro.showToast({ title: '已开启分享', icon: 'success', duration: 2000 })
} catch (_) {
// 错误由 request 拦截器处理
}
await operate({
grpSn,
openId,
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' @@ -8,6 +8,7 @@ import { observer } from 'mobx-react'
import { ArrowDown } from '@taroify/icons'
import FooterButtons from './components/FooterButtons'
import { tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss'
// 使用本地图片资源
@ -39,11 +40,7 @@ const CloseTable = observer(() => { @@ -39,11 +40,7 @@ const CloseTable = observer(() => {
// 拨打电话
const handlePhoneCall = () => {
Taro.makePhoneCall({
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
callShopPhone(scanInfo?.phone)
}
// 切换展开/收起

68
src/pageScan/couponStart/index.tsx

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

57
src/pageScan/renewFee/index.tsx

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

76
src/pageScan/selfStart/index.tsx

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

74
src/pageScan/startTable/index.tsx

@ -3,9 +3,12 @@ @@ -3,9 +3,12 @@
*/
import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react'
import { observer } from 'mobx-react'
import { CtrlType } from '../../api/startDevice'
import { tableStore } from '../../store'
import { CtrlType, getTableInfo } from '../../api/startDevice'
import { preOperate } from '../../api/magdev'
import { userStore, tableStore } from '../../store'
import { callShopPhone } from '../../utils/callShopPhone'
import './index.scss'
const phoneIcon = require('./images/phone.png')
@ -22,6 +25,7 @@ const StartTable = observer(() => { @@ -22,6 +25,7 @@ const StartTable = observer(() => {
const router = useRouter()
const grpSn = router.params.grpSn || tableStore.currentGrpSn
const scanInfo = tableStore.scanInfo
const [loading, setLoading] = useState(false)
useLoad(() => {
console.log('开台页面加载, grpSn:', grpSn)
@ -29,17 +33,63 @@ const StartTable = observer(() => { @@ -29,17 +33,63 @@ const StartTable = observer(() => {
})
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) {
Taro.showToast({ title: '当前条件不满足,无法使用该开台方式', icon: 'none', duration: 2000 })
return
}
// 先用 optionCode 匹配,匹配不到再用 type 匹配
if (loading) return
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (!openId) {
Taro.showToast({ title: '请先登录', icon: 'none', duration: 2000 })
return
}
if (!grpSn) {
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} optionCode=${option.optionCode} type=${option.type} → url=${url}`)
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) {
@ -47,6 +97,11 @@ const StartTable = observer(() => { @@ -47,6 +97,11 @@ const StartTable = observer(() => {
} else {
Taro.showToast({ title: '功能开发中', icon: 'none', duration: 2000 })
}
} catch (error: any) {
console.error('preOperate 失败:', error)
} finally {
setLoading(false)
}
}
const price = scanInfo?.price
@ -105,16 +160,17 @@ const StartTable = observer(() => { @@ -105,16 +160,17 @@ const StartTable = observer(() => {
</View>
<View className="method-right">
<View
className={`start-btn ${!option.available ? 'disabled' : ''}`}
className={`start-btn ${(!option.available || loading) ? 'disabled' : ''}`}
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 className="method-item">
<Text className="method-desc">加载中...</Text>
</View>

67
src/pageScan/timedStart/index.tsx

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

61
src/pageScan/turnOffRestart/index.tsx

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

17
src/pages/index/index.tsx

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

4
src/pages/store/index.tsx

@ -210,7 +210,7 @@ const StoreIndex = observer(() => { @@ -210,7 +210,7 @@ const StoreIndex = observer(() => {
// 跳转到店铺详情
const clickStoreItem = (store: StoreInfoDto) => {
Taro.navigateTo({
url: '/pageStore/storeDetail/index?id=' + store.id
url: '/pageStore/storeDetail/index?sn=' + store.sn
})
}
@ -269,7 +269,7 @@ const StoreIndex = observer(() => { @@ -269,7 +269,7 @@ const StoreIndex = observer(() => {
<View className="list-footer"><Text className="footer-text">暂无门店数据</Text></View>
) : (
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">
<Text className="store-name">{store.name}</Text>

7
src/pages/user/index.tsx

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

89
src/pagesOrder/recharge/index.tsx

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

29
src/pagesUser/editProfile/index.tsx

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

32
src/types/login.ts

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

91
src/types/magdev.ts

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
/**
* magdev(开台/关台)接口相关类型定义
* 对应接口:/api/v2/magdev/peroperate、/api/v2/magdev/operate、/api/v2/magdev/getopstatus
*/
import { PriceInfoDto, CtrlType, PayType, WechatPayParameters } from './startDevice'
/** 去开台(预开台)请求参数 */
export interface PreOperateRequestDto {
type: number
grpSn: string
openId: string
}
/** 支付方式DTO(peroperate 返回) */
export interface PaymentMethodDto {
type: number
name: string
available: boolean
balanceDisplay: string | null
}
/** 设备简要信息(peroperate 返回) */
export interface DevInfoDto {
id: number
sn: string
name: string
number: string
wstatus: number
status: number
shared: number
}
/** 去开台响应数据(对应 data 字段) */
export interface PreOperateResponseData {
/** 余额充足时直接开台返回订单号,为空表示需进子页支付 */
outTradeNo: string
message: string
isMember: boolean
balance: number
bpName: string
bpSn: string
instSn: string
devType: number
devInfo: DevInfoDto
price: PriceInfoDto
paymentMethods: PaymentMethodDto[]
}
/** operate 统一操作请求参数 */
export interface OperateRequestDto {
grpSn: string
openId: string
ctrlType: CtrlType
amount: number
payType: PayType
tradeType?: number
couponCode?: string
remark?: string
attach?: string
}
/** operate 响应中的支付信息 */
export interface OperatePaymentResponseDto {
outTradeNo: string
channel: number
payParameters: WechatPayParameters | null
payUrl: string
}
/** operate 响应数据(对应 data 字段) */
export interface OperateResponseData {
consumptionId: number
errorMessage: string
paymentResponseDto: OperatePaymentResponseDto | null
}
/** getopstatus 响应中的操作选项 */
export interface OpenOptionItem {
title: string
type: number
desc: string
available: boolean
}
/** getopstatus 响应数据(对应 data 字段) */
export interface GetOpStatusResponseData {
isOpened: boolean
openOptions: OpenOptionItem[]
}
export { CtrlType, PayType }

82
src/types/recharge.ts

@ -2,26 +2,11 @@ @@ -2,26 +2,11 @@
* 充值相关类型定义
*/
/**
* 创建充值订单请求参数
*/
export interface CreateRechargePaymentDto {
/** 用户ID */
userId?: string
/** 客户编码(充值对象) */
customerSn?: string
/** 充值金额 */
amount?: number
/** 赠送金额 */
giftAmount?: number
/** 充值类型 */
rcgType?: number
/** 交易类型:1-JSAPI/小程序 */
tradeType?: number
/** 店铺编码 */
/** 店铺编码(必填) */
bpSn?: string
/** 机构编码 */
instSn?: string
/** OpenId(微信支付必传) */
openId?: string
/** 支付标题 */
@ -30,23 +15,13 @@ export interface CreateRechargePaymentDto { @@ -30,23 +15,13 @@ export interface CreateRechargePaymentDto {
body?: string
}
/**
* 创建充值订单响应(data 字段内容)
*/
export interface RechargePaymentResponseDto {
/** 商户订单号 */
outTradeNo: string
/** 支付渠道:1-微信、2-支付宝等 */
channel: number
/** 调起微信支付所需参数 */
payParameters: RechargeWechatPayParameters | null
/** 支付链接(Native扫码支付用) */
payUrl: string
}
/**
* 微信支付参数(充值场景)
*/
export interface RechargeWechatPayParameters {
timeStamp: string
nonceStr: string
@ -55,7 +30,54 @@ export interface RechargeWechatPayParameters { @@ -55,7 +30,54 @@ export interface RechargeWechatPayParameters {
paySign: string
}
/**
* 查询支付状态的支付渠道枚举
*/
export type PaymentChannel = 'WechatPay' | 'Alipay' | 'UnionPay' | 'Balance' | 'BankCard'
export interface RechargeStatusResult {
amount: number
giftAmount: number
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 @@ @@ -8,6 +8,8 @@
* 注意:余额开台通过 ctrlType=DepositStart + payType=Balance 组合实现
*/
export enum CtrlType {
/** 分享球台 / 取消分享(toggle) */
Share = 0,
/** 押金开台(余额开台时配合 payType=Balance 使用) */
DepositStart = 1,
/** 定时开台 */
@ -277,6 +279,8 @@ export interface TableScanResponseDto { @@ -277,6 +279,8 @@ export interface TableScanResponseDto {
shopName: string
shopCode: string
instSn: string
/** 店长/门店联系电话(来自 qrcode.phone) */
phone: string
tableInfo: TableInfoDto
price: PriceInfoDto
businessHours: string

24
src/utils/apiNormalize.ts

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
/**
* 新接口字段大小写兼容:Word/Swagger 多为全小写,现网前端用 camelCase
*/
export const pickField = <T = unknown>(
source: Record<string, any> | null | undefined,
...keys: string[]
): T | undefined => {
if (!source) return undefined
for (const key of keys) {
if (source[key] !== undefined && source[key] !== null) {
return source[key] as T
}
}
return undefined
}
export const toNumber = (value: unknown, fallback = 0): number => {
if (typeof value === 'number' && !Number.isNaN(value)) return value
if (typeof value === 'string' && value.trim() !== '') {
const n = Number(value)
return Number.isNaN(n) ? fallback : n
}
return fallback
}

15
src/utils/callShopPhone.ts

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
/**
* 拨打门店/店长电话(来自扫码 qrcode.phone)
*/
import Taro from '@tarojs/taro'
export const callShopPhone = (phone?: string | null): void => {
const phoneNumber = (phone || '').trim()
if (!phoneNumber) {
Taro.showToast({ title: '暂无店长电话', icon: 'none', duration: 2000 })
return
}
Taro.makePhoneCall({ phoneNumber }).catch((err) => {
console.error('拨打电话失败:', err)
})
}

40
src/utils/customerNormalize.ts

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/**
* 客户字段归一化(新接口全小写 / 旧 camelCase)
*/
import { CustomerDto } from '../types/login'
import { pickField, toNumber } from '../utils/apiNormalize'
export const normalizeCustomer = (raw: Record<string, any> | null | undefined): CustomerDto => {
const src = raw || {}
return {
id: toNumber(pickField(src, 'id')),
sn: String(pickField(src, 'sn') ?? ''),
wxOpenId: String(pickField(src, 'wxOpenId', 'wxopenid') ?? ''),
name: String(pickField(src, 'name') ?? ''),
type: toNumber(pickField(src, 'type')),
typeName: String(pickField(src, 'typeName', 'typename') ?? ''),
amount: toNumber(pickField(src, 'amount')),
wchart: String(pickField(src, 'wchart') ?? ''),
phone: String(pickField(src, 'phone') ?? ''),
bpSn: String(pickField(src, 'bpSn', 'bpsn') ?? ''),
bpInfoName: String(pickField(src, 'bpInfoName', 'bpinfoname', 'bpname') ?? ''),
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''),
ownerName: String(pickField(src, 'ownerName', 'ownername') ?? ''),
createTime: String(pickField(src, 'createTime', 'createtime') ?? ''),
lastLoginTime: String(pickField(src, 'lastLoginTime', 'lastlogintime') ?? ''),
wxNickName: String(pickField(src, 'wxNickName', 'wxnickname', 'name') ?? ''),
wxAvatarUrl: String(pickField(src, 'wxAvatarUrl', 'wxavatarurl') ?? ''),
birthday: pickField<string>(src, 'birthday'),
}
}
/** 登录 data:扁平(Word)或嵌套 customer(旧) */
export const normalizeLoginData = (data: Record<string, any> | null | undefined) => {
const src = data || {}
const nested = pickField<Record<string, any>>(src, 'customer')
const customer = normalizeCustomer(nested || src)
const isNewCustomer = Boolean(
pickField(src, 'isNewCustomer', 'isnewcustomer') ?? false
)
return { customer, isNewCustomer, message: String(pickField(src, 'message') ?? '') }
}

135
src/utils/operateNormalize.ts

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
/**
* magdev operate / peroperate / getopstatus 响应字段归一化
*/
import { pickField, toNumber } from './apiNormalize'
import { normalizeWechatPayParams } from './rechargeNormalize'
import { PriceInfoDto, TimePriceDto } from '../types/startDevice'
import {
OperateResponseData,
OperatePaymentResponseDto,
PreOperateResponseData,
PaymentMethodDto,
DevInfoDto,
GetOpStatusResponseData,
OpenOptionItem,
} from '../types/magdev'
const normalizeTimePrice = (raw: Record<string, any>): TimePriceDto => ({
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''),
endTime: String(pickField(raw, 'endTime', 'endtime') ?? ''),
price: toNumber(pickField(raw, 'price')),
description: String(pickField(raw, 'description') ?? ''),
})
const normalizePrice = (raw: Record<string, any> | null | undefined): PriceInfoDto => {
const src = raw || {}
const timePrices = pickField<Record<string, any>[]>(src, 'timePrices', 'timeprices') || []
return {
standard: toNumber(pickField(src, 'standard')),
memberPrice: toNumber(pickField(src, 'memberPrice', 'memberprice')),
unit: String(pickField(src, 'unit') ?? ''),
deposit: toNumber(pickField(src, 'deposit')),
pricePerMinute: toNumber(pickField(src, 'pricePerMinute', 'pricePerminute', 'priceperminute')),
memberPricePerMinute: toNumber(pickField(src, 'memberPricePerMinute', 'memberpriceperminute')),
currentPrice: toNumber(pickField(src, 'currentPrice', 'currentprice')),
descript: String(pickField(src, 'descript') ?? ''),
pricePeriod: String(pickField(src, 'pricePeriod', 'priceperiod') ?? ''),
timePrices: Array.isArray(timePrices) ? timePrices.map(normalizeTimePrice) : [],
}
}
const normalizeDevInfo = (raw: Record<string, any> | null | undefined): DevInfoDto => {
const src = raw || {}
return {
id: toNumber(pickField(src, 'id')),
sn: String(pickField(src, 'sn') ?? ''),
name: String(pickField(src, 'name') ?? ''),
number: String(pickField(src, 'number') ?? ''),
wstatus: toNumber(pickField(src, 'wstatus')),
status: toNumber(pickField(src, 'status')),
shared: toNumber(pickField(src, 'shared')),
}
}
const normalizePaymentMethods = (raw: any[]): PaymentMethodDto[] => {
if (!Array.isArray(raw)) return []
return raw.map(item => ({
type: toNumber(pickField(item, 'type')),
name: String(pickField(item, 'name') ?? ''),
available: !!pickField(item, 'available'),
balanceDisplay: pickField<string>(item, 'balanceDisplay', 'balancedisplay') ?? null,
}))
}
const normalizeOperatePaymentResponse = (
raw: Record<string, any> | null | undefined
): OperatePaymentResponseDto | null => {
if (!raw) return null
const payParams = normalizeWechatPayParams(
pickField<Record<string, any>>(raw, 'payParameters', 'payparameters')
)
if (!payParams && !pickField(raw, 'outTradeNo', 'outtradeno')) return null
return {
outTradeNo: String(pickField(raw, 'outTradeNo', 'outtradeno') ?? ''),
channel: toNumber(pickField(raw, 'channel')),
payParameters: payParams,
payUrl: String(pickField(raw, 'payUrl', 'payurl') ?? ''),
}
}
/** 归一化 operate(开台/关台/续费)响应 data */
export const normalizeOperateResponse = (
raw: Record<string, any> | null | undefined
): OperateResponseData => {
const src = raw || {}
return {
consumptionId: toNumber(pickField(src, 'consumptionId', 'consumptionid')),
errorMessage: String(pickField(src, 'errorMessage', 'errormessage') ?? ''),
paymentResponseDto: normalizeOperatePaymentResponse(
pickField<Record<string, any>>(src, 'paymentResponseDto', 'paymentresponsedto')
),
}
}
/** 归一化 peroperate(去开台)响应 data */
export const normalizePreOperateResponse = (
raw: Record<string, any> | null | undefined
): PreOperateResponseData => {
const src = raw || {}
return {
message: String(pickField(src, 'message') ?? ''),
outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''),
isMember: !!pickField(src, 'isMember', 'ismember'),
balance: toNumber(pickField(src, 'balance')),
bpName: String(pickField(src, 'bpName', 'bpname') ?? ''),
bpSn: String(pickField(src, 'bpSn', 'bpsn') ?? ''),
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''),
devType: toNumber(pickField(src, 'devType', 'devtype')),
devInfo: normalizeDevInfo(
pickField<Record<string, any>>(src, 'devInfo', 'devinfo')
),
price: normalizePrice(pickField<Record<string, any>>(src, 'price')),
paymentMethods: normalizePaymentMethods(
pickField<any[]>(src, 'paymentMethods', 'paymentmethods') ?? []
),
}
}
/** 归一化 getopstatus 响应 data */
export const normalizeGetOpStatusResponse = (
raw: Record<string, any> | null | undefined
): GetOpStatusResponseData => {
const src = raw || {}
const opts = pickField<any[]>(src, 'openOptions', 'openoptions') ?? []
return {
isOpened: !!pickField(src, 'isOpened', 'isopened'),
openOptions: Array.isArray(opts)
? opts.map((item): OpenOptionItem => ({
title: String(pickField(item, 'title') ?? ''),
type: toNumber(pickField(item, 'type')),
desc: String(pickField(item, 'desc', 'description') ?? ''),
available: !!pickField(item, 'available'),
}))
: [],
}
}

69
src/utils/rechargeNormalize.ts

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/**
* 充值相关字段归一化
*/
import {
RechargePaymentResponseDto,
RechargeWechatPayParameters,
RechargeStatusResult,
RechargeRuleItem,
RechargeRuleQueryResult,
} from '../types/recharge'
import { pickField, toNumber } from './apiNormalize'
export const normalizeWechatPayParams = (
raw: Record<string, any> | null | undefined
): RechargeWechatPayParameters | null => {
if (!raw) return null
const timeStamp = pickField<string>(raw, 'timeStamp', 'timestamp')
if (!timeStamp) return null
return {
timeStamp: String(timeStamp),
nonceStr: String(pickField(raw, 'nonceStr', 'noncestr') ?? ''),
package: String(pickField(raw, 'package') ?? ''),
signType: String(pickField(raw, 'signType', 'signtype') ?? 'RSA'),
paySign: String(pickField(raw, 'paySign', 'paysign') ?? ''),
}
}
export const normalizeRechargePaymentResponse = (
raw: Record<string, any> | null | undefined
): RechargePaymentResponseDto => {
const src = raw || {}
return {
outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''),
channel: toNumber(pickField(src, 'channel')),
payParameters: normalizeWechatPayParams(
pickField<Record<string, any>>(src, 'payParameters', 'payparameters')
),
payUrl: String(pickField(src, 'payUrl', 'payurl') ?? ''),
}
}
export const normalizeRechargeStatus = (
raw: Record<string, any> | null | undefined
): RechargeStatusResult => {
const src = raw || {}
return {
amount: toNumber(pickField(src, 'amount')),
giftAmount: toNumber(pickField(src, 'giftAmount', 'giftamount')),
balance: toNumber(pickField(src, 'balance')),
}
}
export const normalizeRechargeRuleItem = (raw: Record<string, any>): RechargeRuleItem => ({
recharge: toNumber(pickField(raw, 'recharge')),
gift: toNumber(pickField(raw, 'gift')),
desc: String(pickField(raw, 'desc', 'description') ?? ''),
})
export const normalizeRechargeRules = (
raw: Record<string, any> | null | undefined
): RechargeRuleQueryResult => {
const src = raw || {}
const rules = pickField<Record<string, any>[]>(src, 'rules') || []
return {
instName: String(pickField(src, 'instName', 'instname') ?? ''),
bpName: String(pickField(src, 'bpName', 'bpname') ?? ''),
rules: Array.isArray(rules) ? rules.map(normalizeRechargeRuleItem) : [],
}
}

117
src/utils/scanNormalize.ts

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
/**
* 扫码(qrcode)响应字段归一化
*/
import {
TableScanResponseDto,
TableInfoDto,
PriceInfoDto,
TimePriceDto,
OptionDto,
OpInfoDto,
} from '../types/startDevice'
import { pickField, toNumber } from './apiNormalize'
const normalizeTimePrice = (raw: Record<string, any>): TimePriceDto => ({
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''),
endTime: String(pickField(raw, 'endTime', 'endtime') ?? ''),
price: toNumber(pickField(raw, 'price')),
description: String(pickField(raw, 'description') ?? ''),
})
const normalizePrice = (raw: Record<string, any> | null | undefined): PriceInfoDto => {
const src = raw || {}
const timePrices = pickField<Record<string, any>[]>(src, 'timePrices', 'timeprices') || []
return {
standard: toNumber(pickField(src, 'standard')),
memberPrice: toNumber(pickField(src, 'memberPrice', 'memberprice')),
unit: String(pickField(src, 'unit') ?? ''),
deposit: toNumber(pickField(src, 'deposit')),
pricePerMinute: toNumber(pickField(src, 'pricePerMinute', 'pricePerminute', 'priceperminute')),
memberPricePerMinute: toNumber(
pickField(src, 'memberPricePerMinute', 'memberpriceperminute')
),
currentPrice: toNumber(pickField(src, 'currentPrice', 'currentprice')),
descript: String(pickField(src, 'descript') ?? ''),
pricePeriod: String(pickField(src, 'pricePeriod', 'priceperiod') ?? ''),
timePrices: Array.isArray(timePrices) ? timePrices.map(normalizeTimePrice) : [],
}
}
const normalizeTableInfo = (raw: Record<string, any> | null | undefined): TableInfoDto => {
const src = raw || {}
return {
id: toNumber(pickField(src, 'id')),
sn: String(pickField(src, 'sn', 'grpsn') ?? ''),
name: String(pickField(src, 'name', 'tablename') ?? ''),
tableNumber: String(pickField(src, 'tableNumber', 'tablenumber', 'number') ?? ''),
status: toNumber(pickField(src, 'status', 'wstatus')),
statusDescription: String(
pickField(src, 'statusDescription', 'statusdescription', 'statusdesc') ?? ''
),
isShared: Boolean(pickField(src, 'isShared', 'isshared', 'share')),
currentSession: String(pickField(src, 'currentSession', 'currentsession') ?? ''),
area: String(pickField(src, 'area', 'class') ?? ''),
chanNum: toNumber(pickField(src, 'chanNum', 'channum')),
startChan: toNumber(pickField(src, 'startChan', 'startchan')),
}
}
const normalizeOpenOption = (raw: Record<string, any>): OptionDto => ({
title: String(pickField(raw, 'title') ?? ''),
type: toNumber(pickField(raw, 'type')),
description: String(pickField(raw, 'description', 'desc') ?? ''),
actionText: String(pickField(raw, 'actionText', 'actiontext') ?? ''),
refundPolicy: String(pickField(raw, 'refundPolicy', 'refundpolicy') ?? ''),
available: pickField(raw, 'available') !== false,
optionCode: toNumber(pickField(raw, 'optionCode', 'optioncode', 'type')),
icon: String(pickField(raw, 'icon') ?? ''),
condition: String(pickField(raw, 'condition') ?? ''),
actionUrl: String(pickField(raw, 'actionUrl', 'actionurl') ?? ''),
})
const normalizeOpInfo = (raw: Record<string, any> | null | undefined): OpInfoDto | null => {
if (!raw) return null
return {
opId: toNumber(pickField(raw, 'opId', 'opid')),
orderNo: String(pickField(raw, 'orderNo', 'orderno') ?? ''),
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''),
usedTime: String(pickField(raw, 'usedTime', 'usedtime') ?? ''),
usedMinutes: toNumber(pickField(raw, 'usedMinutes', 'usedminutes')),
ctrlType: toNumber(pickField(raw, 'ctrlType', 'ctrltype')),
ctrlTypeDescription: String(
pickField(raw, 'ctrlTypeDescription', 'ctrltypedescription') ?? ''
),
payType: toNumber(pickField(raw, 'payType', 'paytype')),
payTypeDescription: String(
pickField(raw, 'payTypeDescription', 'paytypedescription') ?? ''
),
currentAmount: toNumber(pickField(raw, 'currentAmount', 'currentamount')),
deposit: toNumber(pickField(raw, 'deposit')),
depositStatus: String(pickField(raw, 'depositStatus', 'depositstatus') ?? ''),
discountAmount: toNumber(pickField(raw, 'discountAmount', 'discountamount')),
payableAmount: toNumber(pickField(raw, 'payableAmount', 'payableamount')),
}
}
export const normalizeTableScanResponse = (
raw: Record<string, any> | null | undefined
): TableScanResponseDto => {
const src = raw || {}
const openOptions = pickField<Record<string, any>[]>(src, 'openOptions', 'openoptions') || []
const devInfo = pickField<Record<string, any>>(src, 'tableInfo', 'devinfo', 'tableinfo')
return {
scanType: (pickField<string>(src, 'scanType', 'scantype') || 'open') as 'open' | 'close',
shopName: String(pickField(src, 'shopName', 'bpname', 'shopname') ?? ''),
shopCode: String(pickField(src, 'shopCode', 'bpsn', 'shopcode') ?? ''),
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''),
phone: String(pickField(src, 'phone') ?? ''),
tableInfo: normalizeTableInfo(devInfo),
price: normalizePrice(pickField(src, 'price')),
businessHours: String(pickField(src, 'businessHours', 'businesshours') ?? ''),
openOptions: Array.isArray(openOptions) ? openOptions.map(normalizeOpenOption) : [],
recommendedOption: String(pickField(src, 'recommendedOption', 'recommendedoption') ?? ''),
scanTime: String(pickField(src, 'scanTime', 'scantime') ?? ''),
opInfo: normalizeOpInfo(pickField(src, 'opInfo', 'opinfo')),
}
}

41
src/utils/storeNormalize.ts

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/**
* 门店列表字段归一化(新接口 BpInfoSimpleDto → 现网 StoreInfoDto)
*/
import { StoreInfoDto } from '../api/basicInfo'
import { pickField, toNumber } from './apiNormalize'
const hashSn = (sn: string): number => {
let h = 0
for (let i = 0; i < sn.length; i++) {
h = (Math.imul(31, h) + sn.charCodeAt(i)) | 0
}
return Math.abs(h)
}
export const normalizeStoreItem = (raw: Record<string, any>): StoreInfoDto => {
const inst = pickField<Record<string, any>>(raw, 'inst') || {}
const sn = String(pickField(raw, 'sn') ?? '')
return {
id: hashSn(sn || String(pickField(raw, 'id') ?? '')),
sn,
name: String(pickField(raw, 'name') ?? ''),
type: toNumber(pickField(raw, 'type')),
typeName: String(pickField(raw, 'typeName', 'typename') ?? ''),
isUnion: toNumber(pickField(raw, 'isUnion', 'isunion')),
address: String(pickField(raw, 'address', 'add') ?? ''),
manager: String(pickField(raw, 'manager') ?? ''),
phone: String(pickField(raw, 'phone') ?? ''),
inst_Sn: String(pickField(inst, 'sn', 'instsn') ?? pickField(raw, 'instSn', 'instsn') ?? ''),
instName: String(pickField(inst, 'name', 'instname') ?? pickField(raw, 'instName', 'instname') ?? ''),
rgn_Sn: String(pickField(raw, 'rgn_Sn', 'rgnsn') ?? ''),
regionName: String(pickField(raw, 'regionName', 'regionname', 'city') ?? pickField(raw, 'city') ?? ''),
longItude: toNumber(pickField(raw, 'longItude', 'longitude')),
latItude: toNumber(pickField(raw, 'latItude', 'latitude')),
createTime: String(pickField(raw, 'createTime', 'createtime') ?? ''),
}
}
export const normalizeStoreList = (list: Record<string, any>[] | null | undefined): StoreInfoDto[] => {
if (!Array.isArray(list)) return []
return list.map(normalizeStoreItem)
}
Loading…
Cancel
Save