From 3282fd7bbdedc92a283630a8d0dabb693114920b Mon Sep 17 00:00:00 2001 From: DU Date: Tue, 21 Jul 2026 10:58:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E9=83=A8=E5=88=86=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/basicInfo.ts | 96 ++++++- src/api/customer.ts | 264 ++++++++++++++++-- src/api/login.ts | 26 +- src/api/magdev.ts | 66 +++++ src/api/order.ts | 3 +- src/api/payment.ts | 60 ++++ src/api/recharge.ts | 147 ++++++++-- src/api/startDevice.ts | 6 +- src/app.ts | 82 +++++- .../closeTable/components/FooterButtons.tsx | 79 +++--- src/pageScan/closeTable/index.tsx | 7 +- src/pageScan/couponStart/index.tsx | 72 ++--- src/pageScan/renewFee/index.tsx | 63 ++--- src/pageScan/selfStart/index.tsx | 78 ++---- src/pageScan/startTable/index.tsx | 88 ++++-- src/pageScan/timedStart/index.tsx | 69 ++--- src/pageScan/turnOffRestart/index.tsx | 67 ++--- src/pages/index/index.tsx | 17 +- src/pages/store/index.tsx | 4 +- src/pages/user/index.tsx | 7 +- src/pagesOrder/recharge/index.tsx | 89 +++--- src/pagesUser/editProfile/index.tsx | 29 +- src/types/login.ts | 34 +-- src/types/magdev.ts | 91 ++++++ src/types/recharge.ts | 82 ++++-- src/types/startDevice.ts | 4 + src/utils/apiNormalize.ts | 24 ++ src/utils/callShopPhone.ts | 15 + src/utils/customerNormalize.ts | 40 +++ src/utils/operateNormalize.ts | 135 +++++++++ src/utils/rechargeNormalize.ts | 69 +++++ src/utils/scanNormalize.ts | 117 ++++++++ src/utils/storeNormalize.ts | 41 +++ 33 files changed, 1625 insertions(+), 446 deletions(-) create mode 100644 src/api/magdev.ts create mode 100644 src/api/payment.ts create mode 100644 src/types/magdev.ts create mode 100644 src/utils/apiNormalize.ts create mode 100644 src/utils/callShopPhone.ts create mode 100644 src/utils/customerNormalize.ts create mode 100644 src/utils/operateNormalize.ts create mode 100644 src/utils/rechargeNormalize.ts create mode 100644 src/utils/scanNormalize.ts create mode 100644 src/utils/storeNormalize.ts diff --git a/src/api/basicInfo.ts b/src/api/basicInfo.ts index 78b1637..a17b60e 100644 --- a/src/api/basicInfo.ts +++ b/src/api/basicInfo.ts @@ -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 { 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): 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): 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 = {} + if (params?.sn) query.sn = params.sn + if (params?.type !== undefined) query.type = params.type + + const res = await get[]>('/basicinfo/getallbpinfo', query) + return { ...res, data: normalizeStoreList(res.data as Record[]) } +} + +/** + * 获取机构列表 + */ +export const getInstList = async (instSn?: string) => { + const res = await get[]>( + '/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 = () => { - return get('/BasicInfo/GetAllBpInfo') +export const queryBpDevices = async (sn: string, type: 0 | 1) => { + const res = await get>('/basicinfo/query', { sn, type }) + const src = res.data || {} + const devs = pickField[]>(src as Record, 'devs') || [] + const data: BpDevQueryResultDto = { + name: String(pickField(src as Record, 'name') ?? ''), + devs: Array.isArray(devs) ? devs.map(normalizeDevItem) : [], + } + return { ...res, data } } diff --git a/src/api/customer.ts b/src/api/customer.ts index df723c5..035d2e9 100644 --- a/src/api/customer.ts +++ b/src/api/customer.ts @@ -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 { + + message: string + + code: number + + success: boolean + + data: T + +} + + + +/** + + * 根据 OpenId 获取客户信息 + + */ + +export const getCustomerByOpenId = async (openId: string) => { + + const res = await get>( + + `/customer/byopenid/${encodeURIComponent(openId)}` + + ) + + return { ...res, data: normalizeCustomer(res.data as Record) } + +} + + + +/** + + * 查询客户在指定门店的余额 + + */ + +export const getCustomerBalance = async (openId: string, bpSn: string) => { + + const res = await get>('/customer/balance', { + + openid: openId, + + bpsn: bpSn, + + }) + + const amount = toNumber(pickField(res.data as Record, 'amount')) + + return { ...res, data: { amount } as CustomerBalanceResult } + +} + + + /** - * 根据 OpenId 获取客户信息(含最新余额、会员类型等) - * 登录后或页面刷新时调用,比 getCustomerBySn 更通用(无需 sn) - * @param openId 微信 OpenId + + * 刷新用户资料 + 余额(有 bpSn 时) + */ -export const getCustomerByOpenId = (openId: string) => { - return get(`/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(`/Customer/sn/${sn}`) + +const uploadCustomerWithAvatar = ( + + customerId: number, + + dto: UpdateCustomerDto + +): Promise> => { + + return new Promise((resolve, reject) => { + + const openId = Taro.getStorageSync('openId') as string + + const formData: Record = { 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> + + 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(`/Customer/${customerId}`, dto) + +export const updateCustomer = async (customerId: number, dto: UpdateCustomerDto) => { + + if (dto.avatarFilePath) { + + return uploadCustomerWithAvatar(customerId, dto) + + } + + + + const body: Record = { 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>('/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 => { 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), }) }) } + + diff --git a/src/api/login.ts b/src/api/login.ts index 20292c7..de94d0d 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -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 => { +export const wechatLogin = async (code: string): Promise => { const data: WechatLoginRequest = { code } - - return request({ - url: '/WechatAuth/login-wechat', + + const res = await request({ + url: '/wechatauth/login-wechat', method: 'POST', - data + data, }) -} \ No newline at end of file + + const normalized = normalizeLoginData(res.data as Record) + return { + ...res, + data: { + customer: normalized.customer, + isNewCustomer: normalized.isNewCustomer, + message: normalized.message, + }, + } +} diff --git a/src/api/magdev.ts b/src/api/magdev.ts new file mode 100644 index 0000000..6ce6564 --- /dev/null +++ b/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>( + '/magdev/peroperate', + { type, grpsn: grpSn, openid: openId }, + { showLoading: true, loadingText: '处理中...' } + ) + return { ...res, data: normalizePreOperateResponse(res.data as Record) } +} + +/** + * 统一操作接口(开台/关台/续费/关灯重开/分享等) + * @param dto 操作请求参数 + */ +export const operate = async (dto: OperateRequestDto) => { + const res = await post>( + '/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) } +} + +/** + * 查询开台状态(微信支付成功后调用一次,不轮询) + * @param sn 设备编码(grpSn) + */ +export const getOpStatus = async (sn: string) => { + const res = await get>('/magdev/getopstatus', { sn }) + return { ...res, data: normalizeGetOpStatusResponse(res.data as Record) } +} + +export { CtrlType, PayType } diff --git a/src/api/order.ts b/src/api/order.ts index 5c7bc61..1eed2c3 100644 --- a/src/api/order.ts +++ b/src/api/order.ts @@ -13,8 +13,7 @@ export const queryPaymentResult = (dto: QueryPaymentRequest) => { } /** - * 关闭支付订单(用户取消或支付失败时调用,避免产生悬挂订单) - * @param outTradeNo 商户订单号 + * @deprecated 新接口不对接关单,取消支付由后端自行处理 */ export const closeOrder = (outTradeNo: string) => { return post('/Order/close', { outTradeNo } as CloseOrderDto) diff --git a/src/api/payment.ts b/src/api/payment.ts new file mode 100644 index 0000000..77ea564 --- /dev/null +++ b/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('/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)) + } else if (raw && typeof raw === 'object') { + const channels = pickField[]>( + raw as Record, + 'channels', + 'data' + ) + if (Array.isArray(channels)) { + list = channels.map(normalizeChannelItem) + } + } + + return { ...res, data: list } +} + +const normalizeChannelItem = (raw: Record): 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 => { + 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 + } +} diff --git a/src/api/recharge.ts b/src/api/recharge.ts index 74e5c29..533a6db 100644 --- a/src/api/recharge.ts +++ b/src/api/recharge.ts @@ -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('/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>('/recharge/create', body, { showLoading: true, loadingText: '创建订单中...', }) + + return { + ...res, + data: normalizeRechargePaymentResponse(res.data as Record), + } +} + +export interface RechargePaymentQueryResult { + /** 是否充值到账 */ + paid: boolean + /** 支付成功但充值未到账,需继续轮询 */ + pending: boolean + data: RechargeStatusResult | null +} + +/** + * 查询充值支付结果(getrcgstatus) + * code 200=到账,204=支付成功但未到账 + */ +export const queryPaymentStatus = async (outTradeNo: string): Promise => { + try { + const res = await request>({ + url: '/recharge/getrcgstatus', + method: 'GET', + data: { outtradeno: outTradeNo }, + showError: false, + }) + + const data = normalizeRechargeStatus(res.data as Record) + 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(`/Recharge/query/${outTradeNo}`, channel ? { channel } : undefined) +export const getRechargeRules = async (bpSn: string) => { + const res = await get>('/recharge/query', { bpsn: bpSn }) + return { + ...res, + data: normalizeRechargeRules(res.data as Record), + } +} + +/** + * 查询充值记录 + */ +export const getRechargeRecords = async (params: RechargeRecordQueryParams) => { + const res = await get>('/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[]>(src as Record, '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, 'totalCount', 'totalcount')), + pageIndex: toNumber(pickField(src as Record, 'pageIndex', 'pageindex'), 1), + pageSize: toNumber(pickField(src as Record, 'pageSize', 'pagesize'), 20), + } + + return { ...res, data } } /** * 调起微信支付(充值场景) - * @param payParams 微信支付参数 */ export const invokeWechatPayForRecharge = (payParams: RechargeWechatPayParameters): Promise => { + const params = normalizeWechatPayParams(payParams as unknown as Record) || 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), }) diff --git a/src/api/startDevice.ts b/src/api/startDevice.ts index 0744787..c853c77 100644 --- a/src/api/startDevice.ts +++ b/src/api/startDevice.ts @@ -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) => { * @param openId 用户 OpenId * @param source 来源(可选) */ -export const getTableInfo = (sn: string, openId?: string, source?: string) => { - return get('/TablesDev/qrcode', { sn, openId, source }) +export const getTableInfo = async (sn: string, openId?: string, source?: string) => { + const res = await get>('/magdev/qrcode', { sn, openId, source }) + return { ...res, data: normalizeTableScanResponse(res.data as Record) } } /** diff --git a/src/app.ts b/src/app.ts index 4e8e148..c32b5a2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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) { + 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 + diff --git a/src/pageScan/closeTable/components/FooterButtons.tsx b/src/pageScan/closeTable/components/FooterButtons.tsx index e94fabe..43ffb65 100644 --- a/src/pageScan/closeTable/components/FooterButtons.tsx +++ b/src/pageScan/closeTable/components/FooterButtons.tsx @@ -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 } @@ -57,42 +57,31 @@ const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: Footer } setLoading(true) - - try { - console.log('立即关台,设备编码:', grpSn, '操作记录ID:', operationId) - // 调用立即关台接口 - const result = await immediateClose(grpSn, openId) + try { + 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) { - Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 }) - setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000) - } else { - Taro.showModal({ - title: '支付确认中', - content: '支付结果尚未到账,请稍后查看订单,如有疑问请联系店长。', - showCancel: false, - confirmText: '知道了', - }) - } + await invokeWechatPay(payParams) + // 关台支付成功后无需额外查询,直接清场 + tableStore.clearAll() + Taro.showToast({ title: '关台成功', icon: 'success', duration: 2000 }) + setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 2000) } 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 } } } 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 }) } - // 分享球台 + // 分享球台(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 (_) {} } // 关灯重开 diff --git a/src/pageScan/closeTable/index.tsx b/src/pageScan/closeTable/index.tsx index eb01337..75ae09f 100644 --- a/src/pageScan/closeTable/index.tsx +++ b/src/pageScan/closeTable/index.tsx @@ -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(() => { // 拨打电话 const handlePhoneCall = () => { - Taro.makePhoneCall({ - phoneNumber: '100-666-XXXX', - }).catch((err) => { - console.error('拨打电话失败:', err) - }) + callShopPhone(scanInfo?.phone) } // 切换展开/收起 diff --git a/src/pageScan/couponStart/index.tsx b/src/pageScan/couponStart/index.tsx index f72046b..874ae4a 100644 --- a/src/pageScan/couponStart/index.tsx +++ b/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 { 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(() => { // 拨打电话 const handlePhoneCall = () => { - Taro.makePhoneCall({ - phoneNumber: '100-666-XXXX', - }).catch((err) => { - console.error('拨打电话失败:', err) - }) + callShopPhone(scanInfo?.phone) } // 扫描券码 @@ -158,54 +155,58 @@ 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) - - // 轮询服务端确认支付结果 - Taro.showLoading({ title: '确认支付中...', mask: true }) - const paid = await pollTablePaymentStatus(consumptionId) + await invokeWechatPay(payParams) + + 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(() => { } } } 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) } diff --git a/src/pageScan/renewFee/index.tsx b/src/pageScan/renewFee/index.tsx index 13b4161..5b26dd9 100644 --- a/src/pageScan/renewFee/index.tsx +++ b/src/pageScan/renewFee/index.tsx @@ -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(() => { // 拨打电话 const handlePhoneCall = () => { - Taro.makePhoneCall({ - phoneNumber: '100-666-XXXX', - }).catch((err) => { - console.error('拨打电话失败:', err) - }) + callShopPhone(scanInfo?.phone) } // 续费金额变化 @@ -94,42 +91,33 @@ const RenewFee = observer(() => { } setLoading(true) - + 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) { - Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 }) - setTimeout(() => Taro.navigateBack(), 2000) - } else { - Taro.showModal({ - title: '支付确认中', - content: '支付结果尚未到账,请稍后刷新页面查看,如有疑问请联系店长。', - showCancel: false, - confirmText: '知道了', - }) - } + 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) } 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(() => { } } } 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) } diff --git a/src/pageScan/selfStart/index.tsx b/src/pageScan/selfStart/index.tsx index 7e0781b..c5fc47c 100644 --- a/src/pageScan/selfStart/index.tsx +++ b/src/pageScan/selfStart/index.tsx @@ -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(() => { // 拨打电话 const handlePhoneCall = () => { - Taro.makePhoneCall({ - phoneNumber: '100-666-XXXX', - }).catch((err) => { - console.error('拨打电话失败:', err) - }) + callShopPhone(scanInfo?.phone) } // 押金变化 @@ -122,55 +119,51 @@ const SelfStart = observer(() => { } setLoading(true) - + 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(() => { }) } } 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(() => { } } } 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) } diff --git a/src/pageScan/startTable/index.tsx b/src/pageScan/startTable/index.tsx index 9049276..4544cbc 100644 --- a/src/pageScan/startTable/index.tsx +++ b/src/pageScan/startTable/index.tsx @@ -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(() => { 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,23 +33,74 @@ 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 匹配 - 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}`) - 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 }) + 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} 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(() => { handleStartTable(option, option.available)} > - {option.available ? option.actionText || '去开台' : '不可用'} + + {loading ? '处理中...' : (option.available ? option.actionText || '去开台' : '不可用')} + )) ) : ( - // 接口数据未加载时显示占位 加载中... diff --git a/src/pageScan/timedStart/index.tsx b/src/pageScan/timedStart/index.tsx index 0851907..35a447d 100644 --- a/src/pageScan/timedStart/index.tsx +++ b/src/pageScan/timedStart/index.tsx @@ -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(() => { // 拨打电话 const handlePhoneCall = () => { - Taro.makePhoneCall({ - phoneNumber: '100-666-XXXX', - }).catch((err) => { - console.error('拨打电话失败:', err) - }) + callShopPhone(scanInfo?.phone) } // 选择时长 @@ -104,53 +101,58 @@ const TimedStart = observer(() => { } setLoading(true) - + try { 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(() => { } } } 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) } diff --git a/src/pageScan/turnOffRestart/index.tsx b/src/pageScan/turnOffRestart/index.tsx index 2e3ca57..1f2ac4e 100644 --- a/src/pageScan/turnOffRestart/index.tsx +++ b/src/pageScan/turnOffRestart/index.tsx @@ -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(() => { // 拨打电话 const handlePhoneCall = () => { - Taro.makePhoneCall({ - phoneNumber: '100-666-XXXX', - }).catch((err) => { - console.error('拨打电话失败:', err) - }) + callShopPhone(scanInfo?.phone) } // 确认结算 @@ -78,42 +75,33 @@ const TurnOffRestart = observer(() => { } setLoading(true) - - try { - console.log('关灯重开,设备编码:', grpSn, '操作记录ID:', operationId) - // 调用关灯重开接口 - const result = await turnOffRestart(grpSn, openId) + try { + 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) { - Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 }) - setTimeout(() => Taro.navigateBack(), 2000) - } else { - Taro.showModal({ - title: '支付确认中', - content: '支付结果尚未到账,请稍后刷新页面查看,如有疑问请联系店长。', - showCancel: false, - confirmText: '知道了', - }) - } + 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) } 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(() => { } } } 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) } diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 312db9a..3626d29 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -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(() => { 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, }) diff --git a/src/pages/store/index.tsx b/src/pages/store/index.tsx index 859ea11..594f189 100644 --- a/src/pages/store/index.tsx +++ b/src/pages/store/index.tsx @@ -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(() => { 暂无门店数据 ) : ( storeList.map((store) => ( - clickStoreItem(store)}> + clickStoreItem(store)}> {/* 标题和类型 */} {store.name} diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index fd40254..6a049d0 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -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(() => { 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) } diff --git a/src/pagesOrder/recharge/index.tsx b/src/pagesOrder/recharge/index.tsx index a09d755..63bf793 100644 --- a/src/pagesOrder/recharge/index.tsx +++ b/src/pagesOrder/recharge/index.tsx @@ -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[] = [ const RechargePage = observer(() => { const [selectedPackageId, setSelectedPackageId] = useState(null) const [loading, setLoading] = useState(false) + const [packages, setPackages] = useState(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 => { 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(() => { return false } - // 立即充值 const handleRecharge = async () => { if (loading) return @@ -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(() => { 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(() => { } } catch (error: any) { console.error('创建充值订单失败:', error) - // 错误提示由 request 拦截器统一处理 } finally { setLoading(false) } @@ -149,7 +175,6 @@ const RechargePage = observer(() => { return ( - {/* 上部分:余额卡片区域 */} {storeName} @@ -180,13 +205,11 @@ const RechargePage = observer(() => { - {/* 下部分:套餐和提示区域 */} 余额充值 - {/* 充值套餐列表 */} - {RechargePackages.map(pkg => ( + {packages.map(pkg => ( { ))} - {/* 温馨提示 */} 温馨提示:{'\n'} @@ -211,7 +233,6 @@ const RechargePage = observer(() => { - {/* 底部固定按钮 */} {loading ? '处理中...' : '立即充值'} diff --git a/src/pagesUser/editProfile/index.tsx b/src/pagesUser/editProfile/index.tsx index 97302f9..cfd87bc 100644 --- a/src/pagesUser/editProfile/index.tsx +++ b/src/pagesUser/editProfile/index.tsx @@ -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(() => { 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('') @@ -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(() => { /> - {/* 微信号 */} + {/* 微信号(接口已移除该字段,暂不展示) 微信号 setWchart(e.detail.value)} placeholder="请输入微信号" placeholderClass="form-placeholder" maxlength={30} /> + */} {/* 保存按钮 */} diff --git a/src/types/login.ts b/src/types/login.ts index 39ca48d..e3e2212 100644 --- a/src/types/login.ts +++ b/src/types/login.ts @@ -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 { 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 { 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 -} \ No newline at end of file +} diff --git a/src/types/magdev.ts b/src/types/magdev.ts new file mode 100644 index 0000000..85124e1 --- /dev/null +++ b/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 } diff --git a/src/types/recharge.ts b/src/types/recharge.ts index 72104b4..9ddcadc 100644 --- a/src/types/recharge.ts +++ b/src/types/recharge.ts @@ -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 { 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 { 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 +} diff --git a/src/types/startDevice.ts b/src/types/startDevice.ts index de86e35..a386324 100644 --- a/src/types/startDevice.ts +++ b/src/types/startDevice.ts @@ -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 { shopName: string shopCode: string instSn: string + /** 店长/门店联系电话(来自 qrcode.phone) */ + phone: string tableInfo: TableInfoDto price: PriceInfoDto businessHours: string diff --git a/src/utils/apiNormalize.ts b/src/utils/apiNormalize.ts new file mode 100644 index 0000000..3810f52 --- /dev/null +++ b/src/utils/apiNormalize.ts @@ -0,0 +1,24 @@ +/** + * 新接口字段大小写兼容:Word/Swagger 多为全小写,现网前端用 camelCase + */ +export const pickField = ( + source: Record | 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 +} diff --git a/src/utils/callShopPhone.ts b/src/utils/callShopPhone.ts new file mode 100644 index 0000000..7a062d9 --- /dev/null +++ b/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) + }) +} diff --git a/src/utils/customerNormalize.ts b/src/utils/customerNormalize.ts new file mode 100644 index 0000000..0f12c55 --- /dev/null +++ b/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 | 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(src, 'birthday'), + } +} + +/** 登录 data:扁平(Word)或嵌套 customer(旧) */ +export const normalizeLoginData = (data: Record | null | undefined) => { + const src = data || {} + const nested = pickField>(src, 'customer') + const customer = normalizeCustomer(nested || src) + const isNewCustomer = Boolean( + pickField(src, 'isNewCustomer', 'isnewcustomer') ?? false + ) + return { customer, isNewCustomer, message: String(pickField(src, 'message') ?? '') } +} diff --git a/src/utils/operateNormalize.ts b/src/utils/operateNormalize.ts new file mode 100644 index 0000000..ebd8c4c --- /dev/null +++ b/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): 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 | null | undefined): PriceInfoDto => { + const src = raw || {} + const timePrices = pickField[]>(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 | 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(item, 'balanceDisplay', 'balancedisplay') ?? null, + })) +} + +const normalizeOperatePaymentResponse = ( + raw: Record | null | undefined +): OperatePaymentResponseDto | null => { + if (!raw) return null + const payParams = normalizeWechatPayParams( + pickField>(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 | null | undefined +): OperateResponseData => { + const src = raw || {} + return { + consumptionId: toNumber(pickField(src, 'consumptionId', 'consumptionid')), + errorMessage: String(pickField(src, 'errorMessage', 'errormessage') ?? ''), + paymentResponseDto: normalizeOperatePaymentResponse( + pickField>(src, 'paymentResponseDto', 'paymentresponsedto') + ), + } +} + +/** 归一化 peroperate(去开台)响应 data */ +export const normalizePreOperateResponse = ( + raw: Record | 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>(src, 'devInfo', 'devinfo') + ), + price: normalizePrice(pickField>(src, 'price')), + paymentMethods: normalizePaymentMethods( + pickField(src, 'paymentMethods', 'paymentmethods') ?? [] + ), + } +} + +/** 归一化 getopstatus 响应 data */ +export const normalizeGetOpStatusResponse = ( + raw: Record | null | undefined +): GetOpStatusResponseData => { + const src = raw || {} + const opts = pickField(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'), + })) + : [], + } +} diff --git a/src/utils/rechargeNormalize.ts b/src/utils/rechargeNormalize.ts new file mode 100644 index 0000000..ad8d4e3 --- /dev/null +++ b/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 | null | undefined +): RechargeWechatPayParameters | null => { + if (!raw) return null + const timeStamp = pickField(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 | null | undefined +): RechargePaymentResponseDto => { + const src = raw || {} + return { + outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''), + channel: toNumber(pickField(src, 'channel')), + payParameters: normalizeWechatPayParams( + pickField>(src, 'payParameters', 'payparameters') + ), + payUrl: String(pickField(src, 'payUrl', 'payurl') ?? ''), + } +} + +export const normalizeRechargeStatus = ( + raw: Record | 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): RechargeRuleItem => ({ + recharge: toNumber(pickField(raw, 'recharge')), + gift: toNumber(pickField(raw, 'gift')), + desc: String(pickField(raw, 'desc', 'description') ?? ''), +}) + +export const normalizeRechargeRules = ( + raw: Record | null | undefined +): RechargeRuleQueryResult => { + const src = raw || {} + const rules = pickField[]>(src, 'rules') || [] + return { + instName: String(pickField(src, 'instName', 'instname') ?? ''), + bpName: String(pickField(src, 'bpName', 'bpname') ?? ''), + rules: Array.isArray(rules) ? rules.map(normalizeRechargeRuleItem) : [], + } +} diff --git a/src/utils/scanNormalize.ts b/src/utils/scanNormalize.ts new file mode 100644 index 0000000..398bf8f --- /dev/null +++ b/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): 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 | null | undefined): PriceInfoDto => { + const src = raw || {} + const timePrices = pickField[]>(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 | 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): 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 | 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 | null | undefined +): TableScanResponseDto => { + const src = raw || {} + const openOptions = pickField[]>(src, 'openOptions', 'openoptions') || [] + const devInfo = pickField>(src, 'tableInfo', 'devinfo', 'tableinfo') + + return { + scanType: (pickField(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')), + } +} diff --git a/src/utils/storeNormalize.ts b/src/utils/storeNormalize.ts new file mode 100644 index 0000000..cd9fd64 --- /dev/null +++ b/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): StoreInfoDto => { + const inst = pickField>(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[] | null | undefined): StoreInfoDto[] => { + if (!Array.isArray(list)) return [] + return list.map(normalizeStoreItem) +}