去年国庆回家,我妈接了个电话差点转了五万八。对方冒充某电商客服,说她买的母婴用品有重金属超标,要给她退款,结果一步步套出了银行卡信息。那一刻我才意识到,反诈宣传就算天天挂横幅、贴海报,也敌不过骗子在电话那头一句精心设计的话术。
后来社区负责宣传的同事找到我,说想做一个能装进口袋里的“反诈宣传站”:老百姓打开微信就能看到最新案例、骗术提醒,最好还能自己输入一段可疑话术让机器帮忙判断风险。于是就有了这个项目——后端用Python + Flask,前端用uniapp开发微信小程序,构建一个反诈知识科普宣传网站。整套系统已经跑起来两个多月,本文把从选型到上线全过程记录下来,尤其是那些没人写在小程序文档里的坑,希望给有类似需求的朋友一份可直接参考的实操笔记。
1. 为什么是flask+uniapp,而不是重武器全家桶
1.1 后端选型:Flask的“度”到底在哪里
做这个项目之前,我也认真纠结过技术栈。当时摆在面前的有三个选择:Django、Spring Boot、Flask。Django确实自带一整套admin后台和ORM,但如果只是做一个内容展示型的小程序后端,它的体积还是大了。Spring Boot就更不用说,光开局一个Maven工程、写一个HelloWorld的时间,Flask这边数据库表都建完了。
我的判断标准很简单:这个项目的本质是什么?是内容管理 + 内容查询 + 一个关键词风险识别接口。没有复杂的用户权限体系,不需要高并发,也不需要分布式事务。所以Flask的“轻”就是优点。Flask官方文档写得清楚,它可以在几行代码里就把路由、请求、响应玩明白,配合SQLAlchemy操作SQLite数据库,一个人在一个星期内就能把所有后台接口写完。
当然,选Flask不等于把所有代码都塞进一个app.py。项目上线后发现,凡是那种一百行以上还放在一个文件里的Flask应用,维护起来都让人头疼。后面会讲我是怎么组织结构的。
1.2 小程序端为什么用uniapp,而不是原生WXML
大家可能会有疑问:既然目标是微信小程序,直接用微信原生语法不就行了,为什么要套一层uniapp?
最直接的原因是我考虑到这套系统未来可能会有多个出口。社区做反诈宣传,不只有微信小程序一个场景,后面很可能还要做一个H5版本,方便放在公众号菜单栏里直接打开,甚至打包成一个安卓App在社区服务中心的展示屏上运行。uniapp的核心价值是“一次编写,多端运行”:一套Vue语法的代码,可以编译成微信小程序、H5、App。现在写好小程序,将来转H5和安卓,前端代码几乎不用重写。
还有一点是开发体验。写过原生微信小程序的都知道,它的setData、生命周期、事件绑定都自成一套体系,写起来倒不复杂,但和Vue的响应式思维差别很大。如果你已经熟悉Vue,还不如直接用uniapp来得顺手。
1.3 技术栈坦白说:这个选择有什么代价
选uniapp也不是没有代价。它在自定义组件、原生控件支持上,偶尔会跟原生平台“打架”。比如我在做视频播放这一块时就踩过坑,后面会专门讲。另一个问题是uniap的社区资料虽然多,但单位团队里的同事要上手,还是得先熟悉Vue 3语法。
不过对于反诈科普这个场景,uniapp的代价完全可以接受。因为页面类型非常标准:列表页、详情页、表单页、富文本展示页,没有复杂动画和底层能力要求。把复杂的东西先不碰,把内容和服务做好,才是这个项目的第一优先级。
2. 先把内容盘活:反诈内容模型与数据结构设计
2.1 内容实体讲清楚:文章、分类、话术规则、题库
很多开发者的习惯是拿到需求就开始写接口,但在这个项目里,最大的工程难点其实不在于接口,而在于内容的组织。反诈知识不是几篇新闻稿就能说清楚的,它包含案例、防骗锦囊、骗术话术、自测题等不同类型的内容,如果数据结构不提前设计好,后面改起来就是无底洞。
我最终把核心内容拆成了四个实体:
- 文章(article):包括媒体报道的典型案例、公安机关发布的警示文章、我们自己编写的防骗指南。字段包括标题、封面图、分类ID、正文、来源、发布时间、浏览量。
- 分类(category):对应“冒充公检法”“刷单返利”“杀猪盘”“虚假投资”“冒充客服”“游戏交易诈骗”“婚恋交友诈骗”“代办证件诈骗”等一级分类。每个分类带图标,前端首页按图标展示。
- 话术规则(keyword_rule):这是风险识别功能的数据基础。每条规则记录一个诈骗分类对应哪些关键词,以及命中后的风险等级和提示文案。
- 自测题(quiz):题库里放选择题,每道题有选项、正确答案和解析,用来做“防骗能力自测”。
2.2 为什么用“话术流派”来组织分类
在整理数据的时候,我发现了反诈内容的一个特点:骗子的手法虽然千变万化,但话术是有“流派”的。比如“刷单返利”类的话术,基本上绕不开“刷一单返X元”“操作简单、时间自由”“需要先垫付”这几个关键词;“冒充公检法”类的话术必然会出现“你涉嫌洗钱”“转账到安全账户”“不要联系任何人”。
基于这个观察,关键词识别接口才能设计得比较简单。它不是靠“猜”,而是靠“统计命中”。数据库中保存的每一类诈骗的话术特征,本质上是一张“骗术特征词典”。这样做的准确率比想象中高,因为诈骗话术为了让你信,必须把信息说得很具体,所以关键词特征反而明显。
2.3 SQLite表结构设计与建表代码
我最终选择了SQLite作为数据库,原因有两条:第一,数据量不大,同类反诈网站日活几百到几千,SQLite完全扛得住;第二,SQLite是单文件数据库,备份和迁移只要拷贝一个文件,这对不懂数据库运维的维护人员特别友好。
用Flask-SQLAlchemy建表的代码如下:
from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class Article(db.Model): __tablename__ = 'article' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) cover = db.Column(db.String(300), default='') category_id = db.Column(db.Integer, db.ForeignKey('category.id')) content = db.Column(db.Text, nullable=False) source = db.Column(db.String(100), default='') views = db.Column(db.Integer, default=0) created_at = db.Column(db.DateTime, default=datetime.now) class Category(db.Model): __tablename__ = 'category' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) icon = db.Column(db.String(200), default='') sort = db.Column(db.Integer, default=0) class KeywordRule(db.Model): __tablename__ = 'keyword_rule' id = db.Column(db.Integer, primary_key=True) category = db.Column(db.String(50), nullable=False) patterns = db.Column(db.Text, nullable=False) # 关键词用逗号分隔 risk_level = db.Column(db.String(10), default='high') suggestion = db.Column(db.Text, default='')关键字规则表单独拿出来做,目的就是让运营人员可以在后台不断补充新话术。骗术是不断翻新的,如果关键词写死在代码里,那功能上线一个月就废了。
3. Flask后端:路由、API与关键词风险识别
3.1 Flask应用骨架与模块划分
如果只是把路由写在app.py里,这些路由也能跑起来,但后期的维护性会很差。我用的是工厂模式加蓝图的方式,结构分三层:
- app.py:应用入口,创建Flask实例并注册蓝图。
- models/:数据库模型文件。
- routes/api.py:所有API路由。
app.py的骨架如下:
from flask import Flask from flask_cors import CORS from models import db from routes.api import api_bp def create_app(): app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///fanzha.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) CORS(app) app.register_blueprint(api_bp, url_prefix='/api') with app.app_context(): db.create_all() return app app = create_app() if __name__ == '__main__': app.run(host='127.0.0.1', port=5000, debug=True)注意,生产环境下Flask开发服务器不能直接对外,后面部署章节我会说明怎么用gunicorn加Nginx跑起来。
3.2 几个核心API的设计逻辑
这套系统的API不多,但每个接口都有明确用途:
- GET /api/categories:返回所有诈骗分类,首页展示入口。
- GET /api/articles?category=&page=:按分类分页返回文章列表,首页默认返回全部。
- GET /api/articles/<id>:返回文章详情,并给浏览量加一。
- POST /api/risk/check:接收一段文本,返回匹配到的风险提示。
- GET /api/quiz/random:随机返回一道自测题。
以文章列表接口为例,这里有一个容易忽略的细节:列表页不需要返回文章全文,只要返回标题、封面、发布时间和浏览量。这样能大幅减少接口传输体积,尤其是小程序端对流量比较敏感。
3.3 关键词风险识别:为什么不用复杂NLP
风险识别是本项目最有意思的功能。用户在小程序里输入一段可疑短信或聊天话术,然后后端返回“这可能属于XX类诈骗,风险等级高,建议不要转账并立即拨打96110咨询”。
最初我也想过用NLP文本分类模型,比如BERT之类的。但深入一想就放弃了:这个场景下样本量太少,标注成本高,模型训练周期长,一个小型宣传站扛不起维护成本。真正的需求不是区分“这段话有没有恶意”,而是识别“它是否命中了已知的诈骗话术特征”。
所以我采用的就是简单高效的“关键词命中”方案。核心逻辑如下:
from models import db, KeywordRule def check_risk(text): if not text or len(text) > 600: return {'code': 400, 'msg': '文本长度需在600字以内'} hits = [] for rule in KeywordRule.query.all(): for kw in rule.patterns.split(','): kw = kw.strip() if kw and kw in text: hits.append({ 'category': rule.category, 'keyword': kw, 'level': rule.risk_level, 'suggestion': rule.suggestion }) break return {'code': 200, 'data': hits}实测下来的体验是:只要关键词库维护得当,这个方案在反诈场景的准确率非常高。因为骗子的目的就是让人按他的指令操作,话术里充满“转账”“安全账户”“信用额度”“验证码”这类指向性明确的词。真正的难点不在算法,而在于关键词条目的持续更新。
在此也要说明一个边界:这种识别只能作为辅助参考,不能用来判断任何真实案件,所以在接口返回的文案里一定要有“结果仅供参考,如有疑问请拨打反诈专线或向属地派出所咨询”的提示。
4. uniapp小程序端:页面结构、请求封装与视频科普
4.1 从HBuilderX创建项目到页面划分
小程序端用的是HBuilderX + uniapp + Vue 3的方式创建项目。项目建好以后,我规划的页面结构是这样的:
- pages/index/index 首页:轮播图、分类图标入口、最新骗术案例列表。
- pages/list/list 分类列表页:点击首页分类图标跳转,展示该分类下的文章列表。
- pages/detail/detail 文章详情页:展示富文本内容,支持视频组件。
- pages/check/check 话术识别页:输入文本,调用风险识别接口。
- pages/quiz/quiz 自测页:随机出题,判断防骗能力。
- pages/mine/mine 我的:包含举报指引、关于本站、反馈入口。
这样的页面划分完全对应用户的浏览路径:先看最近发生了什么事,看到感兴趣的内容点进去深入了解,主动查一查,再测一测自己能不能识别骗局。
4.2 request请求封装与环境切换
小程序端最基础的封装是请求工具。最开始我直接在每个页面里写uni.request,写了三个页面之后就发现重复代码太多,而且改一个域名要翻遍所有页面。后来我把请求封装成了一个独立的模块:
// utils/request.js const BASE_URL = 'https://api.example.com' export function request(path, options = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + path, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.data.code !== 200) { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) } resolve(res.data) }, fail: (err) => reject(err) }) }) }这里想特别讲一个很多人问的问题:uniapp开发时怎么切换多个域名?我们在开发阶段要连局域网里自己电脑的Flask服务,测试阶段要连测试服务器,上线之后要连正式域名。
我的做法是用环境配置文件,不把域名写死。在项目根目录的config目录下新建一个环境变量文件,里面根据uni-app的编译条件判断是开发还是生产:
// config/env.js let baseURL = 'https://api.example.com' // #ifdef H5 // #endif // #ifdef MP-WEIXIN baseURL = 'https://api.example.com' // #endif export { baseURL }这样改域名只需要改一个文件,编译到哪个端就走哪个域的配置。微信开发者工具里可以在“详情-本地设置”勾选“不校验合法域名”,这样开发时就可以直接在工具里请求本地的局域网接口,方便很多。
4.3 视频科普的处理与一个坑
反诈内容里很重要的形式是短视频,比如公安机关发布的案例解析视频。小程序里播放视频,第一反应是用web-view嵌入网页,实际上这是个大坑。web-view里播放视频,用户体验差,而且在微信小程序里有相当多的限制,加载速度也不可控。
正确做法是用微信小程序的video组件。uniapp里直接用<video>标签就可以,src指向视频文件的 HTTPS 地址。这里有两个注意点:
一是视频文件要放在对象存储或者CDN上,不能放在Flask的静态目录里,否则服务器带宽会被打满。二是视频域名必须加到微信公众平台的“开发管理-服务器域名-downloadFile合法域名”里,否则在真机上无法加载。
还有一个最容易被忽略的问题:微信小程序的video组件没有提供直接的“下载视频”按钮。如果有用户需要保存视频到相册,就得自己实现uni.downloadFile逻辑,而且这个操作需要用户授权。在反诈科普场景,我最终选择不开启下载,因为视频内容主要目的就是看,下载下来反而容易被转发传播成不完整的信息。
4.4 自定义分享:让用户把防骗锦囊发给亲友
反诈宣传的核心动作是“传播”。小程序最有价值的能力之一是用户可以把内容分享给微信好友、微信群。这里我用了uniapp的自定义分享接口onShareAppMessage:
export default { onShareAppMessage() { return { title: '今天提醒:收到这种“客服退款”信息,千万别点链接', path: '/pages/detail/detail?id=120' } } }这样写之后,用户从列表页点分享,分享出去的就是一条指向具体防骗内容的卡片。实测分享率确实比默认文案高不少,因为默认的“分享”两个汉字没有人会去点。标题要直接告诉别人“这条内容跟我有关,看了能避免被骗”。
5. 从本地跑到线上:发布、域名、审核与连踩三坑
5.1 生产环境部署:gunicorn + Nginx + SQLite
Flask开发服务器直接对外撑不住并发,生产环境我用的是gunicorn当WSGI容器,Nginx做反向代理,后端进程监听本机的5000端口。
在服务器上安装好Python环境和依赖后,启动命令是:
gunicorn -w 2 -b 127.0.0.1:5000 app:app然后配置Nginx把80和443端口转发到5000端口。Nginx配置的关键部分如下:
server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里必须强调的是:微信小程序端的request请求要求必须是HTTPS,而且域名必须已经备案。所以不管是服务器购买、域名备案,还是SSL证书申请,都要提前做。证书我用的是Let‘s Encrypt的免费证书,三个月续期一次,配个定时任务就搞定了。
5.2 小程序提审前的检查清单
小程序功能写完之后,提审之路才是最容易让人抓狂的地方。我总结了一套自己的检查清单:
- 类目选择:反诈科普属于内容科普性质,可以选择“教育-教育信息服务”或“工具-信息查询”,最终以后台类目说明为准。
- 隐私协议:小程序端如果没有收集手机号、位置等隐私信息,要在开发者后台的“用户隐私保护指引”里如实填写。如果后期加了举报反馈表单,涉及用户填写手机号,就必须在前面加一个授权协议弹窗。
- 内容安全:反诈文章和用户提交的识别文本,建议接入微信的内容安全接口做检测。后端在接收到文本时可以通过安全接口过滤一遍,尽量避免内容出现违规风险。
- 域名配置:确保request合法域名、downloadFile合法域名都已经添加,且都是HTTPS。
我踩的第一个坑就是忘记添加downloadFile合法域名,结果真机上所有视频都黑屏,只有开发工具里正常。当时排查了半天,最后才发现是域名配置的问题。这个问题审核阶段的测试人员很容易触发,因为他们的网络环境更接近真实用户。
5.3 微信小程序年审与账号运营
小程序不是上架就一劳永逸的。微信公众平台每年都要做一次年审,年审过程中会重新审核小程序的类目、页面功能、隐私协议等。这个时间节点要提前在日历上标出来,很多站点都是因为年审超时,小程序被暂停了部分能力,用户访问量突然下滑。
另外,当小程序累计访问用户达到一定量级后,平台的审核会更关注内容的时效性和安全性。反诈内容是滚动更新的,需要有固定的更新节奏,建议每周至少更新三到五篇新的案例通报或者防骗手段变化。小程序要做到“让用户每次打开都有新东西可看”,不然用户很快就把这个小程序遗忘在“最近使用”的列表底部了。
5.4 顺带提一下:以后要上安卓应用市场该注意什么
为什么选uniapp的后端价值在这里体现出来了,因为同一套代码可以直接打包成安卓App。但安卓应用市场(包括各大手机厂商的应用商店)审核要求比微信小程序更严格。上架之前必须要有计算机软件著作权登记证书,这是基本门槛。
另外安卓市场现在普遍要求隐私合规检测报告和大概的安全检测报告。反诈类App在权限申请上一定要克制,不要申请通讯录、短信、定位等与功能无关的权限,否则被拒一次再重新上架会浪费几个星期的时间。
6. 如果再来一次,我会设计得更完善的功能
6.1 后台管理:给运营人员一个增删内容的入口
项目上线之后最大的感受就是:内容更新的压力全在开发身上。每次要更新一篇反诈案例通报,我都要登录服务器,用命令行操作数据库,要么就在Flask里写个临时脚本导入数据。这种做法不可持续。
如果重来一次,我会直接加上Flask-Admin,或者做一个简单的后台页面。后台不需要复杂,只要能登录、能添加文章、能修改关键词规则表、能上传封面图片、能定时下线过期内容,就够用了。运营人员自己更新内容,开发就能彻底解放出来。
6.2 内容时效性提醒与订阅消息
反诈内容的生命周期其实很短。一条“新骗术预警”在当周可能浏览量很高,但过了两周就成了过时信息。数据库里如果一直堆着旧内容,不仅占地方,用户翻列表时还会觉得“这东西我早就看过了”。
第二个版本我会增加两个机制:一是文章下架逻辑,定期清理超过六个月的“通告类”内容;二是短信订阅消息提醒,当后台发布了高等级预警内容时,可以通过小程序订阅消息通知关注了该分类的用户。微信小程序的订阅消息需要用户主动订阅,而且一次订阅只能发一次通知,所以要设计得克制一些,只在真正的“高危预警”时使用。
6.3 关于用户反馈与举报入口的真实想法
最后想聊一个容易被误解的功能设计:举报入口。
很多反诈科普平台都想在小程序里直接加“立即举报”按钮,让用户提交线索。这个初衷是好的,但小程序没有能力直接接入本地公安系统,如果用户填了举报信息,我们这些开发者也无法保证信息安全流转。所以我最终的方案是:把举报入口做成“指引页”,告诉用户该通过什么渠道去举报,比如前往派出所报案、拨打反诈专线、下载国家反诈中心App在线举报。这样既履行了宣传引导职能,又避免了因为私自接收敏感信息带来的合规风险。
上线两个多月后,我后台看数据时最有感触的一个现象是:调用风险识别接口最多的分类是“刷单返利”和“冒充客服”,而文章详情页浏览量最高的是“杀猪盘案例”。这说明大家主动来查的,往往是身边正在发生的真实威胁;而看案例最多的,通常是听过但还没在现实中遇到的骗局。反诈科普这件事,重要的不是改变所有人的态度,而是在转账之前多给一次犹豫的机会。只要有一次因为这个小程序里的某个提示,让一个人停下来按了一下拨号键,这个项目就有意义。