Files
qipai/miniapp/pages/manager/business.js
T

224 lines
7.5 KiB
JavaScript

const { request, cents, clientRequestId, ensureLogin } = require('../../utils/api.js')
const providers = [
{ value: 'MEITUAN', label: '美团' },
{ value: 'DIANPING', label: '大众点评' },
{ value: 'DOUYIN', label: '抖音' },
{ value: 'KUAISHOU', label: '快手' },
]
const providerLabels = Object.fromEntries(providers.map((item) => [item.value, item.label]))
Page({
data: {
storeId: '',
storeName: '',
loading: false,
redeeming: false,
errorMessage: '',
canRedeem: false,
rangeDays: 30,
ranges: [7, 30, 90],
summary: {
orderTotal: 0,
collectedAmountText: '¥0.00',
payingMemberTotal: 0,
voucherSucceeded: 0,
availableRoomTotal: 0,
attentionRoomTotal: 0,
},
paymentChannels: [],
dailyRevenue: [],
providers,
providerIndex: 0,
pendingOrders: [],
orderIndex: 0,
voucherCode: '',
manualNote: '',
redemptionRecords: [],
},
async onLoad(options) {
this.setData({
storeId: options.storeId || '',
storeName: options.storeName || '',
})
await this.loadBusiness()
},
async onPullDownRefresh() {
await this.loadBusiness()
wx.stopPullDownRefresh()
},
async loadBusiness() {
if (!this.data.storeId) {
this.setData({ errorMessage: '缺少门店信息' })
return
}
this.setData({ loading: true, errorMessage: '' })
try {
await ensureLogin()
const me = await request('/auth/me')
const access = me.data?.access || { roles: [], capabilities: [] }
const canRedeem = access.capabilities.includes('store.operation.write')
|| access.capabilities.includes('tenant.manage')
|| access.roles.includes('PLATFORM_ADMIN')
this.setData({ canRedeem })
const storeId = encodeURIComponent(this.data.storeId)
const to = new Date()
const from = new Date(to.getTime() - this.data.rangeDays * 86400000)
const [statistics, orders, records] = await Promise.all([
request(`/management/statistics?storeId=${storeId}&from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`),
canRedeem
? request(`/orders?page=1&pageSize=50&storeId=${storeId}&status=PENDING_PAYMENT`)
: Promise.resolve({ data: { items: [] } }),
request(`/management/third-party/records?storeId=${storeId}`),
])
this.presentStatistics(statistics.data || {})
const pendingOrders = (orders.data?.items || []).map((item) => {
const unpaidAmountCents = Math.max(0, Number(item.totalAmountCents || 0) - Number(item.paidAmountCents || 0))
return {
...item,
unpaidAmountCents,
label: `${item.orderNo} · ${item.roomName || item.roomNo || '房间'} · ${cents(unpaidAmountCents)}`,
}
})
const redemptionRecords = (records.data?.redemptions || []).map((item) => ({
...item,
providerText: providerLabels[item.provider] || item.provider,
statusText: this.redemptionStatus(item.status),
timeText: this.formatTime(item.completedAt || item.createdAt),
}))
this.setData({
pendingOrders,
orderIndex: Math.min(this.data.orderIndex, Math.max(0, pendingOrders.length - 1)),
redemptionRecords,
})
} catch (error) {
this.setData({ errorMessage: error.message || '经营数据加载失败' })
} finally {
this.setData({ loading: false })
}
},
presentStatistics(data) {
const summary = data.summary || {}
this.setData({
summary: {
...summary,
collectedAmountText: cents(summary.collectedAmountCents),
},
paymentChannels: (data.paymentChannels || []).map((item) => ({
...item,
amountText: cents(item.amountCents),
})),
dailyRevenue: (data.dailyRevenue || []).slice(-14).reverse().map((item) => ({
...item,
amountText: cents(item.amountCents),
})),
})
},
selectRange(event) {
const rangeDays = Number(event.currentTarget.dataset.days)
if (!rangeDays || rangeDays === this.data.rangeDays) return
this.setData({ rangeDays })
this.loadBusiness()
},
selectProvider(event) {
this.setData({ providerIndex: Number(event.detail.value) || 0 })
},
selectOrder(event) {
this.setData({ orderIndex: Number(event.detail.value) || 0 })
},
inputVoucherCode(event) {
this.setData({ voucherCode: String(event.detail.value || '').trim() })
},
inputManualNote(event) {
this.setData({ manualNote: String(event.detail.value || '').trim() })
},
scanVoucher() {
if (!this.data.canRedeem) return
wx.scanCode({
scanType: ['barCode', 'qrCode'],
success: ({ result }) => this.setData({ voucherCode: String(result || '').trim() }),
fail: (error) => {
if (!String(error.errMsg || '').includes('cancel')) {
this.setData({ errorMessage: '扫码失败,请手工输入券码' })
}
},
})
},
redeemVoucher(event) {
if (!this.data.canRedeem || this.data.redeeming) return
const manual = event.currentTarget.dataset.mode === 'manual'
const order = this.data.pendingOrders[this.data.orderIndex]
const provider = this.data.providers[this.data.providerIndex]
const voucherCode = this.data.voucherCode.trim()
const manualNote = this.data.manualNote.trim()
if (!order || !provider || voucherCode.length < 4) {
this.setData({ errorMessage: '请选择待支付订单并输入有效券码' })
return
}
if (manual && !manualNote) {
this.setData({ errorMessage: '人工核销必须填写确认说明' })
return
}
const title = manual ? '人工确认核销' : '在线验券'
wx.showModal({
title,
content: `确认将${provider.label}券用于订单 ${order.orderNo},抵扣 ${cents(order.unpaidAmountCents)} 吗?`,
success: ({ confirm }) => {
if (confirm) this.submitRedemption({ manual, order, provider, voucherCode, manualNote })
},
})
},
async submitRedemption({ manual, order, provider, voucherCode, manualNote }) {
this.setData({ redeeming: true, errorMessage: '' })
try {
const payload = {
provider: provider.value,
voucherCode,
orderId: order.id,
clientRequestId: clientRequestId(manual ? 'manager-manual-redeem' : 'manager-redeem'),
...(manual ? { amountCents: order.unpaidAmountCents, note: manualNote } : {}),
}
const endpoint = manual
? '/management/group-vouchers/redeem-manual'
: '/management/group-vouchers/redeem'
const response = await request(endpoint, { method: 'POST', data: payload })
const result = response.data || {}
if (result.status !== 'SUCCEEDED') {
throw new Error(result.failureCode ? `验券未成功:${result.failureCode}` : '验券正在处理中')
}
this.setData({ voucherCode: '', manualNote: '' })
wx.showToast({ title: '核销成功', icon: 'success' })
await this.loadBusiness()
} catch (error) {
this.setData({ errorMessage: error.message || '验券失败' })
} finally {
this.setData({ redeeming: false })
}
},
redemptionStatus(status) {
return { SUCCEEDED: '成功', FAILED: '失败', PENDING: '处理中' }[status] || status
},
formatTime(value) {
if (!value) return ''
const date = new Date(value)
if (Number.isNaN(date.getTime())) return String(value)
const pad = (part) => String(part).padStart(2, '0')
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
},
})