Browse Source

大量新接口对接

v2.0
DU 3 months ago
parent
commit
c2dd83c5c8
  1. 221
      QR_CODE_DESIGN.md
  2. 26
      src/api/customer.ts
  3. 39
      src/api/order.ts
  4. 256
      src/api/startDevice.ts
  5. 4
      src/pages/qrHandler/index.tsx
  6. 150
      src/services/tableApi.ts
  7. 42
      src/store/table.ts
  8. 19
      src/types/login.ts
  9. 147
      src/types/order.ts
  10. 212
      src/types/startDevice.ts
  11. 20
      src/utils/customerNormalize.ts
  12. 70
      src/utils/orderNormalize.ts
  13. 2
      src/utils/scanNormalize.ts

221
QR_CODE_DESIGN.md

@ -0,0 +1,221 @@ @@ -0,0 +1,221 @@
# 二维码设计方案
## 📋 **业务分析**
### **扫码场景分析**
1. **开台扫码** - 扫描球桌二维码进行开台
2. **关台扫码** - 扫描球桌二维码进行关台
3. **存杆柜扫码** - 扫描存杆柜二维码进行存取
4. **团购券扫码** - 扫描团购券二维码使用优惠
### **跳转页面映射**
```
pageScan/startTable/index - 开台选择页面
pageScan/selfStart/index - 自助开台页面
pageScan/timedStart/index - 定时开台页面
pageScan/couponStart/index - 团购券开台页面
pageScan/closeTable/index - 关台页面
pageScan/renewFee/index - 续费页面
pageScan/turnOffRestart/index - 关灯重开页面
pageScan/myCoupons/index - 我的卡券页面
pageScan/couponUse/index - 卡券使用页面
```
## 🎯 **推荐方案**
### **1. 统一处理页面** ✅ 推荐
**使用 `pages/qrHandler/index` 作为统一的二维码处理入口**
**优势:**
- ✅ 统一的跳转逻辑,便于维护
- ✅ 支持微信扫码和小程序内扫码
- ✅ 错误处理集中化
- ✅ 便于添加埋点和日志
- ✅ 支持复杂的业务逻辑判断
**使用场景:**
1. **微信扫码进入小程序**:`pages/qrHandler/index?q=xxx`
2. **小程序内扫码**:`pages/qrHandler/index?qrContent=xxx`
3. **小程序码**:`pages/qrHandler/index?scene=xxx`
### **2. 二维码内容格式设计**
#### **方案A:JSON 格式** ✅ 推荐
```json
{
"type": "table_start",
"grpSn": "TB001",
"tableId": "table_123",
"storeId": "store_456"
}
```
#### **方案B:URL 格式**
```
https://yourdomain.com/qr?type=table_start&grpSn=TB001&tableId=table_123&storeId=store_456
```
#### **方案C:简单格式**
```
table_start:TB001:table_123:store_456
```
## 🛠️ **后端实现建议**
### **1. 二维码生成接口**
```typescript
// 后端接口设计
POST /api/v2/qrcode/generate
{
"type": "table_start", // 二维码类型
"grpSn": "TB001", // 设备编码
"tableId": "table_123", // 球桌ID
"storeId": "store_456" // 门店ID
}
// 返回
{
"success": true,
"data": {
"qrContent": "{\"type\":\"table_start\",\"grpSn\":\"TB001\",\"tableId\":\"table_123\",\"storeId\":\"store_456\"}",
"qrCodeUrl": "https://api.qrserver.com/v1/create-qr-code/?data=xxx"
}
}
```
### **2. 二维码类型定义**
```typescript
enum QRCodeType {
TABLE_START = 'table_start', // 开台
TABLE_CLOSE = 'table_close', // 关台
CABINET = 'cabinet', // 存杆柜
COUPON = 'coupon' // 团购券
}
```
### **3. 不同类型二维码的数据结构**
#### **开台二维码**
```json
{
"type": "table_start",
"grpSn": "TB001", // 设备编码(必需)
"tableId": "table_123", // 球桌ID(必需)
"storeId": "store_456", // 门店ID(必需)
"tableName": "1号台", // 球桌名称(可选)
"storeInfo": { // 门店信息(可选)
"name": "黑八大师联盟球厅",
"address": "xxx路xxx号"
}
}
```
#### **关台二维码**
```json
{
"type": "table_close",
"grpSn": "TB001", // 设备编码(必需)
"tableId": "table_123", // 球桌ID(必需)
"orderId": "order_789" // 订单ID(可选)
}
```
#### **存杆柜二维码**
```json
{
"type": "cabinet",
"cabinetId": "cabinet_001", // 存杆柜ID(必需)
"storeId": "store_456", // 门店ID(必需)
"cabinetNo": "A01" // 存杆柜编号(可选)
}
```
#### **团购券二维码**
```json
{
"type": "coupon",
"couponCode": "MT123456", // 团购券码(必需)
"platform": "meituan", // 平台:meituan/douyin/dianping(可选)
"value": 50.00 // 券面值(可选)
}
```
## 🔄 **跳转逻辑流程**
```mermaid
graph TD
A[用户扫码] --> B[qrHandler页面]
B --> C[解析二维码内容]
C --> D{判断二维码类型}
D -->|table_start| E[跳转开台选择页面]
D -->|table_close| F[跳转关台页面]
D -->|cabinet| G[跳转存杆柜页面]
D -->|coupon| H[跳转团购券使用页面]
D -->|unknown| I[显示错误提示]
E --> J[startTable/index]
F --> K[closeTable/index]
G --> L[poleStorage/index]
H --> M[couponUse/index]
```
## 📱 **小程序配置**
### **1. 页面路径配置**
在 `app.config.ts` 中已添加:
```typescript
pages: [
// ...
'pages/qrHandler/index', // 统一二维码处理页面
]
```
### **2. 微信扫码跳转配置**
需要在微信公众平台配置小程序的扫码跳转规则:
- 扫码跳转页面:`pages/qrHandler/index`
- 参数传递:通过 `q` 参数传递二维码内容
## ⚠️ **注意事项**
### **1. 安全考虑**
- 二维码内容建议加密或签名
- 添加时效性验证
- 防止二维码被恶意复制使用
### **2. 用户体验**
- 添加加载状态提示
- 错误情况下提供明确的引导
- 支持重新扫码功能
### **3. 兼容性**
- 支持多种二维码格式解析
- 向后兼容旧版本二维码
- 处理异常情况的降级方案
## 🚀 **实施步骤**
1. ✅ **前端已完成**:
- 创建二维码处理工具 `qrCodeHandler.ts`
- 创建统一处理页面 `qrHandler/index`
- 更新扫码页面集成新逻辑
2. 🔄 **后端需要实现**:
- 二维码生成接口
- 不同类型二维码的数据结构
- 二维码验证和安全机制
3. 🔄 **测试验证**:
- 各种类型二维码的跳转测试
- 异常情况的处理测试
- 微信扫码进入的测试
## 💡 **总结建议**
**给后端的建议:**
1. **使用统一的 `pages/qrHandler/index` 页面**作为所有二维码的处理入口
2. **二维码内容采用 JSON 格式**,便于扩展和解析
3. **不需要为每种业务单独创建处理页面**,统一处理更好维护
4. **重点关注二维码的数据结构设计**,确保包含必要的业务参数
这样的设计既保证了功能的完整性,又具有良好的可维护性和扩展性。

