Files
zt-closet-wxmini-frontend/miniprogram/pages/mall/mall.ts
T
2026-09-13 02:28:46 +08:00

258 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { request } from '../../utils/request'
import { QuotaItem, UserInfo, GoodsItem, QuotaFlowItem, PagedResult, VirtualPayResult, OrderItem } from '../../types/models'
import { QUOTA_NAMES, GoodsType, GOODS_TYPE_NAMES } from '../../utils/config'
import { fenToYuan, formatDate, formatDateTime } from '../../utils/format'
import { normalizePaged } from '../../utils/paged'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'mall'
const GOODS_ICONS: Record<number, string> = {
[GoodsType.MEMBER]: 'icon-crown',
[GoodsType.UPLOAD]: 'icon-camera',
[GoodsType.AI]: 'icon-sparkles',
[GoodsType.CLOSET_SHARE]: 'icon-hanger',
[GoodsType.OUTFIT_SHARE]: 'icon-shirt',
}
const GOODS_FILTERS = [
{ name: '全部', value: 0 },
{ name: '会员包', value: 1 },
{ name: '上传包', value: 2 },
{ name: 'AI 推荐包', value: 3 },
{ name: '衣柜分享包', value: 4 },
{ name: '搭配分享包', value: 5 },
]
type PayPlatform = 'android' | 'ios' | 'harmony' | 'windows'
// 归一化平台:后端仅接受 android/ios/harmony/windows,devtools/mac 等调试环境默认按 android 处理
function normalizePlatform(): PayPlatform {
let p = 'android'
try {
p = wx.getSystemInfoSync().platform
} catch (e) {
// 获取失败时回退 android,避免请求体缺失导致参数校验失败
}
if (p === 'ios' || p === 'harmony' || p === 'windows') return p
return 'android'
}
Page({
data: {
view: 'mall' as 'mall' | 'flow',
userInfo: {} as UserInfo,
isVip: false,
vipExpireText: '',
quotas: [] as QuotaItem[],
goodsFilters: GOODS_FILTERS,
activeFilter: 0,
goods: [] as GoodsItem[],
flowList: [] as QuotaFlowItem[],
goodsIcons: GOODS_ICONS,
orderVisible: false,
orderGoods: null as GoodsItem | null,
submitting: false, // 下单防连点:连点「立即支付」会重复创建虚拟支付订单
loading: true,
},
onLoad(query: Record<string, string>) {
logger.debug(LOG, 'onLoad, query=', JSON.stringify(query))
if (query.tab === 'flow') this.setData({ view: 'flow' })
},
onShow() {
this.fetchUser()
this.fetchQuota()
this.fetchGoods()
if (this.data.view === 'flow') this.fetchFlow()
},
fetchUser() {
request<UserInfo>({ url: '/user/info', showError: false })
.then((user) => {
logger.debug(LOG, '用户信息加载成功, vipType=', user.vip_type)
const isVip = user.vip_type !== 1
this.setData({
userInfo: user,
isVip,
vipExpireText: isVip && user.vip_expire ? `有效期至 ${formatDate(user.vip_expire)}` : '',
})
})
.catch((e: any) => logger.warn(LOG, '用户信息加载失败(静默降级), code=', e && e.code))
},
fetchQuota() {
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => {
const quotas = res.quotas.map((q) => ({
...q,
_name: QUOTA_NAMES[q.quota_type] || '额度',
_percent: q.total > 0 ? Math.round((q.used / q.total) * 100) : 0,
}))
this.setData({ quotas })
})
.catch((e: any) => logger.warn(LOG, '额度加载失败(静默降级), code=', e && e.code))
},
fetchGoods() {
const params: Record<string, any> = { page: 1, page_size: 50 }
const filter = this.data.activeFilter
if (filter > 0) params.goods_type = filter
request<PagedResult<GoodsItem>>({ url: '/goods', data: params, auth: false })
.then((res) => {
const goods = normalizePaged<GoodsItem>(res).list.map((g) => ({
...g,
_icon: GOODS_ICONS[g.goods_type] || 'icon-crown',
_typeName: GOODS_TYPE_NAMES[g.goods_type] || '',
_priceText: fenToYuan(g.discount_price),
_originText: fenToYuan(g.price),
}))
logger.debug(LOG, '商品列表加载成功, 条数=', goods.length)
this.setData({ goods, loading: false })
})
.catch((e: any) => {
logger.warn(LOG, '商品列表加载失败, code=', e && e.code)
this.setData({ loading: false })
})
},
fetchFlow() {
request<PagedResult<QuotaFlowItem>>({ url: '/quota/flow', data: { page: 1, page_size: 50 } })
.then((res) => {
const list = normalizePaged<QuotaFlowItem>(res).list.map((it) => ({
...it,
_time: formatDateTime(it.created_at),
_sign: it.change_num > 0 ? `+${it.change_num}` : `${it.change_num}`,
_name: QUOTA_NAMES[it.quota_type] || '额度',
}))
this.setData({ flowList: list })
})
.catch((e: any) => logger.warn(LOG, '额度流水加载失败(静默降级), code=', e && e.code))
},
onGoodsFilter(e: WechatMiniprogram.TouchEvent) {
const index = Number(e.currentTarget.dataset.index)
if (index === this.data.activeFilter) return
this.setData({ activeFilter: index, loading: true })
this.fetchGoods()
},
onTapMember() {
this.setData({ activeFilter: 1, loading: true })
this.fetchGoods()
},
onSwitchMall() {
this.setData({ view: 'mall' })
},
onSwitchFlow() {
this.setData({ view: 'flow' })
this.fetchFlow()
},
onBuy(e: WechatMiniprogram.TouchEvent) {
const goods = e.currentTarget.dataset.goods as GoodsItem
this.setData({ orderGoods: goods, orderVisible: true })
},
onCloseOrder() {
this.setData({ orderVisible: false })
},
noop() {},
confirmPay() {
if (this.data.submitting) return
if (!this.data.orderGoods) return
this.setData({ submitting: true })
request<VirtualPayResult>({
url: '/order/virtual',
method: 'POST',
data: {
goods_code: this.data.orderGoods.goods_code,
platform: normalizePlatform(),
},
})
.then((res) => {
logger.info(LOG, '下单成功, orderNo=', res.order_no)
this.setData({ orderVisible: false, submitting: false })
this.doPayment(res)
})
.catch((e: any) => {
logger.warn(LOG, '下单失败, code=', e && e.code)
this.setData({ submitting: false })
})
},
doPayment(res: VirtualPayResult) {
logger.debug(LOG, '拉起虚拟支付, orderNo=', res.order_no)
wx.requestVirtualPayment({
signData: res.sign_data,
paySig: res.pay_sig,
signature: res.signature,
mode: 'short_series_goods',
success: () => {
// 拉起成功仅代表进入支付页,最终到账以后端发货推送为准,轮询订单确认
logger.info(LOG, '支付页拉起成功, orderNo=', res.order_no)
this.pollOrder(res.order_no)
},
fail: (e: any) => {
logger.error(LOG, '拉起支付失败, orderNo=', res.order_no, 'err=', e && e.errMsg)
},
})
},
pollOrder(orderNo: string) {
this.clearPayTimer()
let count = 0
;(this as any).payTimer = setInterval(() => {
count++
request<PagedResult<OrderItem>>({ url: '/order', data: { page: 1, page_size: 50 }, showError: false })
.then((res) => {
const order = normalizePaged<OrderItem>(res).list.find((it) => it.order_no === orderNo)
if (order && order.order_status === 2) {
this.clearPayTimer()
logger.info(LOG, '支付成功, orderNo=', orderNo)
wx.showToast({ title: '支付成功', icon: 'success' })
this.fetchQuota()
this.fetchUser()
} else if (order && (order.order_status === 3 || order.order_status === 4 || order.order_status === 5)) {
this.clearPayTimer()
logger.warn(LOG, '订单未成功支付, orderNo=', orderNo, 'status=', order.order_status)
const msg = order.order_status === 5 ? '订单已退款' : '订单未支付成功'
wx.showToast({ title: msg, icon: 'none' })
} else if (count >= 10) {
logger.warn(LOG, '支付轮询超时, orderNo=', orderNo)
this.finishPayTimeout()
}
})
.catch((e: any) => {
logger.warn(LOG, '支付轮询请求失败, orderNo=', orderNo, 'code=', e && e.code)
if (count >= 10) this.finishPayTimeout()
})
}, 1500)
},
clearPayTimer() {
const timer = (this as any).payTimer
if (timer) {
clearInterval(timer)
;(this as any).payTimer = null
}
},
finishPayTimeout() {
this.clearPayTimer()
wx.showToast({ title: '到账可能有延迟,请稍后查看', icon: 'none' })
this.fetchQuota()
this.fetchUser()
},
onUnload() {
// 离开商城后停止支付轮询,避免对已卸载页面 setData
this.clearPayTimer()
},
})