25 KiB
小程序问题修复方案
日期:2026-06-18 基于代码分析,针对 5 个问题逐一给出根因分析和修复方案
问题一:150人短时间同时登录操作是否可以支持
现状分析
| 组件 | 当前配置 | 问题 |
|---|---|---|
| uvicorn | 单进程启动,无 worker 数配置 | 只有一个进程处理请求,无法利用多核 |
| MySQL 连接池 | create_async_engine() 无参数,SQLAlchemy 默认 pool_size=5, max_overflow=10 |
最多 15 个并发数据库连接,150 人同时操作会排队 |
| Redis | 单例连接,无连接池配置 | 高并发下 Redis 操作可能阻塞 |
| 登录限流 | 10次/60秒/IP | 同一 IP 下多人登录会被限流(公司 WiFi 场景) |
| 报名接口 | 无限流、无并发控制 | 150人同时报名可能触发 MySQL 死锁 |
结论:当前配置 无法可靠支持 150 人短时间同时操作
修复方案
1.1 uvicorn 多 worker 启动(必须)
# 启动命令改为:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
# workers = CPU 核心数 * 2 + 1,4核机器建议 4-8 个 worker
1.2 扩大数据库连接池(必须)
文件:backend/app/database.py
# 当前:
engine = create_async_engine(settings.database_url, echo=settings.app_debug, future=True)
# 改为:
engine = create_async_engine(
settings.database_url,
echo=settings.app_debug,
future=True,
pool_size=20, # 默认 5 → 20
max_overflow=30, # 默认 10 → 30
pool_recycle=3600, # 1小时回收空闲连接,防止 MySQL 超时断开
pool_pre_ping=True, # 使用前检测连接是否存活
)
1.3 报名接口加乐观锁 / 唯一索引保护(必须)
文件:backend/app/services/activity_service.py
register_activity() 中的 count + check + insert 不是原子操作,150 人并发报名可能导致超额。
# 方案 A:利用数据库唯一索引(推荐,最简单)
# registrations 表已有 (user_id, activity_id) 唯一索引,防重复报名没问题
# 但名额检查仍需改进:
# 在 _count_registered_by_gender 中加 SELECT FOR UPDATE(悲观锁)
async def _count_registered_by_gender(session, activity_id, gender):
stmt = (
select(func.count())
.select_from(Registration)
.join(User, User.id == Registration.user_id)
.where(
Registration.activity_id == activity_id,
Registration.status.in_([1, 2, 4]),
User.gender == gender,
)
.with_for_update() # 加行锁,防止并发超额
)
result = await session.execute(stmt)
return result.scalar() or 0
# 方案 B(更优):用 Redis 原子计数器做名额预检
# 报名前先 Redis DECR,成功才写数据库,失败回滚
# 适合极高并发场景,实现稍复杂
1.4 调整登录限流策略(建议)
文件:backend/app/routers/auth.py
当前限流是 per IP 的,公司 WiFi 下 150 人共用一个 IP 会被限流。
# 建议改为 per openid 限流,或放宽 IP 限流
# 方案:改为 60次/60秒/IP,或改为 per openid 5次/60秒
1.5 Nginx 反向代理(生产必须)
当前无 Nginx,uvicorn 直接对外服务。生产环境应加 Nginx:
- 处理静态文件(减轻 uvicorn 压力)
- 请求缓冲和超时控制
- 限流和 DDoS 防护
问题二:点击报名一直显示加载中,无法报名成功
根因分析
文件:miniprogram/pages/activity-detail/activity-detail.js:192-227
报名流程:handleRegister() → POST /activities/{id}/bind → 等待响应
"一直加载中" 说明请求没有返回(不是报错,报错会走 catch 分支)。可能原因:
| 可能原因 | 概率 | 说明 |
|---|---|---|
| 请求超时但未正确处理 | 高 | request.js 默认超时 10秒,但 loadDetail() 在 bind 成功后还要再请求一次活动详情,两个请求串联可能超时 |
| 后端死锁/长时间阻塞 | 高 | register_activity() 中 count + check + insert 在高并发下可能死锁 |
| session 未正确 commit | 中 | 如果 session.commit() 抛异常但被吞掉,请求会 hang |
| 微信网络问题 | 低 | 弱网环境下请求可能丢失 |
修复方案
2.1 前端:增加超时控制和重试机制(必须)
文件:miniprogram/pages/activity-detail/activity-detail.js
async handleRegister() {
if (!this.data.activity) return
if (this.data.activity.is_bound || this.data.activity.is_registered) {
wx.showToast({ title: '你已绑定该活动', icon: 'none' })
return
}
// 防重复点击
if (this.data.submitting) return
this.setData({ submitting: true })
const timeoutId = setTimeout(() => {
// 15秒后如果还在 loading,强制结束
if (this.data.submitting) {
this.setData({ submitting: false })
wx.showToast({ title: '网络超时,请重试', icon: 'none' })
}
}, 15000)
try {
await request({
url: `/activities/${this.data.id}/bind`,
method: 'POST',
timeout: 15000 // 单独设置更长超时
})
wx.showToast({ title: '报名成功', icon: 'success' })
await this.loadDetail()
} catch (err) {
console.error('Bind activity failed:', err)
if (err && err.message && err.message.includes('资料')) {
wx.setStorageSync('pending_activity_bind', {
activity_id: this.data.id,
share_token: this.data.shareToken || ''
})
wx.showModal({
title: '请先完善资料',
content: err.message,
success: (res) => {
if (res.confirm) {
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
}
})
return
}
wx.showToast({ title: err.message || '报名失败,请重试', icon: 'none' })
} finally {
clearTimeout(timeoutId)
this.setData({ submitting: false })
}
}
2.2 前端:bind 成功后不再二次请求 loadDetail(建议优化)
当前流程:bind → 成功 → loadDetail() 再请求一次 /activities/{id}。
在弱网下这意味着两个串行请求,耗时翻倍。
// 优化:bind 成功后直接更新本地状态,不重新请求
await request({ url: `/activities/${this.data.id}/bind`, method: 'POST' })
wx.showToast({ title: '报名成功', icon: 'success' })
// 直接更新本地状态,避免二次请求
this.setData({
'activity.is_bound': true,
'activity.is_registered': true,
'activity.actionText': '已报名'
})
// 可选:后台静默刷新
this.loadDetail()
2.3 后端:排查数据库死锁(必须)
文件:backend/app/services/activity_service.py:233-267
# 当前代码在高并发下的问题:
# 1. count(*) 不加锁 → 两个请求同时读到相同 count → 都认为有名额 → 都插入 → 超额
# 2. 如果 MySQL 的 unique 约束冲突,会抛 IntegrityError,但当前没捕获这个异常
# 修复:用 try/except 捕获 IntegrityError
from sqlalchemy.exc import IntegrityError
async def register_activity(session, activity, current_user):
# ... 前面的检查不变 ...
try:
registration = Registration(
user_id=current_user.id,
activity_id=activity.id,
status=1
)
session.add(registration)
await session.commit()
await session.refresh(registration)
return registration
except IntegrityError:
# 并发场景下,另一个请求已经插入了,这里返回已有的
await session.rollback()
existing = await get_registration(session, current_user.id, activity.id)
if existing:
return existing
raise
2.4 后端:添加请求超时日志(排查用)
在 register 和 bind 路由中加日志,记录请求耗时:
import time
import logging
logger = logging.getLogger(__name__)
@router.post("/{activity_id}/bind")
async def bind_activity(...):
start = time.monotonic()
try:
# ... 原有逻辑 ...
elapsed = time.monotonic() - start
if elapsed > 5:
logger.warning(f"Slow bind: activity={activity_id}, user={current_user_id}, elapsed={elapsed:.2f}s")
return result
except Exception:
logger.exception(f"Bind failed: activity={activity_id}, user={current_user_id}")
raise
问题三:转发小程序活动到微信后,分享卡片上看不到图片
根因分析
文件:miniprogram/pages/activity-detail/activity-detail.js:108-115
onShareAppMessage() {
const activity = this.data.activity || {}
return {
title: activity.title || '邀请你参加这个活动',
path: shareToken ? `...?share_token=${shareToken}` : `...?id=${this.data.id}`,
imageUrl: activity.cover_image || '' // ← 问题在这里
}
}
问题:imageUrl 使用的是 activity.cover_image,这个值在 normalizeActivity() 中经过 resolveImageUrl() 处理。但存在以下可能:
cover_image为空字符串:活动未设置封面图,imageUrl为空,微信显示默认卡片cover_image是相对路径:虽然resolveImageUrl会处理,但如果 API 返回的 URL 格式异常,可能解析失败- 微信对 imageUrl 的要求:微信要求图片 URL 必须是 HTTPS,且域名需要在小程序后台配置的 downloadFile 域名白名单中
- 图片尺寸问题:微信分享卡片推荐 5:4 比例,过大或过小的图片可能不显示
修复方案
3.1 前端:确保 imageUrl 使用正确的 HTTPS 绝对路径(必须)
文件:miniprogram/pages/activity-detail/activity-detail.js
onShareAppMessage() {
const activity = this.data.activity || {}
const shareToken = activity.share_token || this.data.shareToken || ''
// 确保使用完整的 HTTPS URL
let shareImage = activity.cover_image || ''
if (shareImage && !shareImage.startsWith('http')) {
shareImage = resolveImageUrl(shareImage)
}
return {
title: activity.title || '邀请你参加这个活动',
path: shareToken
? `/pages/activity-detail/activity-detail?share_token=${shareToken}`
: `/pages/activity-detail/activity-detail?id=${this.data.id}`,
imageUrl: shareImage
}
}
3.2 后端:确保 cover_image 字段返回完整 HTTPS URL(必须)
文件:backend/app/services/activity_service.py 中 get_activity_detail() 返回的 cover_image 需要经过 sanitize_media_url() 和完整 URL 拼接。
检查 cover_image 的存储格式:
- 如果数据库存的是相对路径(如
/static/uploads/activities/1/cover.jpg),需要拼接PUBLIC_SITE_URL - 如果存的是完整 URL,确保是 HTTPS
文件:backend/app/utils/media.py
# 确保 sanitize_media_url 返回完整 HTTPS URL
def sanitize_media_url(url: str | None) -> str | None:
if not url:
return None
url = _normalize_url(url)
# 如果是相对路径,拼接完整 URL
if url.startswith('/'):
url = f"{settings.public_site_url}{url}"
# 确保 HTTPS
if url.startswith('http://'):
url = url.replace('http://', 'https://', 1)
# 检查本地文件是否存在
if '/static/' in url:
local_path = _extract_local_path(url)
if local_path and not os.path.exists(local_path):
return None
return url
3.3 检查微信小程序后台域名配置(必须)
在 微信公众平台 → 开发 → 开发管理 → 服务器域名中,确保 downloadFile 合法域名 包含你的图片域名(如 https://ghxiangqin.com)。
3.4 使用网络图片作为 fallback(建议)
如果 cover_image 为空或加载失败,使用一张默认的活动封面图:
// 建议上传一张默认封面到 static 目录
imageUrl: shareImage || 'https://ghxiangqin.com/api/v1/static/uploads/default-share-cover.jpg'
问题四:嘉宾匹配页面的信息展示切换为与"我的参与"页面一致的列表展示方式
现状对比
| 匹配页面(match) | 我的参与页面(my-matches) | |
|---|---|---|
| 布局 | 卡片堆叠(deck),左右滑动操作 | 列表式,上下滚动 |
| 信息展示 | 头像占满卡片 + 底部浮层显示昵称/年龄/城市 | 头像缩略图 + 右侧文字信息(昵称、状态、城市、学历、爱好) |
| 交互 | 左滑跳过、右滑感兴趣、点击查看详情弹窗 | 点击按钮(撤销选择/查看完整资料) |
需求理解
将匹配页面的候选人展示从"卡片堆叠"改为"列表式",点击可以查看嘉宾详情,然后再选择是否感兴趣。
修复方案
4.1 修改匹配页面 WXML:卡片布局 → 列表布局
文件:miniprogram/pages/match/match.wxml
将现有的 deck-area 卡片堆叠替换为列表:
<!-- 替换原来的 deck-area 部分 -->
<view wx:if="{{candidates.length}}" class="candidate-list">
<view
wx:for="{{candidates}}"
wx:key="user_id"
class="candidate-item"
data-index="{{index}}"
bindtap="openCandidateDetail"
>
<image wx:if="{{item.avatar_url}}" class="candidate-avatar" src="{{item.avatar_url}}" mode="aspectFill" />
<view wx:else class="candidate-avatar avatar-placeholder">用户</view>
<view class="candidate-content">
<view class="candidate-name-row">
<view class="candidate-name">{{item.nicknameText}}</view>
<view wx:if="{{item.hasMatchScore}}" class="score-chip score-chip--{{item.matchScoreLevel}}">
{{item.matchScoreText}}
</view>
</view>
<view class="candidate-meta">{{item.cityText}} · {{item.educationText}}</view>
<view class="candidate-meta">{{item.hobbiesText || item.birthYearRangeText}}</view>
<view wx:if="{{item.personality_tags.length}}" class="candidate-tags">
<text wx:for="{{item.personality_tags}}" wx:for-item="tag" wx:key="*this" class="tag">{{tag}}</text>
</view>
</view>
<view class="candidate-arrow">›</view>
</view>
</view>
<!-- 详情弹窗(保留并增强) -->
<view wx:if="{{activeCandidate}}" class="mask" bindtap="closeCard">
<view class="sheet" catchtap="noop">
<view class="sheet-header">
<image wx:if="{{activeCandidate.avatar_url}}" class="sheet-avatar" src="{{activeCandidate.avatar_url}}" mode="aspectFill" />
<view class="sheet-header-info">
<view class="sheet-nickname">{{activeCandidate.nicknameText}}</view>
<view class="sheet-meta">{{activeCandidate.birthYearRangeText}} · {{activeCandidate.cityText}}</view>
</view>
</view>
<view wx:if="{{activeCandidate.hasMatchScore}}" class="sheet-score">
匹配度:{{activeCandidate.matchScoreText}}
</view>
<view class="sheet-section">
<view class="sheet-label">城市</view>
<view class="sheet-value">{{activeCandidate.cityText}}</view>
</view>
<view class="sheet-section">
<view class="sheet-label">学历</view>
<view class="sheet-value">{{activeCandidate.educationText || '未填写'}}</view>
</view>
<view class="sheet-section">
<view class="sheet-label">年龄</view>
<view class="sheet-value">{{activeCandidate.birthYearRangeText}}</view>
</view>
<view wx:if="{{activeCandidate.personality_tags.length}}" class="sheet-tags">
<text wx:for="{{activeCandidate.personality_tags}}" wx:key="*this" class="tag">{{item}}</text>
</view>
<view wx:if="{{activeCandidate.hasMatchReasons}}" class="sheet-reasons">
<view class="sheet-reasons-title">推荐理由</view>
<view class="sheet-reasons-text">{{activeCandidate.matchReasonsText}}</view>
</view>
<view class="sheet-actions">
<button class="ghost-btn" bindtap="skipCandidate">跳过</button>
<button class="primary-btn" data-id="{{activeCandidate.user_id}}" bindtap="likeUser">加入我的选择</button>
</view>
</view>
</view>
4.2 修改匹配页面 JS:适配列表数据结构
文件:miniprogram/pages/match/match.js
// normalizeCandidates 中增加 hobbiesText、educationText
normalizeCandidates(list) {
return (list || []).map(item => ({
...item,
avatar_url: resolveImageUrl(item.avatar_url || ''),
avatar_blur_url: resolveImageUrl(item.avatar_blur_url || ''),
nicknameText: item.nickname || '匿名用户',
birthYearText: item.birth_year ? `${item.birth_year}年` : '',
birthYearRangeText: item.birth_year ? `${new Date().getFullYear() - item.birth_year}岁左右` : '年龄未知',
cityText: item.city || '城市未知',
educationText: item.education || '学历未知',
hobbiesText: Array.isArray(item.hobbies) && item.hobbies.length ? item.hobbies.join('、') : '',
personality_tags: item.personality_tags || [],
matchScoreText: item.match_score != null ? item.match_score.toFixed(1) : '',
matchScorePercent: item.match_score != null ? Math.round(item.match_score) + '%' : '',
matchScoreLevel: item.match_score >= 80 ? 'high' : item.match_score >= 60 ? 'mid' : 'low',
matchReasonsText: Array.isArray(item.match_reasons) ? item.match_reasons.join(';') : (item.match_reasons || ''),
hasMatchReasons: !!(item.match_reasons && (Array.isArray(item.match_reasons) ? item.match_reasons.length : true)),
hasMatchScore: item.match_score != null,
selected: false
}))
},
// 打开候选人详情
openCandidateDetail(e) {
const index = e.currentTarget.dataset.index
const candidate = this.data.candidates[index]
if (!candidate) return
this.setData({
activeCandidate: candidate,
activeCandidatePersonalityText: (candidate.personality_tags || []).join('、')
})
},
// 跳过当前候选人
skipCandidate() {
const active = this.data.activeCandidate
if (!active) return
// 从列表中移除
const candidates = this.data.candidates.filter(c => c.user_id !== active.user_id)
this.setData({
candidates,
activeCandidate: null,
currentCandidate: candidates[0] || null,
nextCandidate: candidates[1] || null
})
},
4.3 修改匹配页面样式
文件:miniprogram/pages/match/match.wxss
/* 列表样式,参考 my-matches */
.candidate-list {
padding: 0 16rpx;
}
.candidate-item {
display: flex;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.candidate-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
flex-shrink: 0;
margin-right: 24rpx;
}
.candidate-content {
flex: 1;
min-width: 0;
}
.candidate-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.candidate-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.candidate-meta {
font-size: 26rpx;
color: #888;
margin-bottom: 4rpx;
}
.candidate-tags {
margin-top: 8rpx;
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
.candidate-arrow {
font-size: 40rpx;
color: #ccc;
margin-left: 16rpx;
}
/* 详情弹窗增强 */
.sheet-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.sheet-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.sheet-nickname {
font-size: 36rpx;
font-weight: 600;
}
.sheet-meta {
font-size: 26rpx;
color: #888;
margin-top: 4rpx;
}
.sheet-section {
display: flex;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.sheet-label {
width: 140rpx;
color: #888;
font-size: 28rpx;
flex-shrink: 0;
}
.sheet-value {
flex: 1;
font-size: 28rpx;
color: #333;
}
.sheet-actions {
display: flex;
gap: 20rpx;
margin-top: 32rpx;
}
.sheet-actions .ghost-btn {
flex: 1;
}
.sheet-actions .primary-btn {
flex: 2;
}
问题五:匹配详情中隐藏收入,调整字段顺序
现状分析
文件:miniprogram/pages/match-detail/match-detail.wxml
当前字段顺序:城市 → 学历 → 爱好 → 行业 → 公司 → 收入 → 自我介绍
需求:隐藏收入,按 城市 → 学历 → 年龄 → 爱好 → 行业 → 工作单位 → 自我介绍 排列
修复方案
5.1 修改 WXML:移除收入、调整顺序、增加年龄
文件:miniprogram/pages/match-detail/match-detail.wxml
<view class="page detail-page">
<view wx:if="{{loading}}" class="loading">加载中...</view>
<view wx:elif="{{userInfo}}" class="card">
<view class="header">
<image wx:if="{{userInfo.previewImage}}" class="avatar" src="{{userInfo.previewImage}}" mode="aspectFill" />
<view wx:else class="avatar avatar-placeholder">头像</view>
<view class="base-info">
<view class="nickname">{{userInfo.nicknameText}}</view>
<view class="matched-at">匹配时间:{{userInfo.matchedAt}}</view>
</view>
</view>
<view wx:if="{{userInfo.profileImages.length}}" class="gallery-strip">
<image wx:for="{{userInfo.profileImages}}" wx:key="*this" class="gallery-image" src="{{item}}" mode="aspectFill" />
</view>
<!-- 1. 城市 -->
<view class="section">
<view class="label">城市</view>
<view class="value">{{userInfo.cityText}}</view>
</view>
<!-- 2. 学历 -->
<view class="section">
<view class="label">学历</view>
<view class="value">{{userInfo.educationText}}</view>
</view>
<!-- 3. 年龄(新增) -->
<view class="section">
<view class="label">年龄</view>
<view class="value">{{userInfo.ageText}}</view>
</view>
<!-- 4. 爱好 -->
<view class="section">
<view class="label">爱好</view>
<view class="value">{{userInfo.hobbiesText}}</view>
</view>
<!-- 5. 行业 -->
<view class="section">
<view class="label">行业</view>
<view class="value">{{userInfo.jobIndustryText}}</view>
</view>
<!-- 6. 工作单位 -->
<view class="section">
<view class="label">工作单位</view>
<view class="value">{{userInfo.jobCompanyText}}</view>
</view>
<!-- 7. 自我介绍 -->
<view class="section">
<view class="label">自我介绍</view>
<view class="value intro">{{userInfo.selfIntroText}}</view>
</view>
<!-- 收入已隐藏 -->
</view>
</view>
5.2 修改 JS:增加年龄计算、移除收入
文件:miniprogram/pages/match-detail/match-detail.js
// loadDetail 中修改 userInfo 的构建:
this.setData({
userInfo: {
...otherUser,
avatar_url: resolveImageUrl(otherUser.avatar_url || ''),
avatar_blur_url: resolveImageUrl(otherUser.avatar_blur_url || ''),
profileImages,
previewImage: profileImages.length ? profileImages[0] : resolveImageUrl(otherUser.avatar_url || ''),
nicknameText: otherUser.nickname || '匿名用户',
cityText: otherUser.city || '城市未知',
educationText: otherUser.education || '学历未知',
// 新增:年龄计算
ageText: otherUser.birth_year
? `${new Date().getFullYear() - otherUser.birth_year}岁`
: '未填写',
hobbiesText: Array.isArray(otherUser.hobbies) && otherUser.hobbies.length
? otherUser.hobbies.join('、')
: '未填写爱好',
jobIndustryText: otherUser.job_industry || '未填写',
jobCompanyText: otherUser.job_company || '未填写',
// incomeRangeText 已移除,不再传给前端
selfIntroText: otherUser.self_intro || '暂无自我介绍',
matchedAt: detail.matched_at
}
})
5.3 后端:建议同时从 API 响应中移除收入字段(可选)
文件:backend/app/routers/activities.py 中 matches_detail 路由返回的 other_user 数据。
如果后端有 UserResponse schema,可以在序列化时 exclude income_range。或者保留后端不动,仅前端隐藏即可(当前方案)。
修复优先级总结
| 优先级 | 问题 | 修复项 | 预计工作量 |
|---|---|---|---|
| P0 | 问题二 | 报名加载卡住 - 前端超时控制 + 防重复点击 | 0.5h |
| P0 | 问题二 | 报名加载卡住 - 后端捕获 IntegrityError | 0.5h |
| P0 | 问题三 | 分享图片不显示 - 确保 imageUrl 为 HTTPS 绝对路径 | 1h |
| P1 | 问题五 | 匹配详情隐藏收入、调整字段顺序 | 1h |
| P1 | 问题四 | 匹配页面改为列表展示 | 3h |
| P1 | 问题一 | 数据库连接池扩容 | 0.5h |
| P2 | 问题一 | uvicorn 多 worker 启动 | 0.5h |
| P2 | 问题一 | 报名接口并发保护(悲观锁/Redis) | 2h |
| P2 | 问题一 | Nginx 反向代理部署 | 2h |
验证方案
| 问题 | 验证方法 |
|---|---|
| 问题一 | 使用压测工具(如 locust)模拟 150 并发登录 + 报名,观察错误率和响应时间 |
| 问题二 | 弱网环境下(3G 模拟)点击报名,确认不会一直 loading;快速连续点击 10 次,确认不会重复报名 |
| 问题三 | 分享活动到微信聊天,确认卡片显示活动封面图 |
| 问题四 | 进入匹配页,确认显示为列表布局;点击嘉宾可查看详情弹窗;弹窗中可选择"加入我的选择" |
| 问题五 | 查看匹配详情页,确认无收入字段;字段顺序为:城市→学历→年龄→爱好→行业→工作单位→自我介绍 |