From b212dc8764117647ce047f357ae2446cb336e80c Mon Sep 17 00:00:00 2001 From: DU Date: Sat, 6 Jun 2026 18:12:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E7=89=88=E6=9C=AC=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E6=8E=A5=EF=BC=8C=20=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E5=95=86=E5=BA=97=E5=88=97=E8=A1=A8=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- src/api/basicInfo.ts | 36 ++++ src/api/customer.ts | 9 + src/api/startDevice.ts | 56 ++++++ src/app.ts | 15 +- .../closeTable/components/FooterButtons.tsx | 28 ++- src/pageScan/selfStart/index.tsx | 26 +-- src/pages/store/index.tsx | 173 +++++++----------- src/pages/user/index.tsx | 8 +- src/types/startDevice.ts | 35 ++++ 10 files changed, 253 insertions(+), 136 deletions(-) create mode 100644 src/api/basicInfo.ts diff --git a/package.json b/package.json index 12ba100..69aebe0 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,7 @@ "dev:jd": "npm run build:jd -- --watch" }, "browserslist": [ - "defaults and fully supports es6-module", - "maintained node versions" + "chrome >= 67" ], "author": "", "dependencies": { diff --git a/src/api/basicInfo.ts b/src/api/basicInfo.ts new file mode 100644 index 0000000..78b1637 --- /dev/null +++ b/src/api/basicInfo.ts @@ -0,0 +1,36 @@ +/** + * 基础信息相关 API + * 接口地址:/api/v2/BasicInfo/ + */ +import { get } from '../services/request' + +/** + * 门店(场所)信息 + */ +export interface StoreInfoDto { + id: number + sn: string + name: string + /** 店类型:0-重点,1-普通 */ + type: number + typeName: string + /** 是否加盟:0-否,1-是 */ + isUnion: number + address: string + manager: string + phone: string + inst_Sn: string + instName: string + rgn_Sn: string + regionName: string + longItude: number + latItude: number + createTime: string +} + +/** + * 获取所有场所(门店)列表 + */ +export const getStoreList = () => { + return get('/BasicInfo/GetAllBpInfo') +} diff --git a/src/api/customer.ts b/src/api/customer.ts index 9e87683..df723c5 100644 --- a/src/api/customer.ts +++ b/src/api/customer.ts @@ -4,6 +4,15 @@ import { get, put } from '../services/request' import { CustomerDto, UpdateCustomerDto } from '../types/login' +/** + * 根据 OpenId 获取客户信息(含最新余额、会员类型等) + * 登录后或页面刷新时调用,比 getCustomerBySn 更通用(无需 sn) + * @param openId 微信 OpenId + */ +export const getCustomerByOpenId = (openId: string) => { + return get(`/Customer/${openId}`) +} + /** * 根据客户 SN 获取客户详情(含最新余额、会员类型等) * @param sn 客户编码(CustomerDto.sn) diff --git a/src/api/startDevice.ts b/src/api/startDevice.ts index d32bfa9..0744787 100644 --- a/src/api/startDevice.ts +++ b/src/api/startDevice.ts @@ -14,6 +14,7 @@ import { PayType, StopGroupDevRequestDto, StopGroupDevResponseDto, + CheckOpenStatusResponse, } from '../types/startDevice' /** 小程序 JSAPI 支付固定交易类型 */ @@ -218,6 +219,58 @@ export const getTablePrice = (openId: string, groupDevSn: string) => { return post('/TablesDev/get-price', data) } +/** + * 查询开台状态(支付成功后轮询,确认台子是否真正开启) + * 解决"付款成功但 scanType 仍为 open"的问题 + * @param outTradeNo 商户订单号(开台接口返回的 outTradeNo) + */ +export const checkOpenStatus = (outTradeNo: string) => { + return get('/TablesDev/check-open-status', { outTradeNo }) +} + +/** + * 轮询开台状态,直到台子真正开启或超时/失败 + * @param outTradeNo 商户订单号 + * @param maxRetries 最大轮询次数(默认 8 次,每次 2s,共约 16s) + * @param intervalMs 轮询间隔(毫秒) + * @returns 开台成功时返回完整状态数据(含 operationId),失败/超时返回 null + */ +export const pollCheckOpenStatus = async ( + outTradeNo: string, + maxRetries = 8, + intervalMs = 2000 +): Promise => { + for (let i = 0; i < maxRetries; i++) { + await new Promise(resolve => setTimeout(resolve, intervalMs)) + try { + const res = await checkOpenStatus(outTradeNo) + const { isOpened, openStatus } = res.data + if (isOpened && openStatus === 'Opened') { + return res.data + } + if (openStatus === 'Failed') { + console.warn('开台状态查询:台子开启失败') + return null + } + } catch (e) { + console.warn(`查询开台状态第 ${i + 1} 次异常:`, e) + } + } + console.warn('查询开台状态超时') + return null +} + +/** + * 更新球台分享状态(分享球台功能) + * @param orderSn 订单编号(opInfo.orderNo) + */ +export const updateShareStatus = (orderSn: string) => { + return post('/TablesDev/UpdateShareStatus', { orderSn }, { + showLoading: true, + loadingText: '处理中...', + }) +} + // 导出枚举类型 export { CtrlType, PayType } @@ -233,6 +286,9 @@ export default { immediateClose, renewFee, turnOffRestart, + checkOpenStatus, + pollCheckOpenStatus, + updateShareStatus, getTablePrice, getTableInfo, CtrlType, diff --git a/src/app.ts b/src/app.ts index 3e1cf5e..4f440e2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,13 +2,14 @@ import { PropsWithChildren } from 'react' import { useLaunch } from '@tarojs/taro' import Taro from '@tarojs/taro' import { userStore } from './store' +import { getCustomerByOpenId } from './api/customer' import './app.scss' function App({ children }: PropsWithChildren) { useLaunch(() => { console.log('台球馆小程序启动') - // 初始化:从本地存储恢复用户信息 + // 先从本地缓存恢复用户信息(快速) try { const savedUserInfo = Taro.getStorageSync('userInfo') if (savedUserInfo) { @@ -17,6 +18,18 @@ function App({ children }: PropsWithChildren) { } catch (error) { console.warn('初始化用户信息失败:', error) } + + // 后台静默刷新(获取最新余额等信息),不阻塞启动 + const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') + if (openId) { + getCustomerByOpenId(openId) + .then(res => { + if (res.data) { + userStore.setUserInfoFromWechat(res.data) + } + }) + .catch(e => console.warn('启动刷新用户信息失败:', e)) + } }) return children diff --git a/src/pageScan/closeTable/components/FooterButtons.tsx b/src/pageScan/closeTable/components/FooterButtons.tsx index b99cc30..9883cb4 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 } from '../../../api/startDevice' +import { immediateClose, invokeWechatPay, updateShareStatus } from '../../../api/startDevice' import { closeOrder, pollTablePaymentStatus } from '../../../api/order' -import { userStore } from '../../../store' +import { userStore, tableStore } from '../../../store' import './FooterButtons.scss' interface FooterButtonsProps { - type: 'owner' | 'guest' // owner: 开台人员扫码, guest: 非开台人扫码 - grpSn: string // 设备编码 - operationId: number // 操作记录ID + type: 'owner' | 'guest' + grpSn: string + operationId: number } const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { @@ -138,6 +138,21 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { }) } + // 分享球台 + const handleShareTable = async () => { + const orderNo = tableStore.scanInfo?.opInfo?.orderNo + if (!orderNo) { + Taro.showToast({ title: '订单编号暂不可用,请稍后再试', icon: 'none', duration: 2000 }) + return + } + try { + await updateShareStatus(orderNo) + Taro.showToast({ title: '已开启分享', icon: 'success', duration: 2000 }) + } catch (_) { + // 错误由 request 拦截器处理 + } + } + // 邀请球友 const handleInviteFriend = () => { console.log('邀请球友') @@ -168,6 +183,9 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { 续费 + + 分享球台 + 邀请球友 diff --git a/src/pageScan/selfStart/index.tsx b/src/pageScan/selfStart/index.tsx index 5361d79..2bf0da0 100644 --- a/src/pageScan/selfStart/index.tsx +++ b/src/pageScan/selfStart/index.tsx @@ -6,8 +6,8 @@ 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 } from '../../api/startDevice' -import { closeOrder, pollTablePaymentStatus } from '../../api/order' +import { depositStart, balanceStart, invokeWechatPay, PayType, getTableInfo, pollCheckOpenStatus } from '../../api/startDevice' +import { closeOrder } from '../../api/order' import { userStore, tableStore } from '../../store' import './index.scss' @@ -152,30 +152,30 @@ const SelfStart = observer(() => { try { await invokeWechatPay(result.data.paymentResponseDto.payParameters) - // 轮询服务端确认支付结果 - Taro.showLoading({ title: '确认支付中...', mask: true }) - const paid = await pollTablePaymentStatus(consumptionId) + // 轮询台子是否真正开启(check-open-status),解决付款后仍显示 open 的问题 + Taro.showLoading({ title: '开台确认中...', mask: true }) + const openResult = await pollCheckOpenStatus(outTradeNo!) Taro.hideLoading() - if (paid) { - // 支付成功后重新拉取台桌信息,更新 scanInfo(opInfo 会有完整数据) + if (openResult?.isOpened) { + // 用接口返回的 operationId(比 consumptionId 更准确) + const confirmedOperationId = openResult.operationId || consumptionId + // 刷新 scanInfo,关台页 opInfo 有真实数据 try { const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') const freshInfo = await getTableInfo(grpSn, openId2) tableStore.setScanInfo(freshInfo.data) - } catch (_) { - // 刷新失败不阻断流程,关台页会显示 '--' 但功能正常 - } + } catch (_) {} Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${confirmedOperationId}` }) }, 2000) } else { Taro.showModal({ - title: '支付确认中', - content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', + title: '开台确认中', + content: '台桌正在启动,请稍后重新扫码进入关台页,如有疑问请联系店长。', showCancel: false, confirmText: '知道了', }) diff --git a/src/pages/store/index.tsx b/src/pages/store/index.tsx index 9fa2803..bc42e14 100644 --- a/src/pages/store/index.tsx +++ b/src/pages/store/index.tsx @@ -9,6 +9,7 @@ import { Popup, Toast, Tabs } from '@taroify/core' import { useCascader } from '@taroify/hooks' import { Arrow, Cross, Search } from '@taroify/icons' import { appStore } from '../../store' +import { getStoreList, StoreInfoDto } from '../../api/basicInfo' import storeIcon from '../../assets/icon/storeIcon.png' import locationIcon from '../../assets/icon/locationIcon.png' import './index.scss' @@ -60,53 +61,14 @@ const cityOptions = [ }, ] -// 模拟门店数据 -const mockStoreList = [ - { - id: '1', - name: '北京黑八大师联盟球厅店', - price: 16.00, - billiardCount: 6, - billiardEmpty: 6, - chessCount: 2, - chessEmpty: 2, - type: '直营店', - address: '北京市XXX区XXX街道XXX路19号606', - distance: '0.05km', - }, - { - id: '2', - name: '北京晶彩桌球世界店', - price: 24.00, - billiardCount: 8, - billiardEmpty: 8, - chessCount: 1, - chessEmpty: 1, - type: '加盟店', - address: '北京市XXX区XXX街道XXX路XXXXXX大厦101', - distance: '0.1km', - }, - { - id: '3', - name: '北京飓风台球店', - price: 106.00, - billiardCount: 0, - billiardEmpty: 0, - chessCount: 0, - chessEmpty: 0, - type: '加盟店', - address: '北京市XXX区XXX街道106号A01', - distance: '100.00km', - }, -] - const StoreIndex = observer(() => { const [selectedCity, setSelectedCity] = useState('选城市') const [searchValue, setSearchValue] = useState('') const [cityPopupOpen, setCityPopupOpen] = useState(false) - const [storeList, setStoreList] = useState(mockStoreList) + const [allStores, setAllStores] = useState([]) + const [storeList, setStoreList] = useState([]) const [loading, setLoading] = useState(false) - const [isSearched, setIsSearched] = useState(false) // 是否已执行搜索 + const [isSearched, setIsSearched] = useState(false) const [cityValue, setCityValue] = useState([]) const [tabIndex, setTabIndex] = useState(0) @@ -119,8 +81,22 @@ const StoreIndex = observer(() => { useLoad(() => { console.log('门店页面加载') + loadStores() }) + const loadStores = async () => { + setLoading(true) + try { + const res = await getStoreList() + setAllStores(res.data) + setStoreList(res.data) + } catch (e) { + console.error('获取门店列表失败:', e) + } finally { + setLoading(false) + } + } + // 监听弹窗状态,控制TabBar显示/隐藏 useEffect(() => { if (cityPopupOpen) { @@ -166,30 +142,14 @@ const StoreIndex = observer(() => { // 搜索门店 const handleSearch = () => { - // 无内容时提示输入 if (!searchValue.trim()) { Toast.open('请输入门店名称或地址') return } - - setLoading(true) - Toast.loading({ - message: '搜索中...', - duration: 0, - }) - - // 模拟接口请求 - setTimeout(() => { - setLoading(false) - Toast.close() - Toast.success('搜索完成') - setIsSearched(true) // 标记已搜索,切换为取消图标 - - // 根据输入内容过滤数据 - setStoreList(mockStoreList.filter(store => - store.name.includes(searchValue) || store.address.includes(searchValue) - )) - }, 1500) + setIsSearched(true) + setStoreList(allStores.filter(store => + store.name.includes(searchValue) || (store.address || '').includes(searchValue) + )) } // 输入框变化 @@ -200,28 +160,15 @@ const StoreIndex = observer(() => { // 清除搜索并恢复默认数据 const handleClearSearch = () => { setSearchValue('') - setIsSearched(false) // 切换回搜索图标 - - // 恢复默认数据 - setLoading(true) - Toast.loading({ - message: '加载中...', - duration: 0, - }) - - setTimeout(() => { - setLoading(false) - Toast.close() - setStoreList(mockStoreList) - }, 500) + setIsSearched(false) + setStoreList(allStores) } // 跳转到店铺详情 - const clickStoreItem = (store: any) => { + const clickStoreItem = (store: StoreInfoDto) => { Taro.navigateTo({ url: '/pageStore/storeDetail/index?id=' + store.id }) - console.log('跳转到店铺详情', store) } // 跳转到地图找店页面(分包页面) @@ -273,46 +220,50 @@ const StoreIndex = observer(() => { {/* 门店列表 */} - {storeList.map((store) => ( - clickStoreItem(store)}> - {/* 标题和价格 */} - - {store.name} - - ¥ - {store.price.toFixed(2)} - 起/小时 + {loading ? ( + 加载中... + ) : storeList.length === 0 ? ( + 暂无门店数据 + ) : ( + storeList.map((store) => ( + clickStoreItem(store)}> + {/* 标题和类型 */} + + {store.name} + + {store.typeName || (store.isUnion === 1 ? '加盟店' : '直营店')} + - - {/* 标签 */} - - - 台球 {store.billiardCount} 桌空闲 - - - 棋牌 {store.chessCount} 桌空闲 + {/* 标签 */} + + {store.instName && ( + {store.instName} + )} + {store.regionName && ( + {store.regionName} + )} - - {store.type} - - - {/* 地址和距离 */} - - - - {store.address} + {/* 地址和电话 */} + + + + {store.address || '地址未填写'} + + {store.phone && ( + {store.phone} + )} - 距离{store.distance} {'>'} - - ))} + )) + )} - {/* 列表底部提示 */} - - 到底了~ - + {!loading && storeList.length > 0 && ( + + 到底了~ + + )} diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index d6a6c06..785b88a 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react' import { Image as TaroifyImage, Button } from '@taroify/core' import { userStore } from '../../store' import { wechatLogin } from '../../api/login' -import { getCustomerBySn } from '../../api/customer' +import { getCustomerByOpenId } from '../../api/customer' import { getBigImage } from '@/utils/imageHelper' import './index.scss' @@ -30,10 +30,10 @@ const UserIndex = observer(() => { useLoad(async () => { console.log('个人中心页面加载') // 已登录时,刷新最新用户信息(余额、会员类型等) - const sn = userStore.userInfo?.sn - if (userStore.isLoggedIn && sn) { + const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') + if (userStore.isLoggedIn && openId) { try { - const res = await getCustomerBySn(sn) + const res = await getCustomerByOpenId(openId) if (res.data) { userStore.setUserInfoFromWechat(res.data) } diff --git a/src/types/startDevice.ts b/src/types/startDevice.ts index 845b244..de86e35 100644 --- a/src/types/startDevice.ts +++ b/src/types/startDevice.ts @@ -333,3 +333,38 @@ export interface StopGroupDevResponseDto { /** 商户订单号,前端轮询用 */ outTradeNo: string } + +// ─── 查询开台状态接口(/api/v2/TablesDev/check-open-status)相关类型 ────────── + +/** + * 开台状态枚举 + * Pending → Processing → Opened / Failed + */ +export type OpenStatus = 'Pending' | 'Processing' | 'Opened' | 'Failed' + +/** + * 查询开台状态响应(对应 data 字段) + * 接口:GET /api/v2/TablesDev/check-open-status?outTradeNo=xxx + */ +export interface CheckOpenStatusResponse { + /** 商户订单号 */ + outTradeNo: string + /** 是否已成功开台 */ + isOpened: boolean + /** 开台状态:Pending-等待中、Processing-处理中、Opened-已开台、Failed-失败 */ + openStatus: OpenStatus + /** 设备编码 */ + grpSn: string + /** 设备工作状态:0-空闲,1-使用中,2-已预约,3-维护中,4-开台中 */ + workStatus: number + /** 工作状态描述 */ + workStatusDesc: string + /** 当前开台记录 ID(开台成功后有值,用于关台页) */ + operationId: number + /** 开台时间 */ + startTime: string + /** 预计结束时间(定时开台时有值) */ + expectedEndTime: string + /** 开台人用户编码 */ + userSn: string +}