feat(M08-C): 补管理员验券与经营统计
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
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())}`
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user