Files
Mycontent/miniprogram/pages/match/match.js
乘风 35e18a7ccb chore: update experience index and documentation for various roles
- Added recent experience entries for roles including Assistant, Mini Program Developer, Backend Engineer, Product Manager, Software Tester, and Team.
- Enhanced documentation structure in the development assistant's experience list and project index.
- Updated role-flow control documentation to reflect new methodologies and programming thinking.
- Improved error handling in the mini program and backend API for better user feedback during operations.

Made-with: Cursor
2026-04-14 11:24:18 +08:00

799 lines
24 KiB
JavaScript
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.

/**
* 卡若创业派对 - 找伙伴页
* 按H5网页端完全重构
* 开发: 卡若
*/
const app = getApp()
const { checkAndExecute } = require('../../utils/ruleEngine.js')
const { trackClick } = require('../../utils/trackClick')
/** 是否视为未设置头像(勿用 includes('132'):微信 CDN 合法头像 URL 普遍含 /132/ 尺寸段) */
function isMissingOrPlaceholderAvatar(avatarUrl, hasAvatarFromServer) {
if (hasAvatarFromServer === true || hasAvatarFromServer === 1) return false
const a = (avatarUrl || '').trim()
if (!a) return true
const u = a.toLowerCase()
if (u.includes('default')) return true
// 微信默认占位常见以 /0 结尾;/132/ 为正常尺寸路径,不能当作占位
if (/\/0($|[?#])/.test(u)) return true
return false
}
// 默认匹配类型配置
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
// 资源对接:需要登录+购买章节才能使用填写2项信息我能帮到你什么、我需要什么帮助
// 导师顾问:跳转到存客宝添加微信
// 团队招募:跳转到存客宝添加微信
let MATCH_TYPES = [
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: 'star', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: 'users', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '导师顾问', icon: 'heart', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: 'gamepad', matchFromDB: true, showJoinAfterMatch: true }
]
let FREE_MATCH_LIMIT = 1 // 终身免费匹配次数(不按日重置,与后端 match_config 一致)
Page({
data: {
statusBarHeight: 44,
// 匹配类型
matchTypes: MATCH_TYPES,
selectedType: 'partner',
currentTypeLabel: '找伙伴',
// 用户状态
isLoggedIn: false,
hasPurchased: false,
hasFullBook: false,
// 匹配次数(以服务端 purchase-status.matchQuota 为准)
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchesRemaining: FREE_MATCH_LIMIT,
needPayToMatch: false,
// 匹配状态
isMatching: false,
matchAttempts: 0,
currentMatch: null,
// 加入弹窗
showJoinModal: false,
joinType: null,
joinTypeLabel: '',
contactType: 'phone',
phoneNumber: '',
wechatId: '',
userPhone: '',
isJoining: false,
joinSuccess: false,
joinError: '',
needBindFirst: false,
// 资源对接表单
canHelp: '',
needHelp: '',
goodAt: '',
// 解锁弹窗
showUnlockModal: false,
// 手机/微信号弹窗stitch_soul
showContactModal: false,
contactPhone: '',
contactWechat: '',
contactSaving: false,
// 匹配价格可配置matchPriceOriginal 为划线原价展示
matchPrice: 1,
matchPriceOriginal: 9.9,
extraMatches: 0,
auditMode: false
},
async onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44,
auditMode: app.globalData.auditMode || false
})
await this.loadMatchConfig()
this.loadStoredContact()
this.initUserStatus()
await this.refreshMatchQuotaFromServer()
},
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
if (tabBar.updateSelected) {
tabBar.updateSelected()
} else {
tabBar.setData({ selected: 2 })
}
}
this.initUserStatus()
this.refreshMatchQuotaFromServer()
},
// 加载匹配配置
async loadMatchConfig() {
try {
const res = await app.request({ url: '/api/miniprogram/match/config', silent: true, method: 'GET',
method: 'GET'
})
if (res.success && res.data) {
// 更新全局配置,导师顾问类型强制显示「导师顾问」
let types = res.data.matchTypes || MATCH_TYPES
types = types.map(t => {
if (t.id === 'mentor') {
return { ...t, label: '导师顾问', matchLabel: '导师顾问' }
}
return t
})
MATCH_TYPES = types
FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT
if (FREE_MATCH_LIMIT > 1) FREE_MATCH_LIMIT = 1
const matchPrice = res.data.matchPrice || 1
const matchPriceOriginal = res.data.matchPriceOriginal != null ? res.data.matchPriceOriginal : 9.9
this.setData({
matchTypes: MATCH_TYPES,
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchPrice: matchPrice,
matchPriceOriginal: matchPriceOriginal
})
console.log('[Match] 加载匹配配置成功:', {
types: MATCH_TYPES.length,
freeLimit: FREE_MATCH_LIMIT,
price: matchPrice
})
}
} catch (e) {
console.log('[Match] 加载匹配配置失败,使用默认配置:', e)
}
},
// 加载本地存储的联系方式
loadStoredContact() {
const phone = wx.getStorageSync('user_phone') || ''
const wechat = wx.getStorageSync('user_wechat') || ''
this.setData({
phoneNumber: phone,
wechatId: wechat,
userPhone: phone
})
},
/** 从服务端同步匹配配额(终身免费 + 已购次数,不按自然日重置) */
async refreshMatchQuotaFromServer() {
const userId = app.globalData.userInfo?.id
const hasFullBookGlobal = !!app.globalData.hasFullBook
if (!userId) {
this.setData({
matchesRemaining: FREE_MATCH_LIMIT,
needPayToMatch: false,
extraMatches: 0,
totalMatchesAllowed: FREE_MATCH_LIMIT
})
return
}
try {
const res = await app.request({
url: `/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`,
silent: true
})
if (!res.success || !res.data) return
const hasFullBook = res.data.hasFullBook === true
const mq = res.data.matchQuota || {}
const remain = mq.remainToday != null ? Number(mq.remainToday) : 0
const purchasedRemain = mq.purchasedRemain != null ? Number(mq.purchasedRemain) : 0
const purchasedTotal = mq.purchasedTotal != null ? Number(mq.purchasedTotal) : 0
if (hasFullBook) {
this.setData({
hasFullBook: true,
matchesRemaining: 999999,
needPayToMatch: false,
extraMatches: purchasedRemain,
totalMatchesAllowed: 999999
})
return
}
this.setData({
hasFullBook: false,
matchesRemaining: Math.max(0, remain),
needPayToMatch: remain <= 0,
extraMatches: purchasedRemain,
totalMatchesAllowed: FREE_MATCH_LIMIT + purchasedTotal
})
} catch (e) {
console.log('[Match] 同步匹配配额失败', e)
this.setData({
hasFullBook: hasFullBookGlobal,
matchesRemaining: hasFullBookGlobal ? 999999 : this.data.matchesRemaining,
needPayToMatch: !hasFullBookGlobal && this.data.matchesRemaining <= 0
})
}
},
// 初始化用户状态
initUserStatus() {
const { isLoggedIn, hasFullBook } = app.globalData
this.setData({
isLoggedIn,
hasFullBook,
hasPurchased: true
})
},
// 选择匹配类型
selectType(e) {
const typeId = e.currentTarget.dataset.type
trackClick('match', 'tab_click', typeId || '类型')
const type = MATCH_TYPES.find(t => t.id === typeId)
this.setData({
selectedType: typeId,
currentTypeLabel: type?.matchLabel || type?.label || '创业伙伴'
})
},
// 点击匹配按钮(含 2 秒节流防重复触发)
async handleMatchClick() {
if (this.data.isMatching) return
const now = Date.now()
if (this._lastMatchClickTs && now - this._lastMatchClickTs < 2000) return
this._lastMatchClickTs = now
trackClick('match', 'btn_click', '匹配_' + (this.data.selectedType || ''))
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
if (this.data.isLoggedIn && currentType?.matchFromDB) {
await this.ensureContactInfo(() => this._handleMatchClickInner(currentType))
} else {
this._handleMatchClickInner(currentType)
}
},
async ensureContactInfo(callback) {
const userId = app.globalData.userInfo?.id
if (!userId) { callback(); return }
try {
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
const d = res?.data || {}
const avatar = (d.avatar || d.avatarUrl || app.globalData.userInfo?.avatar || app.globalData.userInfo?.avatarUrl || '').trim()
const hasAvatarFlag = d.hasAvatar === true || d.hasAvatar === 1
if (isMissingOrPlaceholderAvatar(avatar, hasAvatarFlag)) {
wx.showModal({
title: '完善头像',
content: '请先设置头像后再使用匹配功能',
confirmText: '去设置',
cancelText: '取消',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
const phone = (d.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
const wechat = (d.wechatId || d.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (phone && /^1[3-9]\d{9}$/.test(phone)) {
callback()
return
}
this.setData({
showContactModal: true,
contactPhone: phone || '',
contactWechat: wechat || '',
})
this._contactCallback = callback
} catch (e) {
callback()
}
},
closeContactModal() {
this.setData({ showContactModal: false })
this._contactCallback = null
},
onContactPhoneInput(e) { this.setData({ contactPhone: e.detail.value }) },
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
async saveContactInfo() {
const phone = (this.data.contactPhone || '').trim().replace(/\s/g, '')
const wechat = (this.data.contactWechat || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showToast({ title: '请输入正确的11位手机号必填', icon: 'none' })
return
}
this.setData({ contactSaving: true })
try {
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: {
userId: app.globalData.userInfo?.id,
phone: phone || undefined,
wechatId: wechat || undefined,
},
})
if (phone) wx.setStorageSync('user_phone', phone)
if (wechat) wx.setStorageSync('user_wechat', wechat)
this.loadStoredContact()
this.closeContactModal()
wx.showToast({ title: '已保存', icon: 'success' })
const cb = this._contactCallback
this._contactCallback = null
if (cb) cb()
} catch (e) {
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
}
this.setData({ contactSaving: false })
},
_handleMatchClickInner(currentType) {
// 资源对接类型需要登录+购买章节才能使用
if (currentType && currentType.id === 'investor') {
// 检查是否登录
if (!this.data.isLoggedIn) {
wx.showModal({
title: '需要登录',
content: '请先登录后再使用资源对接功能',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/my/my' })
}
}
})
return
}
// 检查是否购买过章节
const hasPurchased = app.globalData.purchasedSections?.length > 0 || app.globalData.hasFullBook
if (!hasPurchased) {
wx.showModal({
title: '需要购买章节',
content: '购买任意章节后即可使用资源对接功能',
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/chapters/chapters' })
}
}
})
return
}
}
// 导师顾问:动画后跳转独立导师页
if (currentType && currentType.id === 'mentor') {
this.startMatchingAnimation(currentType)
return
}
// 资源对接/团队招募走真实DB匹配匹配成功后可加入
if (currentType && currentType.showJoinAfterMatch) {
if (this.data.needPayToMatch) {
this.setData({ showUnlockModal: true })
return
}
this.startMatch()
return
}
// 创业合伙类型 - 真正的匹配功能
if (this.data.needPayToMatch) {
this.setData({ showUnlockModal: true })
return
}
this.startMatch()
},
// 导师顾问专用:动画后跳转导师列表页
startMatchingAnimation(currentType) {
this.setData({ isMatching: true, matchAttempts: 0, currentMatch: null })
const timer = setInterval(() => {
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
}, 500)
const delay = Math.random() * 1500 + 1500
setTimeout(() => {
clearInterval(timer)
this.setData({ isMatching: false })
wx.navigateTo({ url: '/pages/mentors/mentors' })
}, delay)
},
// 显示购买提示
showPurchaseTip() {
wx.showModal({
title: '需要购买书籍',
content: '购买《卡若创业派对》后即可使用匹配功能仅需9.9元',
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
this.goToChapters()
}
}
})
},
// 开始匹配 - 只匹配数据库中的真实用户
async startMatch() {
const uidEarly = app.globalData.userInfo?.id
if (!uidEarly) {
wx.showModal({
title: '需要登录',
content: '找伙伴匹配需登录账号,请先登录',
confirmText: '去登录',
success: (r) => { if (r.confirm) wx.switchTab({ url: '/pages/my/my' }) },
})
return
}
this.setData({
isMatching: true,
matchAttempts: 0,
currentMatch: null
})
// 匹配动画计时器
const timer = setInterval(() => {
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
}, 1000)
// 从数据库获取真实用户匹配(带上手机/微信写入 match_records与流量池运营对齐
let matchedUser = null
let matchFailHint = ''
const uid = app.globalData.userInfo?.id || ''
const phoneForMatch = (this.data.phoneNumber || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
const wechatForMatch = (this.data.wechatId || wx.getStorageSync('user_wechat') || '').trim()
try {
const res = await app.request({
url: '/api/miniprogram/match/users',
silent: true,
method: 'POST',
data: {
matchType: this.data.selectedType,
userId: uid,
phone: phoneForMatch || undefined,
wechatId: wechatForMatch || undefined,
},
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
} else if (res && !res.success) {
matchFailHint = res.message || res.error || ''
if (res.code === 'QUOTA_EXCEEDED') {
matchFailHint = matchFailHint || '免费次数已用完,可付费购买匹配次数后再试'
} else if (res.code === 'NO_USERS') {
matchFailHint = matchFailHint || '当前流量池暂无可匹配用户,可稍后再试;补全档案后匹配范围通常更大。'
}
}
} catch (e) {
console.log('[Match] 数据库匹配失败:', e)
matchFailHint = (e && e.message) ? String(e.message) : '网络异常,请稍后重试'
}
// 延迟显示结果(模拟匹配过程)
const delay = Math.random() * 2000 + 2000
setTimeout(() => {
clearInterval(timer)
// 如果没有匹配到用户,提示用户
if (!matchedUser) {
this.setData({ isMatching: false })
wx.showModal({
title: '暂无匹配',
content: matchFailHint || '当前暂无合适的匹配用户,请稍后再试',
showCancel: false,
confirmText: '知道了'
})
return
}
this.setData({
isMatching: false,
currentMatch: matchedUser
})
this.refreshMatchQuotaFromServer()
// 上报匹配行为到存客宝
this.reportMatch(matchedUser)
}, delay)
},
// 上报匹配行为
async reportMatch(matchedUser) {
try {
await app.request({ url: '/api/miniprogram/ckb/match', silent: true,
method: 'POST',
data: {
matchType: this.data.selectedType,
phone: this.data.phoneNumber,
wechat: this.data.wechatId,
userId: app.globalData.userInfo?.id || '',
nickname: app.globalData.userInfo?.nickname || '',
matchedUser: {
id: matchedUser.id,
nickname: matchedUser.nickname,
matchScore: matchedUser.matchScore
}
}
})
// 匹配后规则:资料未齐时提示补全(服务端 profile 合并,见 ruleEngine
checkAndExecute('after_match', this)
} catch (e) {
console.log('上报匹配失败:', e)
}
},
// 取消匹配
cancelMatch() {
this.setData({ isMatching: false, matchAttempts: 0 })
},
// 重置匹配(返回)
resetMatch() {
this.setData({ currentMatch: null })
},
// 资源对接/团队招募匹配成功后打开加入弹窗
openJoinAfterMatch() {
const typeId = this.data.selectedType
const currentType = MATCH_TYPES.find(t => t.id === typeId)
this.setData({
showJoinModal: true,
joinType: typeId,
joinTypeLabel: currentType?.matchLabel || currentType?.label || '加入',
joinSuccess: false,
joinError: '',
needBindFirst: false
})
},
// 添加微信好友
handleAddWechat() {
if (!this.data.currentMatch) return
const wechat = this.data.currentMatch.wechat
if (!wechat) {
this.handleCopyPhone()
return
}
wx.setClipboardData({
data: wechat,
success: () => {
wx.showModal({
title: '微信号已复制',
content: `微信号:${wechat}\n\n请打开微信添加好友,备注"创业合作"即可`,
showCancel: false,
confirmText: '知道了'
})
}
})
},
handleCopyPhone() {
if (!this.data.currentMatch) return
const phone = this.data.currentMatch.phone
if (!phone) {
wx.showToast({ title: '暂无联系方式', icon: 'none' })
return
}
wx.setClipboardData({
data: phone,
success: () => {
wx.showModal({
title: '手机号已复制',
content: `手机号:${phone}\n\n可直接拨打或通过手机号搜索添加微信`,
showCancel: false,
confirmText: '知道了'
})
}
})
},
// 切换联系方式类型
switchContactType(e) {
const type = e.currentTarget.dataset.type
this.setData({ contactType: type, joinError: '' })
},
// 手机号输入
onPhoneInput(e) {
this.setData({
phoneNumber: e.detail.value.replace(/\D/g, '').slice(0, 11),
joinError: ''
})
},
// 资源对接表单输入
onCanHelpInput(e) {
this.setData({ canHelp: e.detail.value })
},
onNeedHelpInput(e) {
this.setData({ needHelp: e.detail.value })
},
onGoodAtInput(e) {
this.setData({ goodAt: e.detail.value })
},
// 微信号输入
onWechatInput(e) {
this.setData({
wechatId: e.detail.value,
joinError: ''
})
},
// 提交加入
async handleJoinSubmit() {
const { contactType, phoneNumber, wechatId, joinType, isJoining, canHelp, needHelp } = this.data
if (isJoining) return
// 验证联系方式
if (contactType === 'phone') {
if (!phoneNumber || phoneNumber.length !== 11) {
this.setData({ joinError: '请输入正确的11位手机号' })
return
}
} else {
if (!wechatId || wechatId.length < 6) {
this.setData({ joinError: '请输入正确的微信号至少6位' })
return
}
}
// 资源对接需要填写两项信息
if (joinType === 'investor') {
if (!canHelp || canHelp.trim().length < 2) {
this.setData({ joinError: '请填写"我能帮到你什么"' })
return
}
if (!needHelp || needHelp.trim().length < 2) {
this.setData({ joinError: '请填写"我需要什么帮助"' })
return
}
}
this.setData({ isJoining: true, joinError: '' })
try {
const res = await app.request('/api/miniprogram/ckb/join', {
method: 'POST',
data: {
type: joinType,
phone: contactType === 'phone' ? phoneNumber : '',
wechat: contactType === 'wechat' ? wechatId : '',
userId: app.globalData.userInfo?.id || '',
// 资源对接专属字段
canHelp: joinType === 'investor' ? canHelp : '',
needHelp: joinType === 'investor' ? needHelp : ''
}
})
// 保存联系方式到本地
if (phoneNumber) wx.setStorageSync('user_phone', phoneNumber)
if (wechatId) wx.setStorageSync('user_wechat', wechatId)
if (res.success) {
this.setData({ joinSuccess: true })
setTimeout(() => {
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
} else {
wx.showToast({ title: res.error || '加入失败', icon: 'none' })
}
} catch (e) {
const resp = e && e.response
wx.showToast({
title: (resp && (resp.message || resp.error)) || (e && e.message) || '网络异常,请重试',
icon: 'none',
})
} finally {
this.setData({ isJoining: false })
}
},
// 关闭加入弹窗
closeJoinModal() {
if (this.data.isJoining) return
this.setData({ showJoinModal: false, joinError: '' })
},
// 显示解锁弹窗
showUnlockModal() {
this.setData({ showUnlockModal: true })
},
// 关闭解锁弹窗
closeUnlockModal() {
this.setData({ showUnlockModal: false })
},
// 购买匹配次数
async buyMatchCount() {
this.setData({ showUnlockModal: false })
try {
const openId = await app.ensurePayOpenId()
if (!openId) {
wx.showModal({
title: '无法拉起支付',
content: '未获取到微信支付标识,请完全关闭小程序后重新进入,或退出账号重新登录。',
showCancel: false,
})
return
}
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 调用支付接口购买匹配次数
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
openId,
productType: 'match',
productId: 'match_1',
amount: 1,
description: '匹配次数x1',
userId: app.globalData.userInfo?.id || '',
referralCode: referralCode || undefined
}
})
if (res.success && res.data?.payParams) {
// 调用微信支付
await new Promise((resolve, reject) => {
wx.requestPayment({
...res.data.payParams,
success: resolve,
fail: reject
})
})
wx.showToast({ title: '购买成功', icon: 'success' })
await this.refreshMatchQuotaFromServer()
} else {
throw new Error(res.error || '创建订单失败')
}
} catch (e) {
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消', icon: 'none' })
} else {
wx.showToast({ title: '支付失败,请重试', icon: 'none' })
}
}
},
// 跳转到目录页购买
goToChapters() {
this.setData({ showUnlockModal: false })
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 阻止事件冒泡
preventBubble() {},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: '卡若创业派对 - 找伙伴',
path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
return { title: '卡若创业派对 - 找伙伴', query: ref ? `ref=${ref}` : '' }
}
})