26
src/api/customer.ts

@ -12,7 +12,7 @@ import { get, post } from '../services/request' @@ -12,7 +12,7 @@ import { get, post } from '../services/request'
import { CustomerDto, UpdateCustomerDto } from '../types/login'
import { normalizeCustomer } from '../utils/customerNormalize'
import { normalizeCustomer, normalizeBalanceList } from '../utils/customerNormalize'
import { pickField, toNumber } from '../utils/apiNormalize'
@ -86,6 +86,30 @@ export const getCustomerBalance = async (openId: string, bpSn: string) => { @@ -86,6 +86,30 @@ export const getCustomerBalance = async (openId: string, bpSn: string) => {
/**
* 查询客户在所有已消费门店的余额列表(含积分/存杆柜数量/VIP到期时间)
* 对应接口:GET /customer/balancelist/{openId}
* 用于 pagesOrder/accountBalance 的"账户余额" Tab(BalanceList 组件)
*/
export const getCustomerBalanceList = async (openId: string) => {
const res = await get<Record<string, unknown>>(
`/customer/balancelist/${encodeURIComponent(openId)}`
)
return { ...res, data: normalizeBalanceList(res.data as unknown as Record<string, unknown>[]) }
}
/**
* 刷新用户资料 + 余额(有 bpSn 时)

39
src/api/order.ts

@ -2,7 +2,8 @@ @@ -2,7 +2,8 @@
* 订单/支付相关 API
*/
import { get, post } from '../services/request'
import { PaymentResult, QueryPaymentRequest, CloseOrderDto } from '../types/order'
import { PaymentResult, QueryPaymentRequest, CloseOrderDto, OrderRecordQueryParams } from '../types/order'
import { normalizeOrderRecordPagedResult, normalizeOrderDetail } from '../utils/orderNormalize'
/**
* 查询支付结果(通用,通过 outTradeNo 或 tradeNo 查询)
@ -53,3 +54,39 @@ export const pollTablePaymentStatus = async ( @@ -53,3 +54,39 @@ export const pollTablePaymentStatus = async (
}
return false
}
/* ======================================================================
* 以下为新接口:/api/v2/order/records、/api/v2/order/detail/{id}
* 来源:后端 2026-07-29 更新的《小程序接口与功能(4).docx》「订单管理」章节
* 与上面的 /Order/query、/Order/close(旧接口,暂缓/已废弃)无关
* ====================================================================== */
/**
* 查询订单记录(消费记录列表),用于 pagesUser/orderList
* 必填 openId;支持 startDate/endDate/payType(可多选)/grpSn/categoryType/status 过滤 + 分页
*/
export const getOrderRecords = async (params: OrderRecordQueryParams) => {
const res = await get<Record<string, unknown>>('/order/records', {
openid: params.openId,
bpsn: params.bpSn,
startdate: params.startDate,
enddate: params.endDate,
paytype: params.payType,
grpsn: params.grpSn,
categorytype: params.categoryType,
status: params.status,
pageindex: params.pageIndex ?? 1,
pagesize: params.pageSize ?? 10,
})
return { ...res, data: normalizeOrderRecordPagedResult(res.data as Record<string, unknown>) }
}
/**
* 查询订单详情,用于 pagesUser/orderDetail
* @param id 订单记录 id(来自 order/records 列表项的 id 字段)
*/
export const getOrderDetail = async (id: number | string) => {
const res = await get<Record<string, unknown>>(`/order/detail/${id}`)
return { ...res, data: normalizeOrderDetail(res.data as Record<string, unknown>) }
}

256
src/api/startDevice.ts

@ -1,40 +1,14 @@ @@ -1,40 +1,14 @@
/**
* 台球桌设备操作相关 API
* 开台接口:/api/v2/TablesDev/start_device
* 关台/续费/关灯重开接口:/api/v2/TablesDev/stop
* 扫码获取台桌信息:GET /api/v2/magdev/qrcode
* 开台/关台/续费/关灯重开/分享等统一操作见 src/api/magdev.ts(peroperate / operate / getopstatus)
*
* 注:旧版 /api/v2/TablesDev/* 系列接口(start_device / stop / get-price /
* check-open-status / UpdateShareStatus)已废弃,未被任何页面使用,已随本次清理移除。
*/
import { get, post } from '../services/request'
import { get } from '../services/request'
import { normalizeTableScanResponse } from '../utils/scanNormalize'
import {
StartGroupDevRequestDto,
StartDeviceResponse,
WechatPayParameters,
PriceRequestDto,
TableScanResponseDto,
CtrlType,
PayType,
StopGroupDevRequestDto,
StopGroupDevResponseDto,
CheckOpenStatusResponse,
} from '../types/startDevice'
/** 小程序 JSAPI 支付固定交易类型 */
const MINI_PROGRAM_TRADE_TYPE = 1
/**
* 开台操作接口
* @param data 开台请求参数
* @returns 开台响应
*/
export const startDevice = (data: StartGroupDevRequestDto) => {
return post<StartDeviceResponse>('/TablesDev/start_device', {
tradeType: MINI_PROGRAM_TRADE_TYPE,
...data,
}, {
showLoading: true,
loadingText: '处理中...',
})
}
import { WechatPayParameters, CtrlType, PayType } from '../types/startDevice'
/**
* 调起微信支付
@ -59,146 +33,6 @@ export const invokeWechatPay = (payParams: WechatPayParameters): Promise<void> = @@ -59,146 +33,6 @@ export const invokeWechatPay = (payParams: WechatPayParameters): Promise<void> =
})
}
/**
* 押金开台
* @param grpSn 设备编码
* @param openId 用户OpenId
* @param amount 押金金额
* @param payType 支付方式
*/
export const depositStart = (
grpSn: string,
openId: string,
amount: number,
payType: PayType = PayType.WechatPay
) => {
return startDevice({
grpSn,
openId,
ctrlType: CtrlType.DepositStart,
amount,
payType,
})
}
/**
* 余额开台(押金开台 + 余额支付方式组合)
* @param grpSn 设备编码
* @param openId 用户OpenId
* @param amount 金额
*/
export const balanceStart = (grpSn: string, openId: string, amount: number) => {
return startDevice({
grpSn,
openId,
ctrlType: CtrlType.DepositStart,
amount,
payType: PayType.Balance,
})
}
/**
* 定时开台
* @param grpSn 设备编码
* @param openId 用户OpenId
* @param durationHours 开台时长(小时)
* @param amount 金额
* @param payType 支付方式
*/
export const timedStart = (
grpSn: string,
openId: string,
durationHours: number,
amount: number,
payType: PayType = PayType.WechatPay
) => {
return startDevice({
grpSn,
openId,
ctrlType: CtrlType.TimedStart,
durationHours,
amount,
payType,
})
}
/**
* 团购券开台
* @param grpSn 设备编码
* @param openId 用户OpenId
* @param couponCode 团购券码
*/
export const couponStart = (grpSn: string, openId: string, couponCode: string) => {
return startDevice({
grpSn,
openId,
ctrlType: CtrlType.CouponStart,
couponCode,
})
}
/**
* 关台操作基础接口(对应 /api/v2/TablesDev/stop)
* 统一处理立即关台、续费、关灯重开三种操作
*/
export const stopDevice = (data: StopGroupDevRequestDto) => {
return post<StopGroupDevResponseDto>('/TablesDev/stop', {
tradeType: MINI_PROGRAM_TRADE_TYPE,
...data,
}, {
showLoading: true,
loadingText: '处理中...',
})
}
/**
* 立即关台
* @param grpSn 设备编码
* @param openId 用户OpenId
*/
export const immediateClose = (grpSn: string, openId: string) => {
return stopDevice({
grpSn,
openId,
ctrlType: CtrlType.ImmediateClose,
})
}
/**
* 续费
* @param grpSn 设备编码
* @param openId 用户OpenId
* @param renewalAmount 续费金额(元)
* @param payType 支付方式
*/
export const renewFee = (
grpSn: string,
openId: string,
renewalAmount: number,
payType: PayType = PayType.WechatPay
) => {
return stopDevice({
grpSn,
openId,
ctrlType: CtrlType.Renew,
payType,
renewalAmount,
})
}
/**
* 关灯重开
* @param grpSn 设备编码
* @param openId 用户OpenId
*/
export const turnOffRestart = (grpSn: string, openId: string) => {
return stopDevice({
grpSn,
openId,
ctrlType: CtrlType.TurnOffRestart,
})
}
/**
* 扫码后获取开台信息(球台状态、价格、可用开台方式、当前订单等)
* @param sn 设备 SN(即 grpSn)
@ -210,88 +44,12 @@ export const getTableInfo = async (sn: string, openId?: string, source?: string) @@ -210,88 +44,12 @@ export const getTableInfo = async (sn: string, openId?: string, source?: string)
return { ...res, data: normalizeTableScanResponse(res.data as Record<string, unknown>) }
}
/**
* 根据用户获取台球桌实际价格(VIP/普通用户价格不同)
* @param openId 用户 OpenId
* @param groupDevSn 设备序列号(即 grpSn)
* @returns 该用户对应的价格(元/小时)
*/
export const getTablePrice = (openId: string, groupDevSn: string) => {
const data: PriceRequestDto = { openId, groupDevSn }
return post<number>('/TablesDev/get-price', data)
}
/**
* 查询开台状态(支付成功后轮询,确认台子是否真正开启)
* 解决"付款成功但 scanType 仍为 open"的问题
* @param outTradeNo 商户订单号(开台接口返回的 outTradeNo)
*/
export const checkOpenStatus = (outTradeNo: string) => {
return get<CheckOpenStatusResponse>('/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<CheckOpenStatusResponse | null> => {
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<boolean>('/TablesDev/UpdateShareStatus', { orderSn }, {
showLoading: true,
loadingText: '处理中...',
})
}
// 导出枚举类型
export { CtrlType, PayType }
// 导出所有方法和类型
export default {
startDevice,
stopDevice,
invokeWechatPay,
depositStart,
balanceStart,
timedStart,
couponStart,
immediateClose,
renewFee,
turnOffRestart,
checkOpenStatus,
pollCheckOpenStatus,
updateShareStatus,
getTablePrice,
getTableInfo,
CtrlType,
PayType,

4
src/pages/qrHandler/index.tsx

@ -100,7 +100,9 @@ const QRHandlerPage = () => { @@ -100,7 +100,9 @@ const QRHandlerPage = () => {
if (scanType === 'open') {
await Taro.navigateTo({ url: `/pageScan/startTable/index?grpSn=${sn}` })
} else {
await Taro.navigateTo({ url: `/pageScan/closeTable/index?grpSn=${sn}` })
// isOwner=false 表示扫的是别人正在使用的台,关台页需展示"帮他付款/AA制付款"按钮组
const type = res.data.isOwner === false ? 'guest' : 'owner'
await Taro.navigateTo({ url: `/pageScan/closeTable/index?grpSn=${sn}&type=${type}` })
}
return
}

150
src/services/tableApi.ts

@ -1,150 +0,0 @@ @@ -1,150 +0,0 @@
/**
* 球台相关 API
* 包含开台、关台、续费等接口
*/
import { post } from './request'
/**
* 开台操作类型
*/
export enum TableOperationType {
/** 押金开台 */
SelfServiceDeposit = 1,
/** 余额开台 */
SelfServiceBalance = 2,
/** 定时开台 */
Timed = 3,
/** 信用开台 */
Credit = 4,
/** 团购券开台 */
GroupBuy = 5,
/** 立即关台 */
ImmediateClose = 6,
/** 续费 */
Renew = 7,
/** 关灯重开 */
ReopenAfterClose = 8,
}
/**
* 支付渠道
*/
export enum PaymentChannel {
/** 微信支付 */
WechatPay = 1,
/** 支付宝 */
Alipay = 2,
/** 银联支付 */
UnionPay = 3,
/** 余额支付 */
Balance = 4,
/** 银行卡支付 */
BankCard = 5,
}
/**
* 开台请求参数
*/
export interface StartTableRequest {
/** 设备编码 */
grpSn: string
/** 用户 OpenId */
openId: string
/** 开台操作类型 */
ctrlType: TableOperationType
/** 消费金额 */
amount?: number
/** 支付渠道 */
payType?: PaymentChannel
/** 开台时长(小时),定时开台使用 */
durationHours?: number
/** 团购券码,团购券开台使用 */
couponCode?: string
}
/**
* 开台响应数据
*/
export interface StartTableResponse {
/** 机构编码 */
instSn: string
/** 店铺编码 */
bpSn: string
/** 球台编码 */
grpSn: string
/** 开台设备id */
operationId: number
/** 开始时间 */
startTime: string
/** 结束时间 */
expectedEndTime: string
/** 开台操作详情id */
detailIds: number[]
/** 控制id */
controlMessageId: number
/** 商户订单号 */
outTradeNo: string
/** 支付参数(用于调起微信支付) */
payParameters: WechatPayParameters | null
/** 支付URL(用于扫码支付) */
payUrl: string
}
/**
* 微信支付参数
*/
export interface WechatPayParameters {
/** 时间戳 */
timeStamp: string
/** 随机字符串 */
nonceStr: string
/** 统一下单接口返回的 prepay_id */
package: string
/** 签名方式 */
signType: string
/** 签名 */
paySign: string
}
/**
* 开台 API
* @param data 开台请求参数
* @returns 开台响应
*/
export const startTable = (data: StartTableRequest) => {
return post<StartTableResponse>('/Tables/start', data, {
showLoading: true,
loadingText: '开台中...',
})
}
/**
* 调起微信支付
* @param payParams 支付参数
* @returns Promise
*/
export const invokeWechatPay = (payParams: WechatPayParameters): Promise<void> => {
return new Promise((resolve, reject) => {
// 使用 wx.requestPayment 调起微信支付
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType as 'MD5' | 'HMAC-SHA256' | 'RSA',
paySign: payParams.paySign,
success: () => {
resolve()
},
fail: (err) => {
reject(err)
},
})
})
}
export default {
startTable,
invokeWechatPay,
TableOperationType,
PaymentChannel,
}

42
src/store/table.ts

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
*/
import { makeAutoObservable } from 'mobx'
import Taro from '@tarojs/taro'
import { TableInfo, OrderInfo, TableScanResponseDto } from '../types/startDevice'
import { TableScanResponseDto } from '../types/startDevice'
class TableStore {
/** 当前设备编码 */
@ -12,12 +12,6 @@ class TableStore { @@ -12,12 +12,6 @@ class TableStore {
/** 当前操作记录ID */
currentOperationId: number = 0
/** 台球桌信息(旧字段,保留兼容) */
tableInfo: TableInfo | null = null
/** 订单信息(旧字段,保留兼容) */
orderInfo: OrderInfo | null = null
/** 扫码后获取的完整开台信息(包含球台、价格、开台选项、当前订单) */
scanInfo: TableScanResponseDto | null = null
@ -36,15 +30,10 @@ class TableStore { @@ -36,15 +30,10 @@ class TableStore {
try {
const grpSn = Taro.getStorageSync('currentGrpSn')
const operationId = Taro.getStorageSync('currentOperationId')
const tableInfo = Taro.getStorageSync('tableInfo')
const orderInfo = Taro.getStorageSync('orderInfo')
const scanInfo = Taro.getStorageSync('scanInfo')
if (grpSn) this.currentGrpSn = grpSn
if (operationId) this.currentOperationId = operationId
if (tableInfo) this.tableInfo = tableInfo
if (orderInfo) this.orderInfo = orderInfo
if (scanInfo) this.scanInfo = scanInfo
} catch (error) {
console.error('读取台球桌信息失败:', error)
@ -67,30 +56,6 @@ class TableStore { @@ -67,30 +56,6 @@ class TableStore {
Taro.setStorageSync('currentOperationId', operationId)
}
/**
* 设置台球桌信息
*/
setTableInfo = (info: TableInfo | null) => {
this.tableInfo = info
if (info) {
Taro.setStorageSync('tableInfo', info)
} else {
Taro.removeStorageSync('tableInfo')
}
}
/**
* 设置订单信息
*/
setOrderInfo = (info: OrderInfo | null) => {
this.orderInfo = info
if (info) {
Taro.setStorageSync('orderInfo', info)
} else {
Taro.removeStorageSync('orderInfo')
}
}
/**
* 设置扫码开台信息(扫码后由 qrHandler 调用)
*/
@ -121,14 +86,9 @@ class TableStore { @@ -121,14 +86,9 @@ class TableStore {
clearAll = () => {
this.currentGrpSn = ''
this.currentOperationId = 0
this.tableInfo = null
this.orderInfo = null
this.scanInfo = null
Taro.removeStorageSync('currentGrpSn')
Taro.removeStorageSync('currentOperationId')
Taro.removeStorageSync('tableInfo')
Taro.removeStorageSync('orderInfo')
Taro.removeStorageSync('scanInfo')
}

19
src/types/login.ts

@ -45,6 +45,25 @@ export interface WechatAuthResponse { @@ -45,6 +45,25 @@ export interface WechatAuthResponse {
data: WechatAuthResultDto
}
/**
* 客户跨店余额列表单项(GET /customer/balancelist/{openId})
* 来源:后端 2026-07-29 更新的《小程序接口与功能(4).docx》§3.3.3
*/
export interface CustomerBalanceListItem {
/** 店编码 */
bpSn: string
/** 店名称 */
bpName: string
/** 余额 */
amount: number
/** 积分 */
points: number
/** 存杆柜数量 */
lockerCount: number
/** VIP 到期时间,无有效会员时为 null */
vipEndTime: string | null
}
/** 更新客户信息:新接口走 multipart,avatar 传本地文件路径 */
export interface UpdateCustomerDto {
id: number

147
src/types/order.ts

@ -61,3 +61,150 @@ export interface CloseOrderDto { @@ -61,3 +61,150 @@ export interface CloseOrderDto {
/** 商户订单号 */
outTradeNo: string
}
/* ======================================================================
* 以下为新接口:/api/v2/order/records(订单记录列表)、/api/v2/order/detail/{id}(订单详情)
* 来源:后端 2026-07-29 更新的《小程序接口与功能(4).docx》「订单管理」章节
* 与上面旧版 /Order/query、/Order/close 无关,属于独立的新业务域
* ====================================================================== */
/** 项目品类(categorytype),对应 GroupDev.Type / 字典 DictKindType.ItemCategory */
export enum OrderCategoryType {
/** 球台(台球) */
Table = 0,
/** 存杆柜 */
Locker = 1,
/** 棋牌 */
ChessRoom = 2,
}
/** order/records 查询参数 status(进行中/已完成筛选,与返回字段的 statusText 是两回事) */
export enum OrderQueryStatus {
/** 进行中 */
Ongoing = 0,
/** 已完成 */
Completed = 1,
}
/**
* 支付状态枚举(0-10),文档明确:适用于 recharge/records 和 order/records 中的 status 字段
* 与 utils/rechargeRecords.ts 的 RechargePaymentStatus 定义一致,此处仅类型层面重复声明供 order 相关代码引用
*/
export enum OrderPaymentStatus {
Invalid = 0,
Pending = 1,
Initialized = 2,
Paying = 3,
Success = 4,
Failed = 5,
Cancelled = 6,
Refunding = 7,
Refunded = 8,
RefundFailed = 9,
Closed = 10,
}
/** 查询订单记录(消费记录列表)请求参数 */
export interface OrderRecordQueryParams {
/** 微信 OpenId(必填) */
openId: string
/** 店编码 */
bpSn?: string
/** 开始时间 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss */
startDate?: string
/** 结束时间 */
endDate?: string
/** 支付方式,可多选(1微信/2支付宝/3银联/4余额/5银行卡) */
payType?: number[]
/** 设施编码(如台球桌编号),精确匹配 */
grpSn?: string
/** 项目品类:0球台,1存杆柜,2棋牌 */
categoryType?: OrderCategoryType
/** 状态筛选:0进行中,1已完成 */
status?: OrderQueryStatus
/** 当前页码,默认 1 */
pageIndex?: number
/** 每页记录数,默认 10 */
pageSize?: number
}
/**
* 订单记录列表项(新结构,后端已直接给出展示文案,不再需要前端拼枚举)
*/
export interface OrderRecordItem {
/** 订单记录 ID,用于跳转详情 order/detail/{id} */
id: number
/** 标题(含球台名称),如"普通球台123132 1号台" */
title: string
/** 订单状态文案,如"已完成"/"进行中" */
statusText: string
/** 消费门店名称 */
bpName: string
/** 开台时间 */
startTime: string
/** 时长文案,如"1小时0分钟" */
durationText: string
/** 实付金额 */
actualAmount: number
}
/** 订单记录分页结果 */
export interface OrderRecordPagedResult {
data: OrderRecordItem[]
totalCount: number
pageIndex: number
pageSize: number
totalPages: number
hasPrevious: boolean
hasNext: boolean
}
/**
* 订单详情(GET /api/v2/order/detail/{id})
*/
export interface OrderDetailDto {
/** 订单记录 id */
id: number
/** 标题(含球台名称) */
projectName: string
/** 订单状态文案 */
statusText: string
/** 店长电话 */
storePhone: string
/** 开台时长文案 */
durationText: string
/** 实付金额 */
actualAmount: number | null
/** 价格(元/小时) */
pricePerHour: number
/** 折扣价(元/小时) */
discountPricePerHour: number
/** 在线总支付 */
onlineTotalPayment: number
/** 押金 */
deposit: number
/** 订单原价 */
originalAmount: number
/** 会员价,null 表示"-" */
memberPrice: number | null
/** 优惠券抵扣金额 */
couponDeduction: number | null
/** 优惠券描述 */
couponDescription: string | null
/** 门店名称 */
bpName: string
/** 门店地址 */
bpAddress: string
/** 桌台信息(如"1号台") */
tableName: string
/** 开台时间 */
startTime: string
/** 结束时间 */
stopTime: string
/** 订单编号 */
outTradeNo: string
/** 收费标准描述 */
chargingStandard: string
/** 支付方式名称 */
payTypeName: string
}

212
src/types/startDevice.ts

@ -1,6 +1,12 @@ @@ -1,6 +1,12 @@
/**
* 开台操作相关类型定义
* 对应接口:/api/v2/TablesDev/start_device
* 开台/关台/扫码相关类型定义
* 对应新版接口(磁力开台):
* - GET /api/v2/magdev/qrcode 扫码获取台桌信息
* - POST /api/v2/magdev/peroperate 去开台(预检)
* - POST /api/v2/magdev/operate 开台/关台/续费/关灯重开/分享等统一操作
* - GET /api/v2/magdev/getopstatus 查询开台结果
* 旧版 /api/v2/TablesDev/* 系列接口已废弃,相关类型与方法已移除,
* 具体操作请求参数见 src/types/magdev.ts 的 OperateRequestDto。
*/
/**
@ -26,6 +32,12 @@ export enum CtrlType { @@ -26,6 +32,12 @@ export enum CtrlType {
TurnOffRestart = 7,
/** 替人付费关台 */
PayForOthersClose = 8,
/** AA制付款关台(棋牌室场景,台球暂未接入) */
AaPayClose = 9,
/** 打开存杆柜(存杆柜场景,暂未接入) */
OpenLocker = 10,
/** 打开棋牌室(开台过程中有人中途加入,暂未接入) */
OpenChessRoom = 11,
}
/**
@ -44,32 +56,6 @@ export enum PayType { @@ -44,32 +56,6 @@ export enum PayType {
BankCard = 5,
}
/**
* 开台请求参数
*/
export interface StartGroupDevRequestDto {
/** 设备编码(必填) */
grpSn: string
/** OpenId(必填) */
openId: string
/** 操作类型:押金开台(1)、定时开台(2)、信用开台(3)、团购券开台(4)、立即关台(5)、续费(6)、关灯重开(7)、替人付费关台(8) */
ctrlType?: CtrlType
/** 消费金额 */
amount?: number
/** 支付方式:微信支付(1)、支付宝(2)、银联支付(3)、余额支付(4)、银行卡支付(5) */
payType?: PayType
/** 交易类型:1-JSAPI/小程序、2-Native扫码、3-APP、4-付款码、5-H5、6-刷脸、7-小程序支付 */
tradeType?: number
/** 开台时长(小时),定时开台使用 */
durationHours?: number
/** 团购券码,团购券开台使用 */
couponCode?: string
/** 备注 */
remark?: string
/** 附加数据(JSON格式) */
attach?: string
}
/**
* 微信支付参数
*/
@ -86,91 +72,7 @@ export interface WechatPayParameters { @@ -86,91 +72,7 @@ export interface WechatPayParameters {
paySign: string
}
/**
* 支付响应数据(嵌套在 StartDeviceResponse 中)
*/
export interface PaymentResponseDto {
/** 商户订单号 */
outTradeNo: string
/** 支付渠道:1-微信、2-支付宝、3-银联等 */
channel: number
/** 调起支付所需参数(微信支付时使用) */
payParameters: WechatPayParameters | null
/** 支付链接(扫码支付时使用) */
payUrl: string
}
/**
* 开台响应数据(对应 data 字段内容)
*/
export interface StartDeviceResponse {
/** 消费记录ID */
consumptionId: number
/** 错误描述(业务失败时有值) */
errorMessage: string
/** 支付信息(需要支付时有值,余额支付等无需支付时为 null) */
paymentResponseDto: PaymentResponseDto | null
}
/**
* 台球桌信息
*/
export interface TableInfo {
/** 设备编码 */
grpSn: string
/** 台球桌名称 */
tableName: string
/** 台球桌类型 */
tableType: string
/** 门店名称 */
storeName: string
/** 门店电话 */
storePhone: string
/** 价格(元/小时) */
price: number
/** VIP价格(元/小时) */
vipPrice: number
/** 押金金额 */
deposit: number
/** 价格时段 */
pricePeriod: string
}
/**
* 订单信息
*/
export interface OrderInfo {
/** 操作记录ID */
operationId: number
/** 设备编码 */
grpSn: string
/** 开始时间 */
startTime: string
/** 已消费时长(分钟) */
consumedMinutes: number
/** 已消费金额 */
consumedAmount: number
/** 剩余时长(分钟) */
remainingMinutes?: number
/** 订单编号 */
orderNo: string
/** 开台方式 */
startMethod: string
/** 收费标准 */
pricingRule: string
}
/**
* 获取台球桌价格请求参数
*/
export interface PriceRequestDto {
/** 用户微信 OpenId */
openId: string
/** 设备序列号 */
groupDevSn: string
}
// ─── 获取开台信息接口相关类型 ───────────────────────────────────────────────
// ─── 获取开台信息接口相关类型(GET /api/v2/magdev/qrcode)──────────────────
/**
* 分时价格
@ -276,6 +178,8 @@ export interface OpInfoDto { @@ -276,6 +178,8 @@ export interface OpInfoDto {
export interface TableScanResponseDto {
/** open-可开台;close-已开台需关台 */
scanType: 'open' | 'close'
/** 是否本人开台,默认 true;false 表示扫的是别人正在使用的台(关台页需展示"帮他付款/AA制付款") */
isOwner: boolean
shopName: string
shopCode: string
instSn: string
@ -290,85 +194,3 @@ export interface TableScanResponseDto { @@ -290,85 +194,3 @@ export interface TableScanResponseDto {
/** 已开台时有值,可开台时为 null */
opInfo: OpInfoDto | null
}
// ─── 关台接口(/api/v2/TablesDev/stop)相关类型 ───────────────────────────────
/**
* 关台操作请求参数(对应 /api/v2/TablesDev/stop 接口)
* ctrlType 使用 CtrlType 枚举:5-立即关台、6-续费、7-关灯重开、8-替人付费关台
*/
export interface StopGroupDevRequestDto {
/** 设备序列号(必填) */
grpSn: string
/** 用户 OpenId(必填) */
openId: string
/** 操作类型:5-立即关台、6-续费、7-关灯重开、8-替人付费关台 */
ctrlType?: CtrlType
/** 支付方式:1-微信支付、2-支付宝、3-银联支付、4-余额支付、5-银行卡支付(可选) */
payType?: PayType
/** 交易类型:1-JSAPI小程序、2-Native扫码、3-APP、4-付款码、5-H5(可选) */
tradeType?: number
/** 续费金额(单位:元),仅 ctrlType=6(续费) 时需要 */
renewalAmount?: number
}
/**
* 关台操作响应数据(对应 /api/v2/TablesDev/stop 接口 data 字段)
*/
export interface StopGroupDevResponseDto {
/** 设备序列号 */
grpSn: string
/** 操作 ID(用于支付轮询) */
operationId: number
/** 关台时间 */
stopTime: string
/** 最终消费金额 */
finalAmount: number
/** 退款金额 */
refundAmount: number
/** 操作类型(对应 CtrlType:5-立即关台、6-续费、7-关灯重开) */
stopType: CtrlType
/** 续费时返回新的结束时间 */
newStopTime: string
/** 微信支付参数(needPay=true 时有值,否则为 null) */
payParameters: WechatPayParameters | null
/** 是否需要前端调起支付 */
needPay: boolean
/** 商户订单号,前端轮询用 */
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
}

20
src/utils/customerNormalize.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
/**
* 客户字段归一化(新接口全小写 / 旧 camelCase)
*/
import { CustomerDto } from '../types/login'
import { CustomerDto, CustomerBalanceListItem } from '../types/login'
import { pickField, toNumber } from '../utils/apiNormalize'
export const normalizeCustomer = (raw: Record<string, any> | null | undefined): CustomerDto => {
@ -28,6 +28,24 @@ export const normalizeCustomer = (raw: Record<string, any> | null | undefined): @@ -28,6 +28,24 @@ export const normalizeCustomer = (raw: Record<string, any> | null | undefined):
}
}
/** 归一化单条门店余额(balancelist 数组项) */
export const normalizeBalanceListItem = (raw: Record<string, any>): CustomerBalanceListItem => ({
bpSn: String(pickField(raw, 'bpSn', 'bpsn') ?? ''),
bpName: String(pickField(raw, 'bpName', 'bpname') ?? ''),
amount: toNumber(pickField(raw, 'amount')),
points: toNumber(pickField(raw, 'points')),
lockerCount: toNumber(pickField(raw, 'lockerCount', 'lockercount')),
vipEndTime: pickField<string | null>(raw, 'vipEndTime', 'vipendtime') ?? null,
})
/** 归一化客户跨店余额列表 */
export const normalizeBalanceList = (
raw: Record<string, any>[] | null | undefined
): CustomerBalanceListItem[] => {
if (!Array.isArray(raw)) return []
return raw.map(normalizeBalanceListItem)
}
/** 登录 data:扁平(Word)或嵌套 customer(旧) */
export const normalizeLoginData = (data: Record<string, any> | null | undefined) => {
const src = data || {}

70
src/utils/orderNormalize.ts

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
/**
* 订单记录 / 订单详情 字段归一化
* 对应接口:/api/v2/order/records、/api/v2/order/detail/{id}
*/
import { pickField, toNumber } from './apiNormalize'
import { OrderRecordItem, OrderRecordPagedResult, OrderDetailDto } from '../types/order'
/** 归一化单条订单记录(列表项) */
export const normalizeOrderRecordItem = (raw: Record<string, any>): OrderRecordItem => ({
id: toNumber(pickField(raw, 'id')),
title: String(pickField(raw, 'title') ?? ''),
statusText: String(pickField(raw, 'statusText', 'statustext') ?? ''),
bpName: String(pickField(raw, 'bpName', 'bpname') ?? ''),
startTime: String(pickField(raw, 'startTime', 'starttime') ?? ''),
durationText: String(pickField(raw, 'durationText', 'durationtext') ?? ''),
actualAmount: toNumber(pickField(raw, 'actualAmount', 'actualamount')),
})
/** 归一化订单记录分页响应 */
export const normalizeOrderRecordPagedResult = (
raw: Record<string, any> | null | undefined
): OrderRecordPagedResult => {
const src = raw || {}
const list = pickField<Record<string, any>[]>(src, 'data') || []
return {
data: Array.isArray(list) ? list.map(normalizeOrderRecordItem) : [],
totalCount: toNumber(pickField(src, 'totalCount', 'totalcount')),
pageIndex: toNumber(pickField(src, 'pageIndex', 'pageindex'), 1),
pageSize: toNumber(pickField(src, 'pageSize', 'pagesize'), 10),
totalPages: toNumber(pickField(src, 'totalPages', 'totalpages')),
hasPrevious: Boolean(pickField(src, 'hasPrevious', 'hasprevious') ?? false),
hasNext: Boolean(pickField(src, 'hasNext', 'hasnext') ?? false),
}
}
/** 归一化订单详情 */
export const normalizeOrderDetail = (
raw: Record<string, any> | null | undefined
): OrderDetailDto => {
const src = raw || {}
const memberPrice = pickField<number | null>(src, 'memberPrice', 'memberprice')
const couponDeduction = pickField<number | null>(src, 'couponDeduction', 'coupondeduction')
const couponDescription = pickField<string | null>(src, 'couponDescription', 'coupondescription')
const actualAmount = pickField<number | null>(src, 'actualAmount', 'actualamount')
return {
id: toNumber(pickField(src, 'id')),
projectName: String(pickField(src, 'projectName', 'projectname') ?? ''),
statusText: String(pickField(src, 'statusText', 'statustext') ?? ''),
storePhone: String(pickField(src, 'storePhone', 'storephone') ?? ''),
durationText: String(pickField(src, 'durationText', 'durationtext') ?? ''),
actualAmount: actualAmount === null || actualAmount === undefined ? null : toNumber(actualAmount),
pricePerHour: toNumber(pickField(src, 'pricePerHour', 'priceperhour')),
discountPricePerHour: toNumber(pickField(src, 'discountPricePerHour', 'discountpriceperhour')),
onlineTotalPayment: toNumber(pickField(src, 'onlineTotalPayment', 'onlinetotalpayment')),
deposit: toNumber(pickField(src, 'deposit')),
originalAmount: toNumber(pickField(src, 'originalAmount', 'originalamount')),
memberPrice: memberPrice === null || memberPrice === undefined ? null : toNumber(memberPrice),
couponDeduction:
couponDeduction === null || couponDeduction === undefined ? null : toNumber(couponDeduction),
couponDescription: couponDescription === null || couponDescription === undefined ? null : String(couponDescription),
bpName: String(pickField(src, 'bpName', 'bpname') ?? ''),
bpAddress: String(pickField(src, 'bpAddress', 'bpaddress') ?? ''),
tableName: String(pickField(src, 'tableName', 'tablename') ?? ''),
startTime: String(pickField(src, 'startTime', 'starttime') ?? ''),
stopTime: String(pickField(src, 'stopTime', 'stoptime') ?? ''),
outTradeNo: String(pickField(src, 'outTradeNo', 'outtradeno') ?? ''),
chargingStandard: String(pickField(src, 'chargingStandard', 'chargingstandard') ?? ''),
payTypeName: String(pickField(src, 'payTypeName', 'paytypename') ?? ''),
}
}

2
src/utils/scanNormalize.ts

@ -102,6 +102,8 @@ export const normalizeTableScanResponse = ( @@ -102,6 +102,8 @@ export const normalizeTableScanResponse = (
return {
scanType: (pickField<string>(src, 'scanType', 'scantype') || 'open') as 'open' | 'close',
// 后端默认 true(本人开台),只有明确返回 false 时才判定为"别人开的台"
isOwner: pickField(src, 'isOwner', 'isowner') !== false,
shopName: String(pickField(src, 'shopName', 'bpname', 'shopname') ?? ''),
shopCode: String(pickField(src, 'shopCode', 'bpsn', 'shopcode') ?? ''),
instSn: String(pickField(src, 'instSn', 'instsn') ?? ''),

Loading…
Cancel
Save