diff --git a/QR_CODE_DESIGN.md b/QR_CODE_DESIGN.md new file mode 100644 index 0000000..a370f14 --- /dev/null +++ b/QR_CODE_DESIGN.md @@ -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. **重点关注二维码的数据结构设计**,确保包含必要的业务参数 + +这样的设计既保证了功能的完整性,又具有良好的可维护性和扩展性。 \ No newline at end of file diff --git a/src/api/customer.ts b/src/api/customer.ts index 035d2e9..0ed1f3d 100644 --- a/src/api/customer.ts +++ b/src/api/customer.ts @@ -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) => { +/** + + * 查询客户在所有已消费门店的余额列表(含积分/存杆柜数量/VIP到期时间) + + * 对应接口:GET /customer/balancelist/{openId} + + * 用于 pagesOrder/accountBalance 的"账户余额" Tab(BalanceList 组件) + + */ + +export const getCustomerBalanceList = async (openId: string) => { + + const res = await get>( + + `/customer/balancelist/${encodeURIComponent(openId)}` + + ) + + return { ...res, data: normalizeBalanceList(res.data as unknown as Record[]) } + +} + + + /** * 刷新用户资料 + 余额(有 bpSn 时) diff --git a/src/api/order.ts b/src/api/order.ts index 1eed2c3..7645a00 100644 --- a/src/api/order.ts +++ b/src/api/order.ts @@ -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 ( } 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>('/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) } +} + +/** + * 查询订单详情,用于 pagesUser/orderDetail + * @param id 订单记录 id(来自 order/records 列表项的 id 字段) + */ +export const getOrderDetail = async (id: number | string) => { + const res = await get>(`/order/detail/${id}`) + return { ...res, data: normalizeOrderDetail(res.data as Record) } +} diff --git a/src/api/startDevice.ts b/src/api/startDevice.ts index c853c77..eabec51 100644 --- a/src/api/startDevice.ts +++ b/src/api/startDevice.ts @@ -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('/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 = }) } -/** - * 押金开台 - * @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('/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) return { ...res, data: normalizeTableScanResponse(res.data as Record) } } -/** - * 根据用户获取台球桌实际价格(VIP/普通用户价格不同) - * @param openId 用户 OpenId - * @param groupDevSn 设备序列号(即 grpSn) - * @returns 该用户对应的价格(元/小时) - */ -export const getTablePrice = (openId: string, groupDevSn: string) => { - const data: PriceRequestDto = { openId, groupDevSn } - 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 } // 导出所有方法和类型 export default { - startDevice, - stopDevice, invokeWechatPay, - depositStart, - balanceStart, - timedStart, - couponStart, - immediateClose, - renewFee, - turnOffRestart, - checkOpenStatus, - pollCheckOpenStatus, - updateShareStatus, - getTablePrice, getTableInfo, CtrlType, PayType, diff --git a/src/pages/qrHandler/index.tsx b/src/pages/qrHandler/index.tsx index e64980b..e15b3eb 100644 --- a/src/pages/qrHandler/index.tsx +++ b/src/pages/qrHandler/index.tsx @@ -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 } diff --git a/src/services/tableApi.ts b/src/services/tableApi.ts deleted file mode 100644 index 551dec1..0000000 --- a/src/services/tableApi.ts +++ /dev/null @@ -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('/Tables/start', data, { - showLoading: true, - loadingText: '开台中...', - }) -} - -/** - * 调起微信支付 - * @param payParams 支付参数 - * @returns Promise - */ -export const invokeWechatPay = (payParams: WechatPayParameters): Promise => { - 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, -} diff --git a/src/store/table.ts b/src/store/table.ts index ccf2e1a..39591f4 100644 --- a/src/store/table.ts +++ b/src/store/table.ts @@ -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 { /** 当前设备编码 */ @@ -11,12 +11,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 { 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 { 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 { 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') } diff --git a/src/types/login.ts b/src/types/login.ts index e3e2212..a8659ea 100644 --- a/src/types/login.ts +++ b/src/types/login.ts @@ -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 diff --git a/src/types/order.ts b/src/types/order.ts index 550b861..79ffe27 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -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 +} diff --git a/src/types/startDevice.ts b/src/types/startDevice.ts index a386324..0e139bf 100644 --- a/src/types/startDevice.ts +++ b/src/types/startDevice.ts @@ -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 { TurnOffRestart = 7, /** 替人付费关台 */ PayForOthersClose = 8, + /** AA制付款关台(棋牌室场景,台球暂未接入) */ + AaPayClose = 9, + /** 打开存杆柜(存杆柜场景,暂未接入) */ + OpenLocker = 10, + /** 打开棋牌室(开台过程中有人中途加入,暂未接入) */ + OpenChessRoom = 11, } /** @@ -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 { 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 { 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 { /** 已开台时有值,可开台时为 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 -} diff --git a/src/utils/customerNormalize.ts b/src/utils/customerNormalize.ts index 0f12c55..10c9323 100644 --- a/src/utils/customerNormalize.ts +++ b/src/utils/customerNormalize.ts @@ -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 | null | undefined): CustomerDto => { @@ -28,6 +28,24 @@ export const normalizeCustomer = (raw: Record | null | undefined): } } +/** 归一化单条门店余额(balancelist 数组项) */ +export const normalizeBalanceListItem = (raw: Record): 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(raw, 'vipEndTime', 'vipendtime') ?? null, +}) + +/** 归一化客户跨店余额列表 */ +export const normalizeBalanceList = ( + raw: Record[] | null | undefined +): CustomerBalanceListItem[] => { + if (!Array.isArray(raw)) return [] + return raw.map(normalizeBalanceListItem) +} + /** 登录 data:扁平(Word)或嵌套 customer(旧) */ export const normalizeLoginData = (data: Record | null | undefined) => { const src = data || {} diff --git a/src/utils/orderNormalize.ts b/src/utils/orderNormalize.ts new file mode 100644 index 0000000..efff582 --- /dev/null +++ b/src/utils/orderNormalize.ts @@ -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): 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 | null | undefined +): OrderRecordPagedResult => { + const src = raw || {} + const list = pickField[]>(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 | null | undefined +): OrderDetailDto => { + const src = raw || {} + const memberPrice = pickField(src, 'memberPrice', 'memberprice') + const couponDeduction = pickField(src, 'couponDeduction', 'coupondeduction') + const couponDescription = pickField(src, 'couponDescription', 'coupondescription') + const actualAmount = pickField(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') ?? ''), + } +} diff --git a/src/utils/scanNormalize.ts b/src/utils/scanNormalize.ts index 398bf8f..3206335 100644 --- a/src/utils/scanNormalize.ts +++ b/src/utils/scanNormalize.ts @@ -102,6 +102,8 @@ export const normalizeTableScanResponse = ( return { scanType: (pickField(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') ?? ''),