From 44c46b78eafd38d71a1c7957dd4c71693acccbf7 Mon Sep 17 00:00:00 2001 From: ltx <19304474232@163.com> Date: Wed, 8 Apr 2026 00:40:29 +0800 Subject: [PATCH] =?UTF-8?q?update:=E6=8A=A5=E5=90=8D&=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/apply/apply.js | 168 +++++++++++++++++++++++++- subpackages/mine/cert/cert.js | 21 +++- subpackages/mine/cert/cert.wxml | 8 +- subpackages/mine/order/order.js | 101 +++++++++------- subpackages/mine/order/order.json | 3 +- subpackages/mine/order/order.wxml | 4 +- subpackages/mine/team/team.js | 46 ++++++-- subpackages/mine/team/team.json | 2 +- subpackages/mine/team/team.wxml | 8 -- subpackages/mine/team/team.wxss | 34 ++++++ utils/auth.js | 26 +++++ utils/http.js | 188 +++++++++++++++++++++--------- 12 files changed, 484 insertions(+), 125 deletions(-) diff --git a/pages/apply/apply.js b/pages/apply/apply.js index b86db8a..9893981 100644 --- a/pages/apply/apply.js +++ b/pages/apply/apply.js @@ -607,10 +607,174 @@ Page({ }, /** 线上支付 */ - onPayOnline() { + async onPayOnline() { const payload = this._validateAndGetPayload(2); if (!payload) return; - this._submitApply(payload); + + try { + // 1. 获取 openid + const openid = await this._getOpenId(); + if (!openid) { + wx.showToast({ title: '获取 openid 失败', icon: 'none' }); + return; + } + + // 2. 提交报名 + this.setData({ submitting: true }); + const registrationId = await this._submitApplyForPayment(payload); + + // 3. 选择支付方式并获取支付参数 + await this._selectPaymentMethod(registrationId, openid, 2); + + // 4. 获取支付信息 + const paymentInfo = await this._getPaymentInfo(registrationId, openid); + + // 5. 调起微信支付 + await this._invokeWeChatPay(paymentInfo.wechatPayParams, registrationId); + + } catch (error) { + console.error('支付流程异常:', error); + wx.showToast({ + title: error && error.message ? error.message : '支付失败,请稍后重试', + icon: 'none' + }); + } finally { + this.setData({ submitting: false }); + } + }, + + /** 获取 openid */ + async _getOpenId() { + const app = getApp(); + const cached = + (app && app.globalData && app.globalData.openid) || wx.getStorageSync('openid') || ''; + if (cached) { + return cached; + } + + return new Promise((resolve, reject) => { + wx.login({ + success: (loginRes) => { + const code = loginRes.code; + if (!code) { + reject(new Error('未获取到 code')); + return; + } + + request({ + path: API.wechatOpenidByJsCode, + method: 'POST', + needAuth: false, + data: { code }, + contentType: 'application/json' + }) + .then((data) => { + const oid = + data && typeof data === 'object' + ? data.openId || data.OpenId || data.openid || '' + : ''; + const openid = typeof oid === 'string' ? oid : ''; + if (openid && app && typeof app.setAuth === 'function') { + app.setAuth({ openid }); + } + if (openid) { + resolve(openid); + } else { + reject(new Error('接口未返回 openid')); + } + }) + .catch((err) => { + reject(err); + }); + }, + fail: () => { + reject(new Error('wx.login 失败')); + } + }); + }); + }, + + /** 提交报名(用于支付流程) */ + _submitApplyForPayment(payload) { + return request({ + path: API.apply, + method: 'POST', + needAuth: true, + data: payload, + contentType: 'application/json' + }); + }, + + /** 选择支付方式 */ + _selectPaymentMethod(registrationId, openId, paymentMethod) { + return request({ + path: '/api/eventclient/select-payment-method', + method: 'POST', + needAuth: true, + data: { registrationId, openId, paymentMethod }, + contentType: 'application/json' + }); + }, + + /** 获取支付信息 */ + _getPaymentInfo(registrationId, openId) { + return request({ + path: '/api/eventclient/payment-info', + method: 'POST', + needAuth: true, + data: { registrationId, openId }, + contentType: 'application/json' + }); + }, + + /** 调起微信支付 */ + _invokeWeChatPay(wechatPayParams, registrationId) { + return new Promise((resolve, reject) => { + wx.requestPayment({ + ...wechatPayParams, + success: async () => { + try { + // 支付成功,同步支付状态 + await request({ + path: `/api/eventclient/sync-payment/${registrationId}`, + method: 'POST', + needAuth: true, + data: {}, + contentType: 'application/json' + }); + + wx.showToast({ title: '支付成功', icon: 'success' }); + setTimeout(() => { + wx.switchTab({ url: '/pages/mine/mine' }); + }, 1500); + resolve(); + } catch (err) { + console.error('同步支付状态失败:', err); + wx.showModal({ + title: '提示', + content: '支付成功,但同步状态失败,请稍后在订单列表中查看', + showCancel: false, + success: () => { + wx.switchTab({ url: '/pages/mine/mine' }); + } + }); + resolve(); + } + }, + fail: (err) => { + console.error('微信支付失败:', err); + if (err.errMsg && err.errMsg.includes('cancel')) { + wx.showToast({ title: '支付已取消', icon: 'none' }); + } else { + wx.showToast({ + title: err.errMsg || '支付失败,请稍后重试', + icon: 'none' + }); + } + reject(err); + } + }); + }); }, _submitApply(payload) { diff --git a/subpackages/mine/cert/cert.js b/subpackages/mine/cert/cert.js index 4dd35f1..6715a10 100644 --- a/subpackages/mine/cert/cert.js +++ b/subpackages/mine/cert/cert.js @@ -24,12 +24,10 @@ Page({ wx.redirectTo({ url: '/subpackages/mine/login/login' }); return; } - this.loadProfile(); this.resetAndLoad(); }, onPullDownRefresh() { - this.loadProfile(); this.resetAndLoad() .finally(() => wx.stopPullDownRefresh()); }, @@ -37,7 +35,6 @@ Page({ onRefresh() { if (this.data.refresherTriggered) return; this.setData({ refresherTriggered: true }); - this.loadProfile(); this.resetAndLoad() .finally(() => this.setData({ refresherTriggered: false })); }, @@ -102,7 +99,23 @@ Page({ const formatted = rows.map((item, idx) => ({ ...item, id: item.id != null ? item.id : `${activeTab}-${pageNo}-${idx}`, - timeStr: item.createdTime ? formatTime(item.createdTime) : (item.timeStr || '') + // 赛事名称 + eventName: item.eventName || '', + // 项目-组别展示,优先用 projectGroupDisplay,否则拼接 projectName + groupName + projectGroupDisplay: + item.projectGroupDisplay || + [item.projectName, item.groupName].filter(Boolean).join(' - ') || + '-', + // 证书链接 + certificateUrl: item.certificateUrl || '', + // 证书名称(用于查看证书时的标题) + certificateName: item.certificateName || item.eventName || '证书', + // 颁发时间 + timeStr: item.issuedTime + ? formatTime(item.issuedTime) + : item.createdTime + ? formatTime(item.createdTime) + : '' })); const newRecords = this.data.records.concat(formatted); diff --git a/subpackages/mine/cert/cert.wxml b/subpackages/mine/cert/cert.wxml index 09b672c..091265d 100644 --- a/subpackages/mine/cert/cert.wxml +++ b/subpackages/mine/cert/cert.wxml @@ -47,12 +47,12 @@ - {{item.title || item.eventTitle || '-'}} - {{item.timeStr || item.createdTimeStr || ''}} + {{item.eventName || '-'}} + {{item.timeStr}} - {{item.projectInfo || item.projectName || '-'}} + {{item.projectGroupDisplay || '-'}} - 查看证书 + 查看证书 diff --git a/subpackages/mine/order/order.js b/subpackages/mine/order/order.js index ced4674..1b250ec 100644 --- a/subpackages/mine/order/order.js +++ b/subpackages/mine/order/order.js @@ -1,51 +1,70 @@ +const auth = require('../../../utils/auth'); +const { formatTime } = require('../../../utils/format'); + +const PAGE_SIZE = 10; + +// orderStatus 枚举 → 显示文案 & 样式 key +const STATUS_MAP = { + Pending: { text: '待支付', key: 'pending' }, + Paid: { text: '交易成功', key: 'success' }, + Cancelled: { text: '已取消', key: 'cancelled' }, + Refunded: { text: '已退款', key: 'refunded' }, + Refunding: { text: '退款中', key: 'pending' } +}; + +function formatOrder(item) { + const statusInfo = STATUS_MAP[item.orderStatus] || { text: item.orderStatus || '-', key: 'default' }; + return { + ...item, + statusText: statusInfo.text, + statusKey: statusInfo.key, + payTimeStr: item.paymentTime ? formatTime(item.paymentTime) : (item.createdTime ? formatTime(item.createdTime) : '-'), + actualAmount: item.actualAmount != null ? item.actualAmount : '-', + refundAmount: item.refundAmount != null && item.refundAmount > 0 ? item.refundAmount : '' + }; +} + Page({ data: { loading: false, noMore: false, - orders: [ - { - id: 1, - eventName: '2024上海市学生运动会跳绳比赛', - statusText: '交易成功', - statusKey: 'success', - payTimeStr: '2025-08-05 16:26', - actualAmount: '859', - refundAmount: '' - }, - { - id: 2, - eventName: '2024上海市学生运动会跳绳比赛', - statusText: '交易取消', - statusKey: 'cancelled', - payTimeStr: '2025-08-05 16:26', - actualAmount: '859', - refundAmount: '859' - }, - { - id: 3, - eventName: '2024上海市学生运动会跳绳比赛', - statusText: '交易成功', - statusKey: 'success', - payTimeStr: '2025-08-05 16:26', - actualAmount: '859', - refundAmount: '' - }, - { - id: 4, - eventName: '2024上海市学生运动会跳绳比赛', - statusText: '交易成功', - statusKey: 'success', - payTimeStr: '2025-08-05 16:26', - actualAmount: '859', - refundAmount: '' - } - ] + pageNo: 1, + total: 0, + orders: [] }, - onLoad() {}, + onLoad() { + this.loadOrders(true); + }, onPullDownRefresh() { - wx.stopPullDownRefresh(); + this.loadOrders(true).finally(() => wx.stopPullDownRefresh()); + }, + + async loadOrders(refresh = false) { + if (this.data.loading) return; + const pageNo = refresh ? 1 : this.data.pageNo; + if (!refresh && this.data.noMore) return; + + this.setData({ loading: true }); + try { + const res = await auth.fetchMyOrders({ pageNo, pageSize: PAGE_SIZE }); + const rows = (res && res.rows) || []; + const total = (res && res.total) || 0; + const formatted = rows.map(formatOrder); + const newOrders = refresh ? formatted : [...this.data.orders, ...formatted]; + const noMore = newOrders.length >= total; + this.setData({ + orders: newOrders, + pageNo: pageNo + 1, + total, + noMore + }); + } catch (err) { + wx.showToast({ title: err?.message || '加载失败', icon: 'none' }); + } finally { + this.setData({ loading: false }); + } }, onBack() { @@ -62,6 +81,6 @@ Page({ }, onLoadMore() { - // TODO: 分页加载 + this.loadOrders(false); } }); diff --git a/subpackages/mine/order/order.json b/subpackages/mine/order/order.json index a5c6c4b..6799bd6 100644 --- a/subpackages/mine/order/order.json +++ b/subpackages/mine/order/order.json @@ -1,5 +1,6 @@ { - "navigationStyle": "custom", + "navigationBarTitleText": "我的订单", + "navigationBarBackgroundColor": "#ffffff", "enablePullDownRefresh": true, "usingComponents": { "mp-navigation-bar": "weui-miniprogram/navigation-bar/navigation-bar" diff --git a/subpackages/mine/order/order.wxml b/subpackages/mine/order/order.wxml index 2025755..7d61ab7 100644 --- a/subpackages/mine/order/order.wxml +++ b/subpackages/mine/order/order.wxml @@ -1,11 +1,11 @@ - + > --> wx.stopPullDownRefresh()); + }, + + async loadTeams(refresh = false) { + if (this.data.loading) return; + const pageNo = refresh ? 1 : this.data.pageNo; + if (!refresh && this.data.noMore) return; + + this.setData({ loading: true }); + try { + const res = await auth.fetchMyTeams({ pageNo, pageSize: PAGE_SIZE }); + const rows = (res && res.rows) || []; + const total = (res && res.total) || 0; + const newTeams = refresh ? rows : [...this.data.teams, ...rows]; + const noMore = newTeams.length >= total; + this.setData({ + teams: newTeams, + pageNo: pageNo + 1, + total, + noMore + }); + } catch (err) { + wx.showToast({ title: err?.message || '加载失败', icon: 'none' }); + } finally { + this.setData({ loading: false }); + } }, onBack() { diff --git a/subpackages/mine/team/team.json b/subpackages/mine/team/team.json index a5c6c4b..b3e6d93 100644 --- a/subpackages/mine/team/team.json +++ b/subpackages/mine/team/team.json @@ -1,5 +1,5 @@ { - "navigationStyle": "custom", + "navigationBarTitleText": "我的团队", "enablePullDownRefresh": true, "usingComponents": { "mp-navigation-bar": "weui-miniprogram/navigation-bar/navigation-bar" diff --git a/subpackages/mine/team/team.wxml b/subpackages/mine/team/team.wxml index 7ef6242..85d83b5 100644 --- a/subpackages/mine/team/team.wxml +++ b/subpackages/mine/team/team.wxml @@ -1,12 +1,4 @@ - - diff --git a/subpackages/mine/team/team.wxss b/subpackages/mine/team/team.wxss index 5a0363e..7bec390 100644 --- a/subpackages/mine/team/team.wxss +++ b/subpackages/mine/team/team.wxss @@ -4,6 +4,10 @@ background: #f5f7fa; } +view{ + box-sizing: border-box; +} + /* ===== 单位卡片 ===== */ .org-card { position: relative; @@ -109,12 +113,30 @@ border-bottom: none; } +.team-left { + display: flex; + align-items: center; + gap: 16rpx; + flex: 1; + min-width: 0; +} + .team-name { font-size: 30rpx; color: #1a1a1a; font-weight: 500; } +.leader-tag { + font-size: 20rpx; + color: #1a56db; + background: rgba(26, 86, 219, 0.1); + border: 1rpx solid rgba(26, 86, 219, 0.3); + border-radius: 8rpx; + padding: 2rpx 12rpx; + flex-shrink: 0; +} + .team-right { display: flex; align-items: center; @@ -146,3 +168,15 @@ color: #999; margin-top: 24rpx; } + +/* ===== 加载更多 ===== */ +.list-footer { + display: flex; + justify-content: center; + padding: 32rpx 0; +} + +.footer-text { + font-size: 26rpx; + color: #999; +} diff --git a/utils/auth.js b/utils/auth.js index 828b920..d7cb34e 100644 --- a/utils/auth.js +++ b/utils/auth.js @@ -259,6 +259,30 @@ async function fetchMyPoints({ }); } +async function fetchMyTeams({ pageNo = 1, pageSize = 20 } = {}) { + return request({ + path: API.myTeams, + method: 'GET', + needAuth: true, + data: { + PageNo: pageNo, + PageSize: pageSize + } + }); +} + +async function fetchMyOrders({ pageNo = 1, pageSize = 10 } = {}) { + return request({ + path: API.myOrders, + method: 'GET', + needAuth: true, + data: { + PageNo: pageNo, + PageSize: pageSize + } + }); +} + async function fetchMyCertificates({ pageNo = 1, pageSize = 10, type } = {}) { const data = { PageNo: pageNo, @@ -286,6 +310,8 @@ module.exports = { submitMyAuth, fetchMyRegistrations, fetchMyPoints, + fetchMyTeams, + fetchMyOrders, fetchMyCertificates, refreshAccessToken }; diff --git a/utils/http.js b/utils/http.js index ecbaffd..413f3f2 100644 --- a/utils/http.js +++ b/utils/http.js @@ -1,15 +1,27 @@ const { getBaseUrl } = require('./constants'); -function withAuthHeader(needAuth, header) { +// Token刷新状态和请求队列 +let isRefreshing = false; +let requestQueue = []; + +function withAuthHeader(needAuth, header, useRefreshTokenOnly = false) { const app = getApp(); if (!needAuth) return header; - const token = app && app.globalData ? app.globalData.accessToken : ''; + const accessToken = app && app.globalData ? app.globalData.accessToken : ''; const refreshToken = app && app.globalData ? app.globalData.refreshToken : ''; - if (typeof token === 'string' && token.length > 0) { - header = header || {}; - header.Authorization = `Bearer ${token}`; - header['x-access-token'] = `Bearer ${token}`; - header['x-refresh-token'] = `Bearer ${refreshToken}`; + + header = header || {}; + + if (useRefreshTokenOnly && refreshToken) { + // 只使用 refresh token 模式(用于刷新) + header['X-Authorization'] = `Bearer ${refreshToken}`; + } else if (accessToken) { + // 正常模式:使用 access token + header.Authorization = `Bearer ${accessToken}`; + // 同时带上 refresh token,供后端在需要时自动刷新 + if (refreshToken) { + header['X-Authorization'] = `Bearer ${refreshToken}`; + } } return header; } @@ -37,18 +49,68 @@ function getHeaderValue(headers, headerName) { function tryUpdateAccessTokenFromResponseHeaders(res) { // wx.request 的返回头在 res.header(部分环境也可能是 res.headers) - console.log( 'res', res); const headers = (res && res.header) || (res && res.headers) || null; - const newAccessToken = getHeaderValue(headers, 'x-access-token'); - if (!newAccessToken) return; + + // 从响应头获取新的 token + const newAccessToken = getHeaderValue(headers, 'access-token') || getHeaderValue(headers, 'x-access-token'); + const newRefreshToken = getHeaderValue(headers, 'x-access-token') || getHeaderValue(headers, 'x-refresh-token'); + + if (!newAccessToken && !newRefreshToken) return false; const app = getApp(); - if (app && typeof app.setAuth === 'function') { - app.setAuth({ accessToken: newAccessToken }); - } else { - wx.setStorageSync('accessToken', newAccessToken); - if (app && app.globalData) app.globalData.accessToken = newAccessToken; + const authData = {}; + let hasUpdate = false; + + if (newAccessToken && newAccessToken !== 'invalid_token') { + authData.accessToken = newAccessToken; + hasUpdate = true; } + if (newRefreshToken && newRefreshToken !== 'invalid_token') { + authData.refreshToken = newRefreshToken; + hasUpdate = true; + } + + if (hasUpdate) { + if (app && typeof app.setAuth === 'function') { + app.setAuth(authData); + } else { + if (authData.accessToken) { + wx.setStorageSync('accessToken', authData.accessToken); + if (app && app.globalData) app.globalData.accessToken = authData.accessToken; + } + if (authData.refreshToken) { + wx.setStorageSync('refreshToken', authData.refreshToken); + if (app && app.globalData) app.globalData.refreshToken = authData.refreshToken; + } + } + } + + // 如果正在刷新Token且获取到了新Token,执行队列中的请求 + if (isRefreshing && newAccessToken && newAccessToken !== 'invalid_token') { + isRefreshing = false; + requestQueue.forEach((callback) => callback(newAccessToken)); + requestQueue = []; + return true; + } + + return hasUpdate; +} + +function clearAllTokensAndRedirect() { + wx.clearStorageSync(); + const app = getApp(); + if (app && app.globalData) { + app.globalData.accessToken = ''; + app.globalData.refreshToken = ''; + } + wx.showModal({ + title: '登录失效', + content: '您的登录已失效,请重新登录。', + showCancel: false, + complete() { + wx.navigateTo({ url: '/subpackages/mine/login/login' }); + } + }); } function request({ @@ -58,14 +120,18 @@ function request({ needAuth = false, contentType = 'application/json', // Internal flag: prevent infinite retry when refresh token也失败 - __retry = false + __retry = false, + // Internal flag: 标记这是刷新token的重试请求 + __refreshRetry = false }) { const baseUrl = getBaseUrl(); const url = `${baseUrl}${path}`; + // 正常请求:同时带上 access token 和 refresh token + // 刷新重试请求:只使用 refresh token const header = withAuthHeader(needAuth, { 'content-type': contentType - }); + }, __refreshRetry); return new Promise((resolve, reject) => { wx.request({ @@ -75,31 +141,65 @@ function request({ header: header, success: async (res) => { // 如果后端在 Response Headers 下发新的 token,就替换本地 token - tryUpdateAccessTokenFromResponseHeaders(res); + const tokenUpdated = tryUpdateAccessTokenFromResponseHeaders(res); const statusCode = res && res.data && res.data.statusCode ? res.data.statusCode : 0; const payload = res && res.data !== undefined ? res.data : null; - // ── 401 未授权 ────────────────────────────────────────────── - if (statusCode === 401) { - wx.clearStorageSync(); - const app = getApp(); - if (app && app.globalData) { - app.globalData.accessToken = ''; - app.globalData.refreshToken = ''; + // ── 401 未授权:尝试刷新Token ── + if (statusCode === 401 && needAuth && !__retry) { + // 如果已经在刷新Token,将请求加入队列 + if (isRefreshing) { + requestQueue.push((newToken) => { + resolve(request({ path, method, data, needAuth, contentType, __retry: true })); + }); + return; } - wx.showModal({ - title: '登录失效', - content: '您的登录已失效,请重新登录。', - showCancel: false, - complete() { - wx.navigateTo({ url: '/subpackages/mine/login/login' }); + + // 开始刷新Token + // 重发原请求 + isRefreshing = true; + + try { + // 检查是否有 refresh token + const app = getApp(); + const refreshToken = app && app.globalData ? app.globalData.refreshToken : ''; + if (!refreshToken) { + throw new Error('No refresh token available'); } - }); + + // 重发请求 + await request({ + path, + method, + data, + needAuth, + contentType, + __retry: false, + __refreshRetry: true + }); + + // 刷新成功,现在重试原请求 + isRefreshing = false; + resolve(request({ path, method, data, needAuth, contentType, __retry: true })); + return; + } catch (refreshErr) { + // 刷新失败,清除Token并跳转登录 + isRefreshing = false; + requestQueue = []; + clearAllTokensAndRedirect(); + reject(new Error('Unauthorized')); + return; + } + } + + // ── 401 未授权(重试后仍然失败) ── + if (statusCode === 401) { + clearAllTokensAndRedirect(); reject(new Error('Unauthorized')); return; } - // ── 403 禁止访问 ───────────────────────────────────────────── + // ── 403 禁止访问 ── if (statusCode === 403) { wx.showToast({ title: '无访问权限', icon: 'none' }); reject(new Error('Forbidden')); @@ -141,27 +241,7 @@ function request({ resolve(payload); }, fail(err) { - if (err && err.statusCode === 401 && needAuth && !__retry) { - // 401: try refresh token, then retry the original request once. - const originalErr = err; - try { - const auth = require('./auth'); - if (auth && typeof auth.refreshAccessToken === 'function') { - auth - .refreshAccessToken() - .then(() => { - return request({ path, method, data, needAuth, contentType, __retry: true }); - }) - .then(resolve) - .catch(() => { - reject(originalErr); - }); - return; - } - } catch (e) { - // ignore and fall through to rejecting original error - } - } + // 网络请求失败处理 reject(err); } });