前阵子帮一个做非遗文创的朋友,搭了一套民间艺术知识答题小程序。玩法不复杂:用户进入小程序,每次从题库里随机抽 10 道题,答对一题得 10 分,累计到一定积分,可以在积分商城里兑换他们店里的文创产品优惠券。就是这个小程序,从 0 到上线大概用了一个多月,中间踩了不少坑,尤其是支付能力和微信审核这两个环节,差点卡住上线。如果你也在做类似的随机出题、积分、兑换优惠券类小程序,这篇文章应该能帮你少走很多弯路。
1. 项目概述:为什么做一套“答题+积分+兑换”的小程序
1.1 民间艺术答题小程序的真实需求
先说项目背景。朋友在做一个地方非遗文创品牌,产品线有剪纸、年画、皮影、手绣这些,线下门店在景区里,线上有个小程序商城。问题很典型:小程序商城流量低,用户下了单就走,没有粘性。他们想做一个能把用户留住的玩法,同时还能普及民间艺术知识,而不是干巴巴地上架商品。
答题小程序正好匹配这个需求。民间艺术本身知识点多、趣味性强,适合做成选择题。比如“剪纸中最常见的吉祥图案是什么”“皮影戏最早出现在哪个朝代”“杨柳青年画产自哪个省份”,这些题目既能让用户长知识,又能和文创产品形成情感连接。用户答完题,对某个非遗项目产生兴趣,再去兑换一张优惠券下单,转化路径非常自然。
这套模式适合谁?文化馆、博物馆、景区、非遗工作室、地方文旅项目,甚至想给电商小程序做用户运营的个人开发者都可以参考。核心产品逻辑就三个闭环:随机出题带来新鲜感,答对积累积分带来成就感,兑换文创产品优惠券带来实打实的利益刺激。三个环节串起来,用户才愿意持续回来。
1.2 技术方案选型:原生小程序加云开发
技术栈我选了微信小程序原生加微信云开发。理由很简单:这个项目不需要复杂的服务器架构,但又需要稳定的登录态、数据库和事务能力。
- 登录:云开发自带
openid识别,用户打开小程序就自动拿到唯一身份,不用做手机号授权、微信登录那一套。 - 数据库:云数据库支持基础查询、聚合、事务,存题库、积分流水、优惠券记录都够用。
- 部署:云函数按调用次数计费,前期流量不大,基本在免费额度内。
如果用传统前后端方案,还得买服务器、配域名、过备案、写接口文档,一个知识答题小程序被运维成本拖死,没必要。
数据模型我设计了 6 个核心集合,这里直接列出来给大家参考:
| 集合名 | 用途 | 关键字段 |
|---|---|---|
| questions | 题库 | title、options、answer、category、difficulty、enabled |
| quiz_sessions | 答题会话 | openid、status、questions、startTime |
| users | 用户扩展信息 | openid、score、nickname、avatar |
| score_logs | 积分流水 | openid、change、balance、type、source |
| coupon_templates | 优惠券模板 | title、cost、stock、validFrom、validTo、status |
| user_coupons | 用户已兑换优惠券 | openid、templateId、code、status、expireAt |
这套表结构从一开始就要定好,不然后面加字段、迁数据相当痛苦。score_logs 尤其重要,每一分钱的变动都要留痕,后面排查用户纠纷、刷分问题全靠它。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 随机出题模块:让每次答题都有新鲜感
2.1 题库结构设计,先给内容定好“书架”
民间艺术知识题目有很多分类,题库设计时第一件事是分类。我按非遗门类拆成了剪纸、年画、皮影、刺绣、陶瓷、戏曲、曲艺、传统工艺等大类,每道题可以打多个标签(tags),比如“剪纸-吉祥图案-北方风格”。
这种设计有什么好处?后期做专题挑战赛时,可以直接按 category 或 tags 筛选题目,不用再造一套题库。比如做“剪纸知识周”,前端传一个 category: 'paper-cutting',后端就把对应分类的题目抽出来。
题目字段里,answer 我存的是选项下标(0、1、2、3),不是答案文本。这样判分逻辑简单,而且前端不接收答案字段,用户从小程序端根本拿不到正确答案。其他字段像 explain,用来答完题显示解析,方便做知识科普。
题库内容来源要特别注意版权和准确性问题。朋友那边是和当地非遗传承人、文化馆合作整理的公开资料,再人工校对一遍,确保没有政治敏感、历史争议类内容。如果你是自己收集题库,千万别从网上随便扒,至少核对两个独立来源,并且避免涉及具体历史人物评价。
2.2 随机抽题怎么做才不卡
随机出题是用户感知最强的功能,也是新手最容易做错的地方。最直观的写法是“从题库里随机取 N 条记录”,听起来没问题,里面藏着性能坑。
在小程序端直接 db.collection('questions').where({ enabled: true }).skip(randomIndex).limit(10) 这种方式,一旦题库超过几千条,skip 的性能会明显下降,而且并发请求容易抽到重复题。云函数里如果用 orderBy(Math.random()) 这种方式,不同数据库实现差异很大,有的甚至不支持。
我在项目里用的是聚合管道里的 sample 操作,自带随机抽样,效率高。示例代码如下:
javascript复制const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
exports.main = async (event) => {
const { OPENID } = cloud.getWXContext()
const { category, count = 10 } = event
const where = { enabled: true }
if (category) where.category = category
const res = await db.collection('questions')
.aggregate()
.match(where)
.sample({ size: Math.min(count, 20) })
.project({
title: true,
options: true,
category: true,
difficulty: true,
explain: true
})
.end()
const questions = res.list
const quizId = `${Date.now()}_${OPENID}_${Math.random().toString(36).slice(2, 8)}`
// 答案只存服务端会话里,不下发给前端
await db.collection('quiz_sessions').add({
data: {
quizId,
openid: OPENID,
questions: questions.map(q => ({
id: q._id,
answer: q.answer
})),
status: 'ongoing',
startTime: db.serverDate()
}
})
return { quizId, questions }
}
这段代码有几个关键点:
第一,sample 的 size 我限制在 20 以内,不是不能一次抽 50 道。抽太多会明显增加响应时间,用户等太久就流失了。一局 10 道题,体感最好。
第二,题目列表返回给前端时不带 answer。答案被单独存在 quiz_sessions 集合里,等用户提交后再由云函数比对。这一步至关重要,如果有人抓包你的小程序请求,看到的题目只有题干和选项,没有正确答案,安全性和公平性都有保障。
第三,quizId 生成加上了 openid 和随机数,避免并发情况下重复创建会话。前端拿到 quizId 后,后续提交答案都会带上它。
题目防重复怎么处理?如果是练习模式,用户连续刷题时,可能抽到已经答过的题,这个其实可以接受,毕竟知识点需要重复巩固。但如果你做“挑战模式”,建议加一个 answered_question_ids 数组记录用户答过的题目,抽题时用 where({ _id: _.nin(answeredQuestionIds) }) 排除掉。
2.3 判分逻辑与答题防作弊
用户答完题,前端把答案数组提交上来,云函数统一判分。我给每个题目的权重设为基础分 10 分,连续答对 3 题后额外加 5 分,连续答对 5 题再加 10 分。这个规则能刺激用户追求连击,但不会让积分泛滥。
判分代码大致长这样:
javascript复制exports.main = async (event) => {
const { quizId, answers } = event
const { OPENID } = cloud.getWXContext()
const sessionRes = await db.collection('quiz_sessions').doc(quizId).get()
const session = sessionRes.data
if (!session || session.openid !== OPENID) {
return { code: 40001, msg: '答题会话不存在或无权访问' }
}
if (session.status !== 'ongoing') {
return { code: 40002, msg: '该会话已提交,不能重复判分' }
}
let score = 0
let combo = 0
const results = session.questions.map(item => {
const ok = answers[item.id] === item.answer
if (ok) {
combo += 1
score += 10
if (combo >= 5) score += 10
else if (combo >= 3) score += 5
} else {
combo = 0
}
return { id: item.id, ok }
})
// 更新会话状态
await db.collection('quiz_sessions').doc(quizId).update({
data: { status: 'finished', score, updatedAt: db.serverDate() }
})
return { code: 0, score, results, combo, explain: '请查看逐题结果' }
}
这个版本我简化了积分入账部分,生产环境还要把 score 写进 score_logs 和 users.score,并且用事务保证一致性,这个在下一章细讲。
防作弊的一个小细节:判分完成后一定要把 quiz_sessions.status 改成 finished。否则用户把同一份答案提交 100 次,积分就会加 100 次。这里还要在前端做按钮加载态,防止用户连点。
3. 积分系统:控制好“通货膨胀”才不会崩
3.1 积分规则设计,先定好收入和支出的边界
积分系统做得不好,小程序很容易出两种问题:一种是积分发太多,用户随手兑换一堆优惠券,商户亏本;另一种是积分给得太少,用户攒一个月也兑不到东西,直接放弃。所以一定要在开发前把积分规则写成表格,跟业务方确认清楚。
我这里的规则:
| 场景 | 积分变动 | 说明 |
|---|---|---|
| 答对一题 | +10 | 基础积分 |
| 连续答对 3 题 | +5 | 额外奖励 |
| 连续答对 5 题 | +10 | 额外奖励 |
| 每日答题积分上限 | 200 | 防止过度刷分 |
| 兑换优惠券 | 按券模板扣减 | 例如 500 分兑 20 元券 |
| 用户举报违规 | -100 | 可选规则 |
每日 200 分上限是个关键设计。如果不限制,有人一天能答几百道题,把库存券全兑走,运营成本失控。用户每天最多认真答个十几道题就能拿满,这个上限既不打击积极性,又控制了成本。
积分有效期我也做了设计:每一笔积分入账后 365 天有效,按最早入账的积分先消耗(FIFO)。逻辑不难,就是 score_logs 里记录每笔积分的 expireAt,兑换时优先扣减最早到期的积分。没有这个逻辑,用户攒了一堆老积分,突然某天一次性消费,会造成会计上的混乱。
3.2 加积分、扣积分一定要用事务
这里是最容易踩坑的地方。积分操作不是“查余额、算新余额、写回数据库”这么简单,并发情况下会出大问题。
举个例子:用户同时提交两次判分请求,由于网络延迟等原因,后端同时处理这两个请求。如果先查当前积分为 100,两个请求各自加上 10 分并写回,最终可能只加了 10 分而不是 20 分。反过来,兑换时两个请求同时扣 500 分,也可能把积分扣成负数,或者把一个库存为 1 的优惠券同时发给了两个人。
云开发提供了事务能力。我建议所有涉及积分变动的操作,统一用 db.runTransaction 或者手动事务处理。判分加积分示例:
javascript复制const transaction = await db.startTransaction()
try {
// 读取用户当前积分
const userRes = await transaction.collection('users')
.where({ openid: OPENID }).get()
const user = userRes.data[0]
const oldScore = user?.score || 0
const newScore = oldScore + score
// 更新用户积分
await transaction.collection('users')
.where({ openid: OPENID })
.update({ data: { score: _.inc(score) } })
// 插入积分流水
await transaction.collection('score_logs').add({
data: {
openid: OPENID,
change: score,
balance: newScore,
type: 'quiz',
source: quizId,
createdAt: db.serverDate()
}
})
await transaction.commit()
} catch (e) {
await transaction.rollback()
throw e
}
注意几点:
- 读用户积分的操作也在事务里完成,保证读到的余额是最新且稳定的。
- 更新用户积分用了
_.inc(score),而不是写死newScore。inc是原子的,能避免并发重复加。但我建议最终还要靠事务保证流水和用户表的一致性,两步一起成功、一起失败。 score_logs里的balance一定要存更新后的余额,方便以后做对账。用户投诉“我积分怎么少了”,一张流水表就能说清楚。
兑换优惠券同理,先检查库存、再检查积分、然后扣减、最后发券,这一步全部放在事务里。如果你用的是云开发,还要注意事务中不能对不存在文档执行局部更新,所以用户不存在或者券模板不存在的情况要单独先判断。
3.3 前端积分展示的几个细节
积分页面不需要花哨,但有几个细节能提升体验:
- 答题结果页展示“本局得分”和“总积分”,让用户明显感知到答题和积分的关系。
- 积分明细页用时间倒序展示流水,每条后面跟着“答题获得 +10”“兑换优惠券 -500”这样的描述。
- 顶部放一个当前积分余额,旁边配一个“去兑换”按钮,缩短从积分到兑换的路径。
- 积分不足时,不要只弹一个“积分不足”的提示,要告诉用户“还差 X 分,再答 X 题即可兑换”,给一个明确的行动指引。
这几个点都是小改动,但直接关系转化率。用户不是不愿意答题,而是不知道积分能干什么、还差多少。把数字摆清楚,他就知道下一步要怎么做了。
4. 兑换模块:从积分到优惠券的最后一公里
4.1 优惠券模板与库存设计
优惠券不是只存一个“满 99 减 20”的字符串,需要一套模板化的数据结构。coupon_templates 表我建议至少包含这些字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| title | 券名称 | 满99减20文创券 |
| type | 满减券/折扣券/无门槛券 | 满减 |
| faceValue | 抵扣金额或折扣 | 20 |
| minAmount | 最低消费金额 | 99 |
| cost | 兑换所需积分 | 500 |
| stock | 剩余库存 | 100 |
| totalCount | 发行总量 | 200 |
| validFrom / validTo | 有效期 | 2025-01-01 ~ 2025-12-31 |
| status | active / paused / expired | active |
stock 和 totalCount 分开,方便看发售进度。库存扣到 0 时,前端兑换按钮要自动置灰,并且显示“已抢完”。不然用户辛苦答了一周题,点兑换才发现没了,体验非常糟糕。
券码生成我用了 12 位大写字母加数字的随机串,前 8 位随机,第 9-10 位是券模板 ID 的哈希加一段校验位,后 2 位是校验位。这样做的目的是:管理员拿到券码能快速看出是哪张券,同时校验位能防止用户随便填一个码就去核销。
code复制function generateCouponCode(templateId) {
const randomPart = Math.random().toString(36).slice(2, 10).toUpperCase()
const hashPart = (templateId % 100).toString().padStart(2, '0')
const checksum = 'AB' // 实际项目中用哈希算法生成
return randomPart + hashPart + checksum
}
真实项目里校验位建议用 crc16 或者简单异或算法,这里只是示意。
4.2 兑换流程与事务实现
兑换流程是积分系统的“出口”,也是唯一消耗积分的地方,必须最严格对待。我的云函数逻辑顺序:
- 校验用户身份和参数。
- 查询优惠券模板,检查状态和库存。
- 开启事务。
- 事务内读取用户积分,检查余额。
- 扣积分,生成用户优惠券记录,扣库存。
- 写入积分流水。
- 提交事务,返回优惠券码。
核心代码如下:
javascript复制exports.main = async (event) => {
const { templateId } = event
const { OPENID } = cloud.getWXContext()
const templateRes = await db.collection('coupon_templates').doc(templateId).get()
const template = templateRes.data
if (!template || template.status !== 'active') {
return { code: 40002, msg: '优惠券模板不存在或已下架' }
}
if (template.stock <= 0) {
return { code: 40003, msg: '库存不足' }
}
const transaction = await db.startTransaction()
try {
const userRes = await transaction.collection('users')
.where({ openid: OPENID }).get()
const user = userRes.data[0]
const oldScore = user?.score || 0
if (oldScore < template.cost) {
await transaction.rollback()
return { code: 40004, msg: '积分不足' }
}
const code = generateCouponCode(template._id)
await transaction.collection('user_coupons').add({
data: {
openid: OPENID,
templateId: template._id,
code,
status: 'unused',
validFrom: template.validFrom,
validTo: template.validTo,
createdAt: db.serverDate()
}
})
await transaction.collection('users')
.where({ openid: OPENID })
.update({ data: { score: _.inc(-template.cost) } })
await transaction.collection('score_logs').add({
data: {
openid: OPENID,
change: -template.cost,
balance: oldScore - template.cost,
type: 'exchange',
source: template._id,
createdAt: db.serverDate()
}
})
await transaction.collection('coupon_templates')
.doc(templateId)
.update({ data: { stock: _.inc(-1) } })
await transaction.commit()
return { code: 0, data: { code, title: template.title } }
} catch (e) {
await transaction.rollback()
throw e
}
}
这里还有一个高并发的小技巧。你可能会想:先查 stock 大于 0,再在事务里 stock: _.inc(-1)。如果两个用户同时看到库存是 1,同时进事务,最终会不会有人兑换失败?理论上事务能锁住,但更稳的写法是直接用条件更新扣库存:
javascript复制const result = await db.collection('coupon_templates')
.where({ _id: templateId, stock: _.gt(0) })
.update({ data: { stock: _.inc(-1) } })
如果 result.stats.updated === 0,说明库存已经没有了,直接返回失败,不需要进事务。这个做法简单高效,推荐在库存判断这个环节用条件更新代替“先查后改”。
4.3 微信支付与商城核销的衔接
积分兑换出的是优惠券,最终要能在商城买单时抵扣,这就会涉及微信支付。如果你做的事情只是线下核销,比如用户到店里给店员看券码,那就简单很多。但朋友需要线上商城能用,所以这里我多写几句。
微信支付现在主要对接 v3 接口,和小程序下单相关的内容有:商户号、APIv3 密钥、API 证书序列号、平台证书、回调地址。小程序端用 wx.requestPayment 拉起收银台,后端用 wechatpay-node-v3 这类 SDK 或者直接封装请求。
几个最容易出问题的点:
第一,签名。v3 的请求签名格式是 Authorization: WECHATPAY2-SHA256-RSA2048,前面的认证算法有固定格式,新手最容易漏掉 Request-Timestamp、Request-Nonce 这些头。所以我不建议手写签名,直接用官方 SDK 或者被广泛验证过的开源 SDK。
第二,回调验签。支付成功后微信服务器会回调你的云函数,这个回调 URL 必须外网可访问。云开发可以用 cloudfunctions 的 HTTP 访问服务来接收回调。回调要严格验签,验签通过后再更新订单状态。不能别人随便 POST 一个假通知就把订单改成已支付。
第三,退款。优惠券抵扣后用户申请退款,要按支付原路径退回。积分抵扣部分要不要退回?业务上建议:如果用户退货,已使用的优惠券不退回积分,但可以返还一张同面额的新券。这个规则要在兑换页写清楚,不然容易引发客诉。
还有一个实际会遇到的情况,就是小程序因为资质或类目问题,支付功能被限制。这个在审核章节我再详细说。
5. 上线避坑实录:审核、支付与那些技术小问题
5.1 小程序审核合规,能提前做的别等被拒
小程序审核是这个项目里最折磨人的环节。知识点答题本身没有太大问题,但一旦涉及到积分、兑换、支付,审核就严格很多。我第一次提审时就被拒了两次,都是在类目和虚拟支付这两个地方卡住。
经验归纳下来有这么几条:
- 类目选择:答题类建议选“教育 > 在线教育”或“工具 > 信息查询”,不要随手选“文娱 > 游戏”。游戏类目需要版号,你只是答题,完全没必要碰。
- 虚拟支付边界:如果用户花钱买的是虚拟内容,比如付费课程、虚拟币,小程序内不能直接用微信支付,这是明令限制的。你这个模式是答题得积分、积分换优惠券,优惠券抵扣的是实物文创产品,走的是实物商品交易,类目上就不要出现任何“虚拟服务”字眼。
- 用户协议:积分规则、兑换规则、有效期、客服联系方式,建议都写进用户协议里。审核员会看你的玩法是否清晰,是否有误导用户的地方。
- 题库内容安全:题目内容本身不能涉及历史评价、地域歧视、低俗文化等敏感信息。民间艺术题目也要注意别把“非遗”和“正统”“正宗”这类词绑定成唯一正确答案,容易引发争议。
如果你的小程序真的收到“支付功能暂时无法使用”这类处罚通知,不要慌。先读站内信,找到违规原因,对应修改类目、用户协议、商品描述,然后在开发者后台提交申诉。处理周期大概 3 到 7 个工作日。这个时候要当作最高优先级来对待,因为支付一停,商城基本就瘫痪了。
5.2 高频技术问题速查
开发过程中遇到的问题,我整理成了下面这个速查表,都是我实际踩过或者帮身边人排查过的:
| 问题 | 原因分析 | 解决方法 |
|---|---|---|
| 微信支付 v3 报 401 无权限 | 证书序列号填错或商户号不匹配 | 检查商户号、APIv3 密钥、证书序列号是否对应同一主体 |
| 小程序审核被拒,提示“涉及虚拟支付” | 商城存在虚拟服务描述 | 删除虚拟服务词条,只保留实物商品交易 |
| 输入答案时键盘遮挡题目 | 页面没有适配软键盘高度 | 设置 adjust-position 和 cursor-spacing,给底部按钮留出安全区 |
| 小程序页面标题不生效 | 动态设置时机不对 | 用 wx.setNavigationBarTitle 并在 onLoad 或接口回调成功后调用 |
| 部分安卓机导航栏过高 | 没有适配胶囊按钮 | 用 wx.getMenuButtonBoundingClientRect() 动态计算自定义导航栏高度 |
| 用户重复提交答案刷积分 | 会话状态没有锁定 | 判分完成后立刻把 status 改为 finished,并在服务端做幂等校验 |
| 优惠券兑换后列表看不到 | 查询条件只查了 status=unused,但已过期券没算进去 |
用户券列表按“未使用/已使用/已过期”分组查询 |
键盘遮挡问题是最容易忽略的,尤其在答题页。题目较长、选项多时,用户在页面底部点击选项,输入法弹出来很容易把选项遮住。设置 cursor-spacing="20" 并且把整个页面加上 scroll-view,基本能解决。
动态标题在答题结果页很实用。用户提交完答案,可以设置“本次得分 80,累计积分 320”作为标题,让微信聊天列表里的会话卡片显得更有信息量,间接带来一点分享效果。
5.3 别把核心逻辑放在前端,安全是靠后端守住的
很多新手做小程序有个误区:把判分和积分逻辑放在前端 JS 里,以为压缩代码别人就看不懂。实际上小程序前端代码在用户手机上是可以被分析和调试的,客户端的一切数据都不可信。
我这套方案里,哪怕用户非常精通小程序逆向,他能拿到的也只是题目和选项,因为答案从始至终只存在于云函数和 quiz_sessions 集合里。他可以在开发者工具里改内存里的分数,但提交给云函数的参数永远是那些题目的答案,不对就是不对。
为了避免有人通过直接调云函数接口刷积分,还需要做一件事:请求参数完整性校验。简单做法是云函数内对 openid 和业务参数做组合校验,比如同一个 quizId 只允许判分一次,同一个 openid 一天内的 startQuiz 调用次数限制在 50 次以内。这个限制不用太复杂,防住脚本批量刷就够。
另外提一句,题库的 answer 字段在数据库里的权限要设置成“仅云函数可读写”,用户端哪怕是创建者也读不了。云开发控制台里集合权限选“所有用户不可读写,仅云函数可读写”,这样最安全。
6. 冷启动运营与迭代方向
6.1 冷启动:从哪找种子用户
小程序做完只是第一步,没有用户,一切都白搭。朋友那边冷启动主要靠三条线,效果还不错:
第一,景区门店扫码。线下展柜、收银台、海报上都放了小程序码,写着“答对 5 题领 20 元文创优惠券”。扫码即玩,玩完即兑换,线下场景转化率非常高,因为用户本来就在门店里,券马上能用。
第二,本地社群和公众号。把题目“每日一题”做成图文推送到公众号,文末放小程序链接,既能科普知识,又能导流。
第三,跨界合作。找当地的学校、社区做非遗知识问答赛,线下比赛用这个小程序统一答题,大屏实时显示得分,既是一个活动工具,又是一个拉新渠道。
这里我想提醒一点:不要把优惠券面额定得太高。满 99 减 20 这种是合理设置,能拉动客单价,又不会让用户感觉“羊毛不够大”。如果你发的是无门槛 20 元券,很快会被一群专门薅羊毛的用户刷走,成本失控。
6.2 数据复盘与后续迭代方向
运营两周后,我开始看数据,重点指标有三个:答题完成率、积分兑换率、优惠券核销率。
答题完成率低,说明题库太难或太长,可以把每局题数从 10 道改成 5 道,或者增加更多简单题。积分兑换率低,说明积分规则或券的吸引力不够,要么降低兑换门槛,要么加大优惠力度。优惠券核销率低,说明用户兑换完就忘了用,这时候要做的是到期提醒,在券过期前 3 天通过订阅消息推送给用户。
后续我建议可以加这些功能:
- 排行榜。按本周积分排个名次,前 50 名给额外奖励,能明显提升答题活跃度。
- 错题本。把用户答错的题单独收藏,方便反复练习,也为主题挑战赛提供依据。
- 用户投稿题目。用户可以上传自己出的民间艺术题,审核通过后奖励 50 积分,这是低成本扩充题库的好办法。
- 专题挑战赛。按主题做周期活动,比如“剪纸周”“年画月”,配合当期主推的文创产品,形成运营节奏。
我个人做完这个项目最大的体会是:答题只是钩子,积分是中间激励,真正让用户留下来的,是兑换后的那个实物文创产品能不能打动他。所以迭代的重心不要全放在技术功能上,要花更多精力去选品、定优惠力度、优化兑换体验。技术上的随机出题、积分、兑换,其实都是标准能力,难的是让用户觉得“值”。
如果你是从零开始,我建议先跑通“随机出题-判分-积分入账-兑换发券”这条主链路,再去做支付、排行榜这些延伸功能。主链路跑通了,这套小程序就像有了骨架,后面怎么长肉都行。
