882 lines
27 KiB
JavaScript
882 lines
27 KiB
JavaScript
const { API } = require('../../utils/constants');
|
||
const { request } = require('../../utils/http');
|
||
const { formatDateTime } = require('../../utils/format');
|
||
const auth = require('../../utils/auth');
|
||
|
||
// 中文数字映射(项目一、二、三…)
|
||
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 '暂不可报名';
|
||
}
|
||
|
||
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)
|
||
}))
|
||
};
|
||
});
|
||
}
|
||
|
||
/** 计算已选组别的总价 */
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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 };
|
||
}
|
||
|
||
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
|
||
}));
|
||
}
|
||
|
||
Page({
|
||
data: {
|
||
eventId: null,
|
||
loggedIn: false,
|
||
eventDetail: null,
|
||
projects: [],
|
||
projectGroups: [], // 分组后的展示数据
|
||
couponCode: '',
|
||
totalPrice: 0,
|
||
originalPrice: 0,
|
||
discountPrice: 0,
|
||
usedCouponId: null,
|
||
submitting: false,
|
||
openidLoading: false,
|
||
myAuth: null
|
||
},
|
||
|
||
_calcTimer: null,
|
||
_calcSeq: 0,
|
||
|
||
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();
|
||
},
|
||
|
||
onBack() {
|
||
if (getCurrentPages().length > 1) {
|
||
wx.navigateBack();
|
||
} else {
|
||
wx.switchTab({ url: '/pages/home/home' });
|
||
}
|
||
},
|
||
|
||
fetchDetail(eventId) {
|
||
if (!eventId) return;
|
||
request({
|
||
path: API.eventDetail(eventId),
|
||
method: 'GET',
|
||
needAuth: false
|
||
})
|
||
.then((detail) => {
|
||
const es = getEventStatus(detail);
|
||
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);
|
||
const projectGroups = buildProjectGroups(projects);
|
||
this.setData({
|
||
eventDetail: detail,
|
||
projects,
|
||
projectGroups,
|
||
totalPrice: 0
|
||
});
|
||
})
|
||
.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 || '') : '';
|
||
if (!birthDate) {
|
||
wx.showModal({
|
||
title: '需先完成认证',
|
||
content: '报名个人项目需要先完成身份认证(用于校验年龄与性别限制)。',
|
||
confirmText: '去个人中心',
|
||
cancelText: '取消',
|
||
success: (r) => {
|
||
if (r.confirm) wx.switchTab({ url: '/pages/mine/mine' });
|
||
}
|
||
});
|
||
this.setData({ myAuth: null });
|
||
return null;
|
||
}
|
||
this.setData({ myAuth: d });
|
||
return d;
|
||
})
|
||
.catch(() => {
|
||
wx.showModal({
|
||
title: '获取认证信息失败',
|
||
content: '请稍后重试,或前往个人中心完成认证。',
|
||
confirmText: '去个人中心',
|
||
cancelText: '取消',
|
||
success: (r) => {
|
||
if (r.confirm) wx.switchTab({ url: '/pages/mine/mine' });
|
||
}
|
||
});
|
||
this.setData({ myAuth: null });
|
||
return null;
|
||
});
|
||
},
|
||
|
||
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}`,
|
||
confirmText: '复制 code',
|
||
cancelText: '关闭',
|
||
success: (r) => {
|
||
if (r.confirm) wx.setClipboardData({ data: code });
|
||
}
|
||
});
|
||
});
|
||
},
|
||
fail: () => {
|
||
wx.hideLoading();
|
||
this.setData({ openidLoading: false });
|
||
wx.showToast({ title: 'wx.login 失败', icon: 'none' });
|
||
}
|
||
});
|
||
},
|
||
|
||
/** 点击组别标签切换选中状态 */
|
||
onGroupTap(e) {
|
||
const { projectId, groupId } = e.currentTarget.dataset;
|
||
if (!this.data.myAuth) {
|
||
wx.showToast({ title: '请先完成身份认证', icon: 'none' });
|
||
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;
|
||
}
|
||
// 年龄限制:优先支持“出生日期范围”,兼容旧“岁数范围”
|
||
const birthRange = parseBirthDateRangeText(clickedGroup.ageRequirement);
|
||
if (birthRange) {
|
||
const bd = parseYmdDateAny(birthDate);
|
||
if (!bd) {
|
||
wx.showToast({ title: '无法解析出生日期,请先完善认证信息', icon: 'none' });
|
||
return;
|
||
}
|
||
if (bd < birthRange.start || bd > birthRange.end) {
|
||
wx.showToast({
|
||
title: `出生日期需在${birthRange.startText}至${birthRange.endText}`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
} 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const nextProjectGroups = this.data.projectGroups.map((p) => {
|
||
if (p.projectId !== projectId) return p;
|
||
return {
|
||
...p,
|
||
groups: p.groups.map((g) => {
|
||
if (g.groupId !== groupId) return g;
|
||
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;
|
||
}
|
||
|
||
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) {
|
||
this.setData({
|
||
totalPrice: 0,
|
||
originalPrice: 0,
|
||
discountPrice: 0,
|
||
usedCouponId: null
|
||
});
|
||
return;
|
||
}
|
||
|
||
const seq = (this._calcSeq += 1);
|
||
request({
|
||
path: API.calculateAmount,
|
||
method: 'POST',
|
||
needAuth: true,
|
||
data: {
|
||
eventId: Number(this.data.eventId),
|
||
registrationMode: 1,
|
||
items
|
||
},
|
||
contentType: 'application/json'
|
||
})
|
||
.then((res) => {
|
||
if (seq !== this._calcSeq) return;
|
||
|
||
let totalAmount = null;
|
||
let originalPrice = 0;
|
||
let discountPrice = 0;
|
||
|
||
if (typeof res === 'number') {
|
||
totalAmount = res;
|
||
originalPrice = res;
|
||
} else if (typeof res === 'string') {
|
||
const n = Number(res);
|
||
totalAmount = Number.isNaN(n) ? null : n;
|
||
originalPrice = totalAmount || 0;
|
||
} else if (res && typeof res === 'object') {
|
||
// 从统一的 CalculateAmountOutput 中获取数据
|
||
totalAmount = res.totalAmount != null ? Number(res.totalAmount) :
|
||
(res.amount != null ? Number(res.amount) :
|
||
(res.payAmount != null ? Number(res.payAmount) : null));
|
||
originalPrice = res.originalAmount != null ? Number(res.originalAmount) : totalAmount || 0;
|
||
discountPrice = res.discountAmount != null ? Number(res.discountAmount) : 0;
|
||
}
|
||
|
||
if (totalAmount == null) {
|
||
// fallback:仍然展示前端计算的费用,避免空白
|
||
const fallbackPrice = calcTotalPrice(this.data.projectGroups);
|
||
this.setData({
|
||
totalPrice: fallbackPrice,
|
||
originalPrice: fallbackPrice,
|
||
discountPrice: 0
|
||
});
|
||
} else {
|
||
this.setData({
|
||
totalPrice: totalAmount,
|
||
originalPrice: originalPrice || totalAmount,
|
||
discountPrice: discountPrice || 0
|
||
});
|
||
}
|
||
})
|
||
.catch(() => {
|
||
if (seq !== this._calcSeq) return;
|
||
const fallbackPrice = calcTotalPrice(this.data.projectGroups);
|
||
this.setData({
|
||
totalPrice: fallbackPrice,
|
||
originalPrice: fallbackPrice,
|
||
discountPrice: 0
|
||
});
|
||
});
|
||
},
|
||
|
||
onCouponInput(e) {
|
||
this.setData({ couponCode: (e.detail.value || '').trim() });
|
||
},
|
||
|
||
/**
|
||
* 验证并应用优惠券
|
||
*/
|
||
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) {
|
||
wx.showToast({
|
||
title: error && error.message ? error.message : '验证失败,请稍后重试',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
wx.hideLoading();
|
||
}
|
||
},
|
||
|
||
/** 收集已选的 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) });
|
||
}
|
||
});
|
||
});
|
||
return items;
|
||
},
|
||
|
||
_validateAndGetPayload(paymentMethod) {
|
||
if (!this.data.loggedIn) {
|
||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||
return null;
|
||
}
|
||
const items = this._getSelectedItems();
|
||
if (items.length === 0) {
|
||
wx.showToast({ title: '请至少选择一个组别', icon: 'none' });
|
||
return null;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
return {
|
||
eventId: Number(this.data.eventId),
|
||
registrationMode: 1,
|
||
paymentMethod, // 1=线下 2=线上
|
||
couponCode: this.data.couponCode || null,
|
||
items
|
||
};
|
||
},
|
||
|
||
/** 线下支付 */
|
||
onPayOffline() {
|
||
const payload = this._validateAndGetPayload(1);
|
||
if (!payload) return;
|
||
this._submitApply(payload);
|
||
},
|
||
|
||
/** 线上支付 */
|
||
async onPayOnline() {
|
||
const payload = this._validateAndGetPayload(2);
|
||
if (!payload) return;
|
||
|
||
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: openid,
|
||
couponId: this.data.usedCouponId
|
||
},
|
||
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) {
|
||
this.setData({ submitting: true });
|
||
request({
|
||
path: API.apply,
|
||
method: 'POST',
|
||
needAuth: true,
|
||
data: payload,
|
||
contentType: 'application/json'
|
||
})
|
||
.then(() => {
|
||
wx.showToast({ title: '报名成功', icon: 'success' });
|
||
setTimeout(() => wx.switchTab({ url: '/pages/mine/mine' }), 800);
|
||
})
|
||
.catch((err) => {
|
||
wx.showToast({ title: err && err.message ? err.message : '提交失败', icon: 'none' });
|
||
})
|
||
.finally(() => this.setData({ submitting: false }));
|
||
}
|
||
});
|
||
|
||
|