984 lines
31 KiB
JavaScript
Raw Normal View History

2026-03-30 18:08:27 +08:00
const { API } = require('../../utils/constants');
const { request } = require('../../utils/http');
const { formatDateTime } = require('../../utils/format');
const auth = require('../../utils/auth');
2026-03-30 18:08:27 +08:00
2026-04-04 18:57:50 +08:00
// 中文数字映射(项目一、二、三…)
const CN_NUM = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
/**
* 将 projects 数组转换为按项目分组的结构
* 每个项目下展开所有 groups,附带 selected 状态
*/
function getEventStatus(detail) {
if (!detail || typeof detail !== 'object') return null;
const v = detail.eventStatus != null ? detail.eventStatus : detail.EventStatus;
if (v == null || v === '') return null;
const n = Number(v);
return Number.isNaN(n) ? null : n;
}
/** 仅 1=报名中 可进入报名页;与后端 EventStatus 枚举一致 */
function toastForBlockedEventStatus(es) {
if (es === 0) return '赛事未开始';
if (es === 2) return '报名已结束';
if (es === 3) return '比赛进行中,暂不可报名';
if (es === 4) return '赛事已结束';
return '暂不可报名';
}
2026-04-04 18:57:50 +08:00
function buildProjectGroups(projects) {
return projects.map((p, idx) => {
const groups = Array.isArray(p.groups) ? p.groups : [];
return {
projectId: p.id || p.Id || idx,
projectName: p.name || p.Name || '-',
index: CN_NUM[idx] || String(idx + 1),
fee: p.fee != null ? p.fee : 0,
groups: groups.map((g) => ({
groupId: g.groupId || g.GroupId || g.id,
groupName: g.groupName || g.GroupName || '-',
ageRange: g.ageRange || g.AgeRange || '',
gender: g.gender || g.Gender || '',
fee: g.fee != null ? g.fee : (p.fee != null ? p.fee : 0),
selected: false,
ageRequirement: g.ageRequirement || g.AgeRequirement || '',
genderRequirement: g.genderRequirement != null ? g.genderRequirement : g.GenderRequirement != null ? g.GenderRequirement : '',
genderRequirementText: genderRequirementText(g.genderRequirement != null ? g.genderRequirement : g.GenderRequirement)
2026-04-04 18:57:50 +08:00
}))
};
});
}
/** 计算已选组别的总价 */
function calcTotalPrice(projectGroups) {
let total = 0;
projectGroups.forEach((p) => {
p.groups.forEach((g) => {
if (g.selected) total += Number(g.fee) || 0;
});
});
return total;
}
function toNonNegInt(v) {
const n = Number(v);
if (Number.isNaN(n)) return null;
const i = Math.floor(n);
return i >= 0 ? i : null;
}
function genderRequirementText(v) {
if (v === undefined || v === null || v === '') return '不限';
const s = String(v).trim();
if (s === '0' || s === '男' || s.toUpperCase() === 'M') return '男';
if (s === '1' || s === '女' || s.toUpperCase() === 'F') return '女';
if (s === '2' || s === '不限' || s === '全部') return '不限';
return s;
}
function normalizeGenderToNum(v) {
if (v === undefined || v === null || v === '') return null;
const s = String(v).trim();
if (s === '0' || s === '男' || s.toUpperCase() === 'M') return 0;
if (s === '1' || s === '女' || s.toUpperCase() === 'F') return 1;
if (s === '2' || s === '不限' || s === '全部') return 2;
const n = Number(s);
if (!Number.isNaN(n) && (n === 0 || n === 1 || n === 2)) return n;
return null;
}
function parseYmdDate(s) {
if (!s) return null;
const str = String(s).trim();
const m = str.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!m) return null;
const y = Number(m[1]);
const mo = Number(m[2]);
const d = Number(m[3]);
if ([y, mo, d].some((x) => Number.isNaN(x))) return null;
const dt = new Date(y, mo - 1, d);
return Number.isNaN(dt.getTime()) ? null : dt;
}
2026-04-07 17:54:54 +08:00
function parseYmdDateAny(text) {
if (!text) return null;
const s = String(text);
const m = s.match(/(\d{4})-(\d{2})-(\d{2})/);
if (!m) return null;
const y = Number(m[1]);
const mo = Number(m[2]);
const d = Number(m[3]);
if ([y, mo, d].some((x) => Number.isNaN(x))) return null;
const dt = new Date(y, mo - 1, d);
return Number.isNaN(dt.getTime()) ? null : dt;
}
function calcAgeYears(birthDateStr, atDateStr) {
const b = parseYmdDate(birthDateStr);
if (!b) return null;
const at = parseYmdDate(atDateStr) || new Date();
let age = at.getFullYear() - b.getFullYear();
const m = at.getMonth() - b.getMonth();
if (m < 0 || (m === 0 && at.getDate() < b.getDate())) age -= 1;
return age >= 0 ? age : null;
}
2026-04-07 17:54:54 +08:00
function parseBirthDateRangeText(text) {
// e.g. "2021-03-31 至 2026-04-29" / "2021-03-31~2026-04-29"
if (!text) return null;
const s = String(text);
const ms = s.match(/(\d{4}-\d{2}-\d{2})/g);
if (!ms || ms.length < 2) return null;
const a = parseYmdDate(ms[0]);
const b = parseYmdDate(ms[1]);
if (!a || !b) return null;
const start = a <= b ? a : b;
const end = a <= b ? b : a;
return { start, end, startText: ms[0], endText: ms[1] };
}
function parseAgeYearsRangeText(text) {
// 兼容旧格式:如 "6-12岁" / "6~12"
if (!text) return null;
const s = String(text);
const nums = s.match(/\d+/g);
if (!nums || nums.length === 0) return null;
const a = Number(nums[0]);
if (Number.isNaN(a)) return null;
if (nums.length === 1) return { min: a, max: a };
const b = Number(nums[1]);
if (Number.isNaN(b)) return { min: a, max: a };
return { min: Math.min(a, b), max: Math.max(a, b) };
}
function isIndividualProject(p) {
const t = (p && (p.typeName || p.TypeName)) || '';
const s = String(t);
// 只显示个人项目:个人速度、个人花样等
return s.includes('个人');
}
function projectTypeOf(rawProject) {
const t = (rawProject && (rawProject.typeName || rawProject.TypeName)) || '';
const s = String(t);
if (s.includes('团队')) return 'team';
if (s.includes('个人')) return 'individual';
return 'unknown';
}
function countSelectedByType(projectGroups, rawProjects) {
const map = new Map();
(rawProjects || []).forEach((p) => {
const pid = Number(p.id != null ? p.id : p.Id);
if (!Number.isNaN(pid)) map.set(pid, projectTypeOf(p));
});
let individual = 0;
let team = 0;
let unknown = 0;
projectGroups.forEach((pg) => {
const pid = Number(pg.projectId);
const type = map.get(pid) || 'unknown';
pg.groups.forEach((g) => {
if (!g.selected) return;
if (type === 'individual') individual += 1;
else if (type === 'team') team += 1;
else unknown += 1;
});
});
return { individual, team, unknown };
}
2026-04-07 17:54:54 +08:00
function buildCalculateAmountItems(projectGroups) {
const map = new Map(); // projectId -> personCount
projectGroups.forEach((p) => {
const pid = Number(p.projectId);
if (Number.isNaN(pid)) return;
let cnt = 0;
p.groups.forEach((g) => {
if (g.selected) cnt += 1;
});
if (cnt > 0) map.set(pid, (map.get(pid) || 0) + cnt);
});
return Array.from(map.entries()).map(([projectId, personCount]) => ({
projectId,
personCount
}));
}
2026-03-30 18:08:27 +08:00
Page({
data: {
eventId: null,
loggedIn: false,
eventDetail: null,
projects: [],
2026-04-04 18:57:50 +08:00
projectGroups: [], // 分组后的展示数据
couponCode: '',
totalPrice: 0,
2026-04-13 18:15:37 +08:00
originalPrice: 0,
discountPrice: 0,
usedCouponId: null,
submitting: false,
openidLoading: false,
myAuth: null
2026-03-30 18:08:27 +08:00
},
2026-04-07 17:54:54 +08:00
_calcTimer: null,
_calcSeq: 0,
2026-03-30 18:08:27 +08:00
onLoad(options) {
const eventId = Number(options.eventId);
this.setData({ eventId });
this.fetchDetail(eventId);
},
onShow() {
const accessToken = wx.getStorageSync('accessToken') || '';
const loggedIn = Boolean(accessToken);
this.setData({ loggedIn });
if (loggedIn) this._ensureMyAuth();
2026-03-30 18:08:27 +08:00
},
2026-04-04 18:57:50 +08:00
onBack() {
if (getCurrentPages().length > 1) {
wx.navigateBack();
} else {
wx.switchTab({ url: '/pages/home/home' });
}
},
2026-03-30 18:08:27 +08:00
fetchDetail(eventId) {
if (!eventId) return;
request({
path: API.eventDetail(eventId),
method: 'GET',
needAuth: false
})
.then((detail) => {
const es = getEventStatus(detail);
2026-04-14 16:17:38 +08:00
console.log('es', es);
if (es !== null && es !== 1) {
wx.showToast({ title: toastForBlockedEventStatus(es), icon: 'none' });
setTimeout(() => {
if (getCurrentPages().length > 1) {
wx.navigateBack();
} else {
wx.switchTab({ url: '/pages/home/home' });
}
}, 1600);
return;
}
const rawProjects = Array.isArray(detail.projects) ? detail.projects : [];
const projects = rawProjects.filter(isIndividualProject);
2026-04-04 18:57:50 +08:00
const projectGroups = buildProjectGroups(projects);
2026-03-30 18:08:27 +08:00
this.setData({
eventDetail: detail,
projects,
2026-04-04 18:57:50 +08:00
projectGroups,
totalPrice: 0
2026-03-30 18:08:27 +08:00
});
})
.catch(() => {
wx.showToast({ title: '加载失败', icon: 'none' });
});
},
_ensureMyAuth() {
// 认证信息用于年龄/性别限制校验;没有认证则引导去个人中心
return auth
.fetchMyAuth()
.then((res) => {
const d = res && typeof res === 'object' ? res : null;
const birthDate = d ? (d.birthDate || d.birthday || '') : '';
2026-04-14 16:17:38 +08:00
console.log('birthDate', birthDate);
if (!birthDate) {
wx.showModal({
title: '需先完成认证',
2026-04-14 16:17:38 +08:00
content: '报名个人项目需要先完成身份认证。',
success(r) {
if (r.confirm) wx.switchTab({ url: '/pages/mine/mine' });
}
2026-04-14 16:17:38 +08:00
})
this.setData({ myAuth: null });
return null;
}
this.setData({ myAuth: d });
return d;
})
.catch(() => {
wx.showModal({
title: '获取认证信息失败',
content: '请稍后重试,或前往个人中心完成认证。',
cancelText: '取消',
success: (r) => {
if (r.confirm) wx.switchTab({ url: '/pages/mine/mine' });
}
});
this.setData({ myAuth: null });
return null;
});
},
2026-03-30 18:08:27 +08:00
onGoMineLogin() {
wx.switchTab({ url: '/pages/mine/mine' });
},
_showOpenidResult(openid) {
wx.showModal({
title: 'openid',
content: String(openid),
confirmText: '复制',
success: (r) => {
if (r.confirm) {
wx.setClipboardData({ data: String(openid) });
}
}
});
},
/** 优先读本地登录缓存;没有则 wx.login + 后端换 openid */
onFetchOpenId() {
if (this.data.openidLoading) return;
const app = getApp();
const cached =
(app && app.globalData && app.globalData.openid) || wx.getStorageSync('openid') || '';
if (cached) {
this._showOpenidResult(cached);
return;
}
this.setData({ openidLoading: true });
wx.showLoading({ title: '获取中', mask: true });
wx.login({
success: (loginRes) => {
const code = loginRes.code;
if (!code) {
wx.hideLoading();
this.setData({ openidLoading: false });
wx.showToast({ title: '未获取到 code', icon: 'none' });
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 });
}
wx.hideLoading();
this.setData({ openidLoading: false });
if (openid) {
this._showOpenidResult(openid);
} else {
wx.showToast({ title: '接口未返回 openid', icon: 'none' });
}
})
.catch(() => {
wx.hideLoading();
this.setData({ openidLoading: false });
wx.showModal({
title: '换取 openid 失败',
content: `请确认后端接口路径:${API.wechatOpenidByJsCode}\n\n可临时复制 code 给服务端调试:\n${code}`,
2026-04-14 16:17:38 +08:00
confirmText: '复制',
cancelText: '关闭',
success: (r) => {
if (r.confirm) wx.setClipboardData({ data: code });
}
});
});
},
fail: () => {
wx.hideLoading();
this.setData({ openidLoading: false });
wx.showToast({ title: 'wx.login 失败', icon: 'none' });
}
});
},
2026-04-04 18:57:50 +08:00
/** 点击组别标签切换选中状态 */
onGroupTap(e) {
const { projectId, groupId } = e.currentTarget.dataset;
if (!this.data.myAuth) {
2026-04-14 16:17:38 +08:00
console.log('myAuth', this.data.myAuth);
wx.showModal({
title: '需先完成认证',
content: '报名个人项目需要先完成身份认证。',
cancelText: '取消',
success: (r) => {
if (r.confirm) wx.switchTab({ url: '/pages/mine/mine' });
}
});
return;
}
// 年龄/性别限制校验
const my = this.data.myAuth || {};
const birthDate = my.birthDate || my.birthday || '';
const myGender = normalizeGenderToNum(my.gender != null ? my.gender : my.sex);
const eventStart = (this.data.eventDetail && this.data.eventDetail.matchStartTime) || '';
const clickedProject = this.data.projectGroups.find((p) => p.projectId === projectId);
const clickedGroup = clickedProject ? clickedProject.groups.find((g) => g.groupId === groupId) : null;
if (clickedGroup) {
const reqGender = normalizeGenderToNum(clickedGroup.genderRequirement);
if (reqGender != null && reqGender !== 2 && myGender != null && reqGender !== myGender) {
wx.showToast({ title: `该组别仅限${genderRequirementText(reqGender)}报名`, icon: 'none' });
return;
}
2026-04-07 17:54:54 +08:00
// 年龄限制:优先支持“出生日期范围”,兼容旧“岁数范围”
const birthRange = parseBirthDateRangeText(clickedGroup.ageRequirement);
if (birthRange) {
const bd = parseYmdDateAny(birthDate);
if (!bd) {
wx.showToast({ title: '无法解析出生日期,请先完善认证信息', icon: 'none' });
return;
}
2026-04-07 17:54:54 +08:00
if (bd < birthRange.start || bd > birthRange.end) {
wx.showToast({
title: `出生日期需在${birthRange.startText}至${birthRange.endText}`,
icon: 'none'
});
return;
}
2026-04-07 17:54:54 +08:00
} else {
const range = parseAgeYearsRangeText(clickedGroup.ageRequirement);
if (range) {
const age = calcAgeYears(birthDate, eventStart);
if (age == null) {
wx.showToast({ title: '无法解析出生日期,请先完善认证信息', icon: 'none' });
return;
}
if (age < range.min || age > range.max) {
wx.showToast({ title: `年龄需在${range.min}-${range.max}岁`, icon: 'none' });
return;
}
}
}
}
2026-04-15 13:23:23 +08:00
// 同一项目只能选一个组别:点选时取消该项目下其他组别;再次点击已选项则取消
const nextProjectGroups = this.data.projectGroups.map((p) => {
2026-04-04 18:57:50 +08:00
if (p.projectId !== projectId) return p;
return {
...p,
groups: p.groups.map((g) => {
2026-04-15 13:23:23 +08:00
if (g.groupId !== groupId) return { ...g, selected: false };
2026-04-04 18:57:50 +08:00
return { ...g, selected: !g.selected };
})
};
});
const detail = this.data.eventDetail || {};
const individualLimit = toNonNegInt(detail.individualProjectLimit);
const teamLimit = toNonNegInt(detail.teamProjectLimit);
const { individual, team } = countSelectedByType(nextProjectGroups, this.data.projects);
if (individualLimit != null && individual > individualLimit) {
wx.showToast({ title: `个人项目最多选择${individualLimit}个`, icon: 'none' });
return;
}
if (teamLimit != null && team > teamLimit) {
wx.showToast({ title: `团队项目最多选择${teamLimit}个`, icon: 'none' });
return;
}
2026-04-07 17:54:54 +08:00
this.setData({ projectGroups: nextProjectGroups }, () => {
this._scheduleRecalcAmount();
});
},
_scheduleRecalcAmount() {
if (this._calcTimer) {
clearTimeout(this._calcTimer);
this._calcTimer = null;
}
this._calcTimer = setTimeout(() => {
this._calcTimer = null;
this._recalcAmountNow();
}, 250);
},
_recalcAmountNow() {
const items = buildCalculateAmountItems(this.data.projectGroups);
if (!items.length) {
2026-04-14 16:17:38 +08:00
this.setData({
2026-04-13 18:15:37 +08:00
totalPrice: 0,
originalPrice: 0,
discountPrice: 0,
usedCouponId: null
});
2026-04-07 17:54:54 +08:00
return;
}
const seq = (this._calcSeq += 1);
request({
path: API.calculateAmount,
method: 'POST',
needAuth: true,
data: {
eventId: Number(this.data.eventId),
2026-04-13 18:15:37 +08:00
registrationMode: 1,
2026-04-07 17:54:54 +08:00
items
},
contentType: 'application/json'
})
.then((res) => {
if (seq !== this._calcSeq) return;
2026-04-14 16:17:38 +08:00
2026-04-13 18:15:37 +08:00
let totalAmount = null;
let originalPrice = 0;
let discountPrice = 0;
2026-04-14 16:17:38 +08:00
2026-04-13 18:15:37 +08:00
if (typeof res === 'number') {
totalAmount = res;
originalPrice = res;
} else if (typeof res === 'string') {
2026-04-07 17:54:54 +08:00
const n = Number(res);
2026-04-13 18:15:37 +08:00
totalAmount = Number.isNaN(n) ? null : n;
originalPrice = totalAmount || 0;
2026-04-07 17:54:54 +08:00
} else if (res && typeof res === 'object') {
2026-04-13 18:15:37 +08:00
// 从统一的 CalculateAmountOutput 中获取数据
2026-04-14 16:17:38 +08:00
totalAmount = res.totalAmount != null ? Number(res.totalAmount) :
(res.amount != null ? Number(res.amount) :
(res.payAmount != null ? Number(res.payAmount) : null));
2026-04-13 18:15:37 +08:00
originalPrice = res.originalAmount != null ? Number(res.originalAmount) : totalAmount || 0;
discountPrice = res.discountAmount != null ? Number(res.discountAmount) : 0;
2026-04-07 17:54:54 +08:00
}
2026-04-14 16:17:38 +08:00
2026-04-13 18:15:37 +08:00
if (totalAmount == null) {
2026-04-07 17:54:54 +08:00
// fallback:仍然展示前端计算的费用,避免空白
2026-04-13 18:15:37 +08:00
const fallbackPrice = calcTotalPrice(this.data.projectGroups);
2026-04-14 16:17:38 +08:00
this.setData({
2026-04-13 18:15:37 +08:00
totalPrice: fallbackPrice,
originalPrice: fallbackPrice,
discountPrice: 0
});
2026-04-07 17:54:54 +08:00
} else {
2026-04-14 16:17:38 +08:00
this.setData({
2026-04-13 18:15:37 +08:00
totalPrice: totalAmount,
originalPrice: originalPrice || totalAmount,
discountPrice: discountPrice || 0
});
2026-04-07 17:54:54 +08:00
}
})
.catch(() => {
if (seq !== this._calcSeq) return;
2026-04-13 18:15:37 +08:00
const fallbackPrice = calcTotalPrice(this.data.projectGroups);
2026-04-14 16:17:38 +08:00
this.setData({
2026-04-13 18:15:37 +08:00
totalPrice: fallbackPrice,
originalPrice: fallbackPrice,
discountPrice: 0
});
2026-04-07 17:54:54 +08:00
});
2026-03-30 18:08:27 +08:00
},
2026-04-04 18:57:50 +08:00
onCouponInput(e) {
this.setData({ couponCode: (e.detail.value || '').trim() });
},
2026-04-13 18:15:37 +08:00
/**
* 验证并应用优惠券
*/
async onValidateCoupon() {
const couponCode = this.data.couponCode;
if (!couponCode) {
wx.showToast({ title: '请输入优惠券兑换码', icon: 'none' });
return;
}
wx.showLoading({ title: '验证中', mask: true });
try {
const res = await request({
path: API.couponValidate,
method: 'POST',
needAuth: true,
data: {
eventId: Number(this.data.eventId),
couponCode: couponCode,
originalAmount: this.data.originalPrice || this.data.totalPrice
},
contentType: 'application/json'
});
if (res && res.isValid) {
this.setData({
usedCouponId: res.couponId,
totalPrice: res.finalAmount,
discountPrice: res.discountAmount || 0,
originalPrice: res.originalAmount || this.data.originalPrice
});
wx.showToast({ title: '优惠券应用成功', icon: 'success' });
} else {
wx.showToast({ title: res.invalidReason || '优惠券无效', icon: 'none' });
}
} catch (error) {
2026-04-14 16:17:38 +08:00
wx.showToast({
title: error && error.message ? error.message : '验证失败,请稍后重试',
icon: 'none'
2026-04-13 18:15:37 +08:00
});
2026-05-18 09:21:50 +08:00
}
2026-04-13 18:15:37 +08:00
},
2026-04-04 18:57:50 +08:00
/** 收集已选的 projectId + groupId */
_getSelectedItems() {
const items = [];
this.data.projectGroups.forEach((p) => {
p.groups.forEach((g) => {
if (g.selected) {
items.push({ projectId: Number(p.projectId), groupId: Number(g.groupId) });
}
});
2026-03-30 18:08:27 +08:00
});
2026-04-04 18:57:50 +08:00
return items;
2026-03-30 18:08:27 +08:00
},
2026-04-04 18:57:50 +08:00
_validateAndGetPayload(paymentMethod) {
2026-03-30 18:08:27 +08:00
if (!this.data.loggedIn) {
wx.showToast({ title: '请先登录', icon: 'none' });
2026-04-04 18:57:50 +08:00
return null;
2026-03-30 18:08:27 +08:00
}
2026-04-04 18:57:50 +08:00
const items = this._getSelectedItems();
if (items.length === 0) {
wx.showToast({ title: '请至少选择一个组别', icon: 'none' });
return null;
2026-03-30 18:08:27 +08:00
}
const detail = this.data.eventDetail || {};
const individualLimit = toNonNegInt(detail.individualProjectLimit);
const teamLimit = toNonNegInt(detail.teamProjectLimit);
const { individual, team, unknown } = countSelectedByType(this.data.projectGroups, this.data.projects);
if (unknown > 0 && (individualLimit != null || teamLimit != null)) {
// typeName 识别不出来时,不阻断提交,但提醒后端应补齐 typeName
console.warn('存在未识别项目类型的选项,无法严格执行个人/团队限报', { unknown });
}
if (individualLimit != null && individual > individualLimit) {
wx.showToast({ title: `个人项目最多选择${individualLimit}个`, icon: 'none' });
return null;
}
if (teamLimit != null && team > teamLimit) {
wx.showToast({ title: `团队项目最多选择${teamLimit}个`, icon: 'none' });
return null;
}
2026-04-04 18:57:50 +08:00
return {
eventId: Number(this.data.eventId),
registrationMode: 1,
paymentMethod, // 1=线下 2=线上
couponCode: this.data.couponCode || null,
items
2026-03-30 18:08:27 +08:00
};
2026-04-04 18:57:50 +08:00
},
/** 线下支付 */
onPayOffline() {
const payload = this._validateAndGetPayload(1);
if (!payload) return;
this._submitApply(payload);
},
2026-03-30 18:08:27 +08:00
2026-04-04 18:57:50 +08:00
/** 线上支付 */
2026-04-07 23:50:41 +08:00
async onPayOnline() {
2026-04-04 18:57:50 +08:00
const payload = this._validateAndGetPayload(2);
if (!payload) return;
2026-04-07 23:50:41 +08:00
2026-05-11 16:59:15 +08:00
// 判断是否为免费赛事
const isFree = this.data.totalPrice <= 0;
if (isFree) {
// 免费赛事:直接显示确认弹窗
wx.showModal({
title: '确认报名',
content: '确定要报名参加本次赛事吗?',
success: async (res) => {
if (res.confirm) {
try {
this.setData({ submitting: true });
// 免费赛事直接提交报名
await this._submitApply(payload);
} catch (error) {
console.error('免费报名异常:', error);
wx.showToast({
title: error && error.message ? error.message : '报名失败,请稍后重试',
icon: 'none'
});
} finally {
this.setData({ submitting: false });
}
}
}
});
return;
}
// 收费赛事:走原有支付流程
let registrationId = null;
2026-04-07 23:50:41 +08:00
try {
// 1. 获取 openid
const openid = await this._getOpenId();
if (!openid) {
wx.showToast({ title: '获取 openid 失败', icon: 'none' });
return;
}
// 2. 提交报名(创建预报名记录)
2026-04-07 23:50:41 +08:00
this.setData({ submitting: true });
registrationId = await this._submitApplyForPayment(payload);
2026-04-14 16:17:38 +08:00
2026-04-07 23:50:41 +08:00
// 3. 选择支付方式并获取支付参数
await this._selectPaymentMethod(registrationId, openid, 2);
2026-04-14 16:17:38 +08:00
2026-04-07 23:50:41 +08:00
// 4. 获取支付信息
const paymentInfo = await this._getPaymentInfo(registrationId, openid);
2026-05-09 13:29:01 +08:00
console.log('获取的支付信息:', paymentInfo);
2026-04-14 16:17:38 +08:00
2026-05-11 16:59:15 +08:00
// 5. 检查是否为免费(后端可能有优惠导致免费)
if (paymentInfo.isFree || (paymentInfo.actualAmount != null && paymentInfo.actualAmount <= 0)) {
// 免费:直接提示成功
wx.showToast({ title: '报名成功', icon: 'success' });
setTimeout(() => {
wx.switchTab({ url: '/pages/home/home' });
}, 1500);
return;
}
// 6. 检查支付参数是否存在
2026-05-09 13:29:01 +08:00
const wechatPayParams = paymentInfo.wechatPayParams || paymentInfo.WechatPayParams;
if (!wechatPayParams || !wechatPayParams.paySign) {
console.error('支付参数异常:', wechatPayParams);
throw new Error('获取支付参数失败,请稍后重试');
}
2026-05-11 16:59:15 +08:00
// 7. 调起微信支付
2026-05-09 13:29:01 +08:00
await this._invokeWeChatPay(wechatPayParams, registrationId);
2026-04-14 16:17:38 +08:00
2026-04-07 23:50:41 +08:00
} catch (error) {
console.error('支付流程异常:', error);
// 如果已经创建了预报名记录,尝试取消
if (registrationId) {
try {
await this._cancelPreRegistration(registrationId);
} catch (cancelErr) {
console.error('取消预报名失败:', cancelErr);
}
}
2026-04-14 16:17:38 +08:00
wx.showToast({
title: error && error.message ? error.message : '支付失败,请稍后重试',
icon: 'none'
2026-04-07 23:50:41 +08:00
});
} 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;
}
2026-04-14 16:17:38 +08:00
2026-04-07 23:50:41 +08:00
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'
});
},
/** 获取支付信息 */
2026-04-13 18:15:37 +08:00
_getPaymentInfo(registrationId, openid) {
2026-04-07 23:50:41 +08:00
return request({
path: '/api/eventclient/payment-info',
method: 'POST',
needAuth: true,
2026-04-14 16:17:38 +08:00
data: {
registrationId,
2026-04-13 18:15:37 +08:00
openId: openid,
couponId: this.data.usedCouponId
},
2026-04-07 23:50:41 +08:00
contentType: 'application/json'
});
},
/** 调起微信支付 */
_invokeWeChatPay(wechatPayParams, registrationId) {
return new Promise((resolve, reject) => {
2026-05-09 13:29:01 +08:00
// 确保所有支付参数都是字符串类型
const params = {
appId: String(wechatPayParams.appId || wechatPayParams.AppId || ''),
timeStamp: String(wechatPayParams.timeStamp || wechatPayParams.TimeStamp || ''),
nonceStr: String(wechatPayParams.nonceStr || wechatPayParams.NonceStr || ''),
package: String(wechatPayParams.package || ''),
signType: String(wechatPayParams.signType || wechatPayParams.SignType || 'MD5'),
paySign: String(wechatPayParams.paySign || wechatPayParams.PaySign || '')
};
console.log('准备调用微信支付,参数:', params);
2026-04-07 23:50:41 +08:00
wx.requestPayment({
2026-05-09 13:29:01 +08:00
...params,
2026-04-07 23:50:41 +08:00
success: async () => {
try {
// 支付成功,同步支付状态
await request({
path: API.syncPayment(registrationId),
2026-04-07 23:50:41 +08:00
method: 'POST',
needAuth: true,
data: {},
contentType: 'application/json'
});
2026-04-14 16:17:38 +08:00
wx.showToast({ title: '支付成功,报名完成', icon: 'success' });
2026-04-07 23:50:41 +08:00
setTimeout(() => {
2026-04-23 00:25:43 +08:00
wx.switchTab({ url: '/pages/home/home' });
2026-04-07 23:50:41 +08:00
}, 1500);
resolve();
} catch (err) {
console.error('同步支付状态失败:', err);
wx.showModal({
title: '提示',
content: '支付成功,但同步状态失败,请稍后在订单列表中查看',
showCancel: false,
success: () => {
wx.switchTab({ url: '/pages/mine/mine' });
}
});
resolve();
}
},
fail: async (err) => {
2026-04-07 23:50:41 +08:00
console.error('微信支付失败:', err);
// 支付失败,尝试取消预报名记录
try {
await this._cancelPreRegistration(registrationId);
} catch (cancelErr) {
console.error('取消预报名失败:', cancelErr);
}
2026-04-07 23:50:41 +08:00
if (err.errMsg && err.errMsg.includes('cancel')) {
wx.showToast({ title: '支付已取消', icon: 'none' });
} else {
2026-04-14 16:17:38 +08:00
wx.showToast({
title: err.errMsg || '支付失败,请稍后重试',
icon: 'none'
2026-04-07 23:50:41 +08:00
});
}
reject(err);
}
});
});
2026-04-04 18:57:50 +08:00
},
/** 取消预报名记录 */
async _cancelPreRegistration(registrationId) {
try {
await request({
path: API.cancelRegistration(registrationId),
method: 'POST',
needAuth: true,
data: {},
contentType: 'application/json'
});
console.log('预报名记录已取消:', registrationId);
} catch (err) {
console.error('取消预报名失败:', err);
// 取消预报名失败不影响主要流程,仅记录日志
}
},
2026-04-04 18:57:50 +08:00
_submitApply(payload) {
2026-03-30 18:08:27 +08:00
this.setData({ submitting: true });
request({
path: API.apply,
method: 'POST',
needAuth: true,
data: payload,
contentType: 'application/json'
})
.then(() => {
2026-04-04 18:57:50 +08:00
wx.showToast({ title: '报名成功', icon: 'success' });
setTimeout(() => wx.switchTab({ url: '/pages/mine/mine' }), 800);
2026-03-30 18:08:27 +08:00
})
.catch((err) => {
wx.showToast({ title: err && err.message ? err.message : '提交失败', icon: 'none' });
})
.finally(() => this.setData({ submitting: false }));
}
});
2026-04-04 18:57:50 +08:00