2023我的年度最佳之选——答题小程序

2023我的年度最佳之选——答题小程序

开发者广场内容精选NaN-NaN-NaN
解决方案
作者:潘龙(phiron.pan)
推荐理由
和大家分享一下我在飞书制作的一个答题小程序,让我们在知识的海洋尽情享受吧~
一、关于我
hello大家好,我是一位全栈开发工程师,喜欢折腾和学习新技术。今天我给大家分享一下我在飞书制作的一个答题小程序,让我们在知识的海洋尽情享受吧~
二、需求分析
公司年终活动即将来临,为了庆祝新的一年的到来,我们决定推出一款精彩的答题小程序,让大家在欢笑声中检验今年所获得的知识,并赢取丰厚的奖品!
三、方案调研
我们选择在飞书平台上进行答题,考虑到大家都在飞书中工作,这将是一个便捷而有趣的方式。我们特意自建了一个飞书小程序,不仅可以免去繁琐的登录步骤,而且还能充分利用飞书的分享功能,让活动更加流畅而互动性更强。让我们一起参与这场年终答题,共享奖品和欢乐,为新的一年开个好头!
四、开发流程
小程序应用整体效果是满满科技感十足的暗黑风格,特别适合it主题。
1.免登录流程,打开应用即可自动登录,可以参考我之前发过的一篇免登录文章哦,链接在此:在飞书从0到1实现开发一个免登录应用是什么体验? - 开发者广场
2.首页长这样,按钮go是有一个动态效果的,那种放大缩小,让人忍不住跃跃欲试。
3.然后是规则弹窗页面,默认每次打开应用自动弹出一次,让参与答题的小伙伴更加了解游戏规则。
4.接下来是答题开始后的页面,答题的一个逻辑就是每次从后台生成一个10道题目的记录返回到前端,前端放进缓存保存即可,即使关闭应用退出后,重新打开也可以立即读取缓存,不需要从头开始答题。中间有个小插曲,一开始我设计的是每答一道题就去修改分数和排行榜记录这样特别慢,体验很不好,之后还是想了一下直接最终答题让用户手动去提交这样比较好,这样等用户最终答完题,去提交10道题的答案然后去数据库对比一次保存得分即可,然后排行榜的数据可以异步进行处理,这样就不会让用户等待很久。代码如下:
def update_qa_record(params):
try:
# 创建游标对象
cursor = conn.cursor()
# 查询是否已答完题目
select_sql = "SELECT * FROM h_activity_record WHERE user_id = %s AND activity_id = %s"
cursor.execute(select_sql, (params["user_id"], params["activity_id"]))
one = cursor.fetchone()
if one and one["status"] == 1:
return False
# 计算最终得分保存更新活动记录分数
qa_list = params["qa_list"]
logging.debug("qa_list: %s", qa_list)
ids = []
options_map = {}
# 顺便把每题的用户答案 保存到id->option_key键值对里
# 获取答题的ids组合
for qa in qa_list:
ids.append(qa["id"])
key = ""
for option in qa["answer_list"]:
if option["checked"]:
key += option["option_key"]
options_map[qa["id"]] = key
logging.debug("ids: %s", ids)
logging.debug("options_map: %s", options_map)
all_point = 0
option_result = {}
if len(ids) > 0:
# 创建一个占位符字符串,其数量与 ids 的长度相同
placeholders = ', '.join(['%s'] * len(ids))
# 查询ids的题目 并计算分数
select_sql = "SELECT * FROM h_question WHERE id IN ({})".format(placeholders)
logging.debug("placeholders: %s", placeholders)
logging.debug("select_sql: %s", select_sql)
cursor.execute(select_sql, tuple(ids))
result = cursor.fetchall()
logging.debug("result: %s", result)
# 处理总分数
for re in result:
pa = {
"option_key": options_map[re["id"]]
}
if re["answer"] == options_map[re["id"]]:
pa["quality"] = True
option_result[re["id"]] = pa
all_point += 10
else:
pa["quality"] = False
option_result[re["id"]] = pa
# 更新记录表
update_sql = "UPDATE h_activity_record SET current_id = %s , point = %s, status = 1 WHERE user_id = %s AND activity_id = %s"
cursor.execute(update_sql, (
params["qa_list"][len(params["qa_list"]) - 1]["id"], all_point, params["user_id"],
params["activity_id"]))
logging.debug("all_point: %s", all_point)
logging.debug("option_result: %s", option_result)
# 异步处理答题和排行榜情况
background_thread = threading.Thread(target=update_qa_ranking, args=(params, all_point, option_result))
background_thread.start()
# 提交事务
conn.commit()
# 关闭游标
cursor.close()
# 返回成功状态
return True
except Exception as e:
print(e)
# 发生异常时回滚事务
conn.rollback()
return False
5.然后就是分享和抽奖了,答题满足80分则可以抽奖,未达到则提示分享,展示两种完全不一样的界面效果。
6.抽奖页面,抽奖这块也比较复杂,首先的在数据库设计好奖品奖池和中奖概率,然后抽奖时用户点击抽奖时后端通过计算得出中奖结果并返回给前端,然后前端根据结果做一个小动画渲染抽奖的过程:
后端代码如下:
def get_mini_draw_lottery(user_id):
# 查询当前进行中活动
activity = common_dao.get_started_activity()
data = {
"can": False, # 是否可以抽奖
"sjNum": 0,
"error": False,
"prize": {},
}
if activity is not None:
# 根据活动id查询当前参与记录
# 然后查询h_activity_record表用户是否参与了活动
record = common_dao.query_activity_record(user_id, activity["id"])
if record is not None:
# 判断是否抽过奖 并且分数是否大于80分
if record["point"] >= 80 and record["answer_lottery"] == 0:
data["can"] = True
# 调用抽奖方法 返回中奖的索引
prize_info = prize_dao.start_mini_draw(user_id)
if prize_info['error']:
return prize_info
else:
data["sjNum"] = prize_info["data"]["index_num"]
data["prize"] = prize_info["data"]
return data
def start_mini_draw(userid):
# 创建游标对象
cursor = conn.cursor()
try:
# 开始事务
conn.begin()
# 查询可抽奖的奖品
cursor.execute("SELECT * FROM h_prize WHERE inventory > 0 AND type = 2")
result = cursor.fetchall()
if not result:
conn.rollback() # 回滚事务
return {'error': True, 'msg': "没有可抽奖库存", 'data': None}
# 计算总的中奖概率
total_probability = sum(prize['rate'] for prize in result)
# 生成一个随机数
random_number = random.uniform(0, 1)
# 根据随机数和中奖概率选择中奖的奖品
accumulated_probability = 0
chosen_prize = None
for prize in result:
accumulated_probability += prize['rate'] / total_probability
if random_number <= accumulated_probability:
chosen_prize = prize
break
if chosen_prize and chosen_prize['inventory'] > 0:
# 更新奖品库存
cursor.execute("UPDATE h_prize SET inventory = inventory - 1 WHERE id = %s", (chosen_prize['id'],))
# 修改个人中奖为1 已参与抽奖
# 执行更新操作
update_sql = "UPDATE h_activity_record SET answer_lottery = %s WHERE user_id = %s"
cursor.execute(update_sql, (1, userid))
# 把抽奖记录添加到记录表
# 否则执行插入操作
# 谢谢惠顾不保存
if chosen_prize['name'] != '下次好运':
insert_sql = "INSERT INTO h_prize_record (user_id, prize_id, prize_name, prize_url, create_date, type) VALUES (%s, %s, %s, %s, %s, %s)"
cursor.execute(insert_sql,
(userid, chosen_prize['id'], chosen_prize['name'], chosen_prize['prize_url'],
datetime.now(tz),
2))
conn.commit() # 提交事务
return {'error': False, 'msg': "", 'data': chosen_prize}
else:
conn.rollback() # 回滚事务
return {'error': True, 'msg': "奖品超出库存", 'data': None}
except Exception as e:
print("e", e)
conn.rollback() # 出现异常时回滚事务
return {'error': True, 'msg': "An error occurred while processing the request", 'data': None}
finally:
cursor.close()
前端关键代码如下:
// 开始抽奖
begin: throttle(function(index) {
if (index == 4) {
if (!this.noClick) {
uni.showToast({
title: '请勿频繁点击',
icon: "none",
position: 'bottom'
});
return
};
this.getLottery()
}
}, 1000),
async getLottery() {
if (this.user.name) {
let params = {
"username": this.user.name,
"open_id": this.user.open_id,
"user_id": this.user.user_id
};
// 使用async/await等待请求完成
try {
const res = await request('/mini/draw/lottery', params, 'POST', { Authorization: this.user.token });
if(res.error){
uni.showToast({
title:res.msg,
icon:'error'
})
return
}
this.readInfo = res.data
this.sjNum = res.data.sjNum
if(!this.readInfo.can){
uni.showModal({
title:"抽奖提示",
content:"很抱歉,您没有获得抽奖机会",
confirmText:"我知道了",
showCancel:false
})
return
}
this.noClick = false;
this.FastNums = 0
this.SlowNums = 0
this.time = 200
this.LoopStatus = true
this.loop()
} catch (error) {
console.log(error);
}
} else {
uni.showToast({
title: "还未登录,请刷新登录",
icon: "none"
});
}
},
// 抽奖过程 控制
loop() {
let sjNum = this.sjNum
console.log(sjNum)
if (!this.sel || this.sel < 9) {
if (this.sel == 3) {
this.sel = 0
} else if (this.sel === '') {
this.sel = 0
} else if (this.sel == 2) {
this.sel = 5
} else if (this.sel == 5) {
this.sel = 8
} else if (this.sel == 8) {
this.sel = 7
} else if (this.sel == 7) {
this.sel = 6
} else if (this.sel == 6) {
this.sel = 3
} else {
this.sel++
}
this.FastNums++
if (this.FastNums == 4) {
this.FastNums = 0
this.time = 50
this.SlowNums++
}
if (this.SlowNums == 8) {
this.SlowNums = 0
this.time = 500
this.FastNums = 5
}
if (this.FastNums > 5) {
if (this.sel == sjNum) {
this.noClick = true;
this.LoopStatus = false
// 成功的逻辑
this.updateMoney(sjNum)
}
}
if (this.LoopStatus) {
setTimeout(() => {
this.loop()
}, this.time);
}
}
},
7.我的页面和排行榜页面,没有什么难度:
8.最后是飞书的分享效果,一开始不熟悉飞书的分享功能,最后问客服才了解清楚,如果图片打不开,则分享时飞书会没反应,所以要确保分享的图片url可以打开,前端代码如下:
onShareAppMessage: function (opt) {
var that = this
console.log(opt);
return {
title: "你收到一个抽奖邀请~",
path: '/pages/index/index?user_id='+that.user.user_id,
PCPath: '/pages/index/index?user_id='+that.user.user_id,
PCMode: 'window-semi',
imageUrl: "https://xxx/file/images/luck.png",
success(res) {
console.log('success', res);
},
fail(errr) {
console.error(errr);
},
};
},
五、更多相关开发心得
开发过程中确实遇到很多问题,多动脑,多转换思路,有遇到不会的问题也可以去网络搜索,或者问chatgpt,实在解决不了,就请教身边的有经验的大佬吧。关于飞书开发的更多问题可以去参考官方文档,也可以直接请教客服哦,客服真的很热情很耐心呢。好了,就到这里了,我们下次再见。如果有更多开发问题,欢迎联系我互相交流哈哈。
先进生产力和业务协同平台
联系我们立即试用
更多人气推荐
查看更多

先进团队,先用飞书

欢迎联系我们,飞书效能顾问将为您提供全力支持
分享先进工作方式
输送行业最佳实践
全面协助组织提效
反馈给飞书 CEO:ceo@feishu.cn