做这个“python鲜而廉水果百科网站微信小程序”的时候,我并不是想做一个正经的生鲜电商。起因很简单:我妈在菜场买水果,总会被摊主一句“今天刚到的,新鲜”打动,但回家一吃又觉得亏。反过来,我的一个朋友在水果店打工,常常为店里当天的榴莲和草莓卖不动发愁。两头的痛点都非常具体——买的人不知道什么价格合理、什么状态算新鲜;卖的人不知道怎么让用户快速判断这批货值不值。
所以这个项目的定位非常明确:用 python 写一套后端服务,负责水果百科数据、多源价格数据和新鲜度评分;用微信小程序做用户界面,让用户在小程序里搜水果、看百科、查价格区间、看新鲜度提示。它不是一个通用商城模板,而是一个带“信息服务”属性的水果工具。接下来我会把整个设计与实现过程拆开讲,包括为什么选 Flask、数据库表怎么设计、价格和新鲜度怎么计算、小程序端怎么对接,以及上线时容易踩的坑。
1. 先想清楚一件事:这个项目到底要解决谁的什么麻烦
1.1 水果交易里的两头信息差
大多数水果类小程序一上来就做下单、支付、物流,结果连商品标准都没建立。我的思路是反过来的:先做信息服务,再考虑交易。买水果的人其实需要三类信息:这个水果是什么、现在这个季节值不值得吃、当前价格有没有买贵。对应到系统里就是百科、产季和价格。如果这三类信息足够准,用户自然会信任,后面再做社区、团购、优惠券才有基础。
“鲜而廉”三个字听起来像广告语,落到系统里其实是两个可计算的指标。鲜不是靠商家自吹,而是用产季、采摘/上架时长、产地距离三个维度做加权评分;廉也不是绝对低价,而是当前价格相对该水果近期市场均价的折扣程度。后面我会详细讲这两个指标的计算,这里先记住一个结论:这个项目的核心不是页面数量,而是数据质量和计算逻辑。
1.2 把“鲜”和“廉”拆成可计算的指标
如果只做展示,这个项目会非常单薄。我在需求分析阶段就把“鲜”和“廉”拆成了公式:
- 鲜度评分
fresh_score = 季节匹配分 * 0.4 + 上架时效分 * 0.35 + 产地损耗分 * 0.25 - 廉值参考
price_ratio = current_price / avg_price_last_30_days - 当
price_ratio < 0.9时标记为“低于近期均价”;0.9~1.1为“正常”;超过1.1为“偏贵”
季节匹配分来自一个月份表,例如荔枝在6月属于产季,得分高;12月基本是反季存储果,得分低。上架时效分取决于商品首次录入或被商家标记的时间,越短越新鲜。产地损耗分根据水果产地到当前城市的距离估算,距离越近损耗越低。这三个分数在小程序里不是直接暴露原始数字,而是转换成“新鲜度很好/一般/建议谨慎购买”的文案,用户不需要理解算法也能用。
举个例子,用户在小程序里搜索“荔枝”,系统拿到当前月份为6月,从季节表查到荔枝产季是5到7月,季节匹配分90;用户所在城市是武汉,而数据库中荔枝的常见产地是广东茂名,距离约1100公里,产地损耗分50;该条商品在上午采集到的价格为8.8元/500g,近30日该城市荔枝均价是10.2元/500g,算出来 price_ratio=0.86,于是后端返回“属于当季水果,价格低于近期均价,整体建议购买”。这一系列判断从请求到返回必须在500毫秒内完成,不然用户在水果摊前等太久就会关掉页面,所以后端的接口缓存和查询索引非常重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选择:为什么是 Python 配微信小程序
2.1 Flask 与 Django、FastAPI 的取舍
这个项目最稳妥的方案是 Python Flask + MySQL + Redis + 微信小程序原生,而不是用 uniapp 或者 Django。很多人会问为什么不直接用 FastAPI?FastAPI 的优势在于异步和高并发,但这个阶段的数据量级不会超过几万用户,性能瓶颈在数据采集和数据库查询,不在接口框架本身。Django 带了很多用不上的组件,后台管理、ORM、模板、middleware、auth,对一个百科加价格查询类应用来说太重,部署和内存开销也更大。Flask 足够轻,上手快,而且从最简单的 app.py 到模块化蓝图都可以平滑演进,非常适合这个项目。
我实际的项目结构这样组织:
text复制fruit_project/
├── app.py # Flask 入口
├── config.py # 配置项:数据库、密钥、外部接口
├── models.py # SQLAlchemy 模型
├── api/ # 蓝图:auth, fruit, price, user
├── services/ # 业务逻辑:新鲜度、价格计算、采集任务
├── spider/ # 价格采集与解析
└── requirements.txt
这个结构不是一开始就有的。我最早把所有路由都塞在 app.py 里,后来水果种类超过50个、接口超过20个之后,文件变得不可维护,才拆成蓝图。如果你也是从单体开始,建议至少把 models、api 和 services 分开,后面不用返工。特别是 services 层负责新鲜度计算和价格聚合,把计算逻辑分离出来之后,写单元测试或者换接口框架都很方便,不会被路由代码干扰。
2.2 小程序端与服务端的接口约定
小程序端和 Python 后端是两个独立部署的应用,接口约定必须一开始就定好。我统一使用 JSON 传输,所有返回结构固定为:
json复制{
"code": 0,
"msg": "success",
"data": {}
}
code 为 0 表示成功,非 0 表示错误。错误码分段管理,例如 100x 表示用户相关,200x 表示水果数据相关,300x 表示价格相关。分页接口统一接收 page 和 pageSize,返回 list、total、has_more 三个字段。时间字段统一使用毫秒时间戳,避免前后端时区不一致。
为什么不用更简单的直接返回数组?因为小程序端在统一处理登录过期、网络错误、业务错误时,需要从同一个位置读取状态码。如果数据结构不统一,每次请求都要写一遍繁琐的错误处理代码。我见过很多项目每个接口返回格式都不同,前端请求层越写越乱,这本来是可以用约定解决的问题。后期加活动、加收藏功能时,只要后端保证 {code, msg, data} 不变,前端请求层几乎不用动。
2.3 数据库表结构设计:从用户到水果到价格记录
数据库我使用 MySQL 8.0。起初想过用 SQLite 省事,但考虑到价格数据会持续写入、用户收藏也会增长,SQLite 的写入锁会越来越难受,所以直接上 MySQL。核心表有5张:
user:微信用户,字段包括id, openid, nickname, avatar, city, created_at。fruit:水果主数据,字段包括id, name, alias, category, season_month, origin, description, tips。fruit_price:价格记录,字段包括id, fruit_id, source_type, source_name, price, unit, city, recorded_at。fresh_rule:新鲜度评分配置表,存放季节匹配分、时效分权重等。favorite:用户收藏表,字段包括id, user_id, fruit_id, created_at。
用 SQLAlchemy 定义模型时,注意给 fruit_price 的 recorded_at 和 fruit_id 建联合索引,因为“查某水果近30天平均价格”是最高频的查询。示例:
python复制class FruitPrice(db.Model):
__tablename__ = 'fruit_price'
id = db.Column(db.Integer, primary_key=True)
fruit_id = db.Column(db.Integer, nullable=False, index=True)
source_type = db.Column(db.String(20), default='manual')
source_name = db.Column(db.String(100))
price = db.Column(db.Numeric(10, 2))
unit = db.Column(db.String(10), default='500g')
city = db.Column(db.String(50), index=True)
recorded_at = db.Column(db.DateTime, index=True)
价格表单独拆出来非常重要。如果只在 fruit 表里放一个 current_price 字段,每次采集更新都会覆盖历史,后面想算30日均价就得另建表。把价格做成一叠记录,随时可以按时间窗口聚合,这设计就灵活多了。
3. 数据从哪来:百科内容、价格采集与新鲜度模型
3.1 水果百科内容的人工整理与半自动抓取
百科是这个项目的信息底座。我最初从网上找了一份常见水果清单,按“名称、别名、主要产地、成熟月份、挑选技巧、保存方法、热量”等字段整理成 CSV,导入数据库。后来再用 Python 脚本从几个公开的水果科普网站抓取补充描述,但不是全文复制,而是提取关键句后做人工校对。这里要特别注意版权问题,百科文案不能大段照抄,我采取的是“结构参考+自己改写”的方式,确保内容可发布。
在小程序端,百科页展示的信息分为基础信息区和实用信息区。基础信息包括产地、产季、热量;实用信息包括怎么挑、怎么存、什么状态不能买。为了不让用户觉得像是在看说明书,我加了一个“当前季节建议”的区块,直接调用新鲜度评分结果:产季内显示“正当时,可以多吃”,反季显示“目前多为存储果,购买时注意看果柄和果皮状态”。这个区块虽然实现起来只有几行代码,但用户反馈最好,因为大家真正想知道的是“我现在该不该买”。
3.2 多源价格采集与“廉”的计算
价格数据是整个项目最花精力的一部分。我接了三个来源:一个公开批发市场行情网站、两三个本地商超的小程序接口(通过正常商品详情页获取公开价格)、以及用户上报。前两个来源用 Python 的 requests + BeautifulSoup 定时抓取,但是频率不能太高,我设置成每天早上8点和下午2点各跑一次,避免给目标站点造成压力,也能覆盖上午和下午的价格变化。
采集脚本的大致逻辑如下:
python复制def fetch_price(fruit_name, source_url):
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(source_url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")
# 这里按具体网页结构解析价格节点,不同来源写不同解析器
prices = parse_price_nodes(soup)
for price_item in prices:
save_price_record(fruit_name, price_item, source_type="spider")
解析器要写得很“防御”。因为目标页面经常改版,我加了一层字段异常校验:如果解析出的价格不在合理区间,例如每500克0.1元或9999元,直接丢弃并写入日志,等下次人工检查。廉价指标不直接用当天某一家的价格,而是用近30天均价作为基准,计算公式如下:
python复制def calc_price_ratio(fruit_id, current_price):
avg_price = db.session.query(func.avg(FruitPrice.price)).filter(
FruitPrice.fruit_id == fruit_id,
FruitPrice.recorded_at >= datetime.now() - timedelta(days=30)
).scalar()
return round(float(current_price) / float(avg_price), 2) if avg_price else None
用户上报的价格同样进入价格表,但在前端会标记为“用户最近看到的价格”,不参与均价计算,防止有人乱填造成数据失真。等上报量上来了,再加入异常值剔除逻辑。
3.3 新鲜度模型:把经验变成规则引擎
新鲜度是最容易做成“玄学”的部分。我实现了一套简单的规则引擎,避免把评分写死在业务代码里。Python 后端有一个 fresh_score(fruit_id, city) 服务函数,每次用户访问水果详情页时动态计算,并对结果做15分钟缓存。
规则配置放在 fresh_rule 表里,允许运营人员调整权重。例如6月份荔枝的季节匹配分是90,而12月份只有20;产地距离损耗分的基准是:同城100分,500公里以内80分,500到1000公里60分,超过1000公里40分。这套规则在初期完全不依赖机器学习,因为样本量不支持模型,而且用户要的只是一个可解释的“建议”。规则引擎的好处是每条分数都能给出理由,比如“荔枝正值产季,产地距离300公里,鲜度较好”,用户觉得靠谱,后面调整起来也简单。
规则引擎的核心是一个配置驱动函数,大概是这个思路:
python复制def calc_fresh_score(fruit, city_name):
season_score = get_season_score(fruit.id)
shelf_score = get_shelf_score(fruit.id)
distance_score = get_distance_score(fruit.origin, city_name)
score = season_score * 0.4 + shelf_score * 0.35 + distance_score * 0.25
if score >= 80:
return {"score": round(score), "text": "新鲜度很好"}
if score >= 60:
return {"score": round(score), "text": "新鲜度一般"}
return {"score": round(score), "text": "建议谨慎购买"}
分数只作为内部依据,对外主要输出文案,这样即使用户看到“建议谨慎购买”也不会觉得是算法出了问题,反而会觉得系统真的在为他考虑。
4. 小程序端开发与 Python 后端的联动
4.1 微信登录态处理:code2session 与自定义 token
小程序端不能直接用微信返回的 openid 请求后端,更不能把 session_key 下发到前端。标准做法是:
- 小程序
wx.login()获取临时 code。 - 将 code 发送到 Python 后端的
/api/auth/login。 - 后端用 code 调用微信接口
code2Session,换取openid和session_key。 - 后端生成自己的 token,我用
itsdangerous签名,也可以用 JWT,将 token 返回小程序。 - 小程序后续请求在 header 中带
Authorization: Bearer <token>。
这里最容易被忽略的是 token 过期时间。我设置7天过期,过期时返回错误码 1003,小程序端拦截到该错误后自动重新执行 wx.login,用户无感知。
Flask 端代码大致如下:
python复制@app_api.route('/auth/login', methods=['POST'])
def login():
code = request.json.get('code')
res = requests.get('https://api.weixin.qq.com/sns/jscode2session', params={
'appid': app.config['APP_ID'],
'secret': app.config['APP_SECRET'],
'js_code': code,
'grant_type': 'authorization_code'
}).json()
if 'openid' not in res:
return jsonify({'code': 1001, 'msg': '登录失败', 'data': None})
user = find_or_create_user(res['openid'])
token = generate_token(user.id)
return jsonify({'code': 0, 'msg': 'success', 'data': {'token': token}})
注意 session_key 不能出现在返回结果里。它只在需要解密手机号、运动数据时用,普通项目不需要前端感知。登录逻辑里还要处理重复登录的幂等性,同一用户连续调用两次 login 不能产生两条 user 记录。
4.2 小程序页面与 API 的对接实践
小程序端我用了原生框架,没有引入第三方 UI 库。页面包括首页、百科分类页、水果详情页、价格趋势页和个人中心。首页显示当季水果推荐,需要调用 /api/fruit/recommend?city=xxx,小程序通过 wx.getLocation 获取城市后调用后端,城市解析用腾讯位置服务或直接在请求里带上 location 字符串。
水果详情页的数据结构设计为一次请求拿全,减少请求次数:
json复制{
"fruit": {
"id": 12,
"name": "荔枝",
"alias": "离枝",
"season": "5-7月",
"origin": "广东、福建",
"fresh_text": "正值产季,鲜度很好",
"fresh_score": 86,
"price_ratio": 0.82,
"current_price": "8.8元/500g",
"avg_price_30d": "10.7元/500g"
}
}
小程序端用 wx.request 请求这个接口,再把数据渲染到页面。这里有一个很实用的经验:初始版本不要在小程序端做数据过滤和排序,全部交给后端。小程序代码包体积有限,而且前端排序逻辑很难维护,后端一次给到排好序的结构,前端只负责渲染,这样定位问题也方便。
4.3 数据缓存和请求层设计
价格和百科内容不是实时变化的数据,我在小程序端做了两层缓存:内存缓存和 Storage 缓存。详情页数据设置10分钟缓存,百科正文设置24小时缓存。这样用户反复打开同一个水果详情时不会每次都请求后端,小程序加载速度会明显提升。
请求层抽成一个 request 方法,统一处理 baseURL、token、错误码和 loading:
javascript复制function request(path, method, data) {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + path,
method: method || 'GET',
data: data || {},
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + wx.getStorageSync('token')
},
success(res) {
if (res.data.code === 0) resolve(res.data.data);
else if (res.data.code === 1003) { handleLoginExpired(); reject(res.data); }
else reject(res.data);
},
fail: reject
})
})
}
有了这个统一请求层,业务页面只需要写 request('/api/fruit/recommend').then(...),页面代码会干净很多。这也是我后面重构时最值得的一步,强烈建议一开始就做。
5. 上线前必须解决的几个实际问题
5.1 本地联调与真机调试的域名限制
微信小程序对请求域名有严格限制,正式环境必须使用 HTTPS,并且在微信公众平台后台配置 request 合法域名。开发阶段可以在开发者工具中勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,但这只能解决本地模拟器问题,真机预览时依然会失败。
我第一次用本地 Flask 服务配真机调试,手机一直报 net::ERR_CONNECTION_RESET,排查下来发现是本地服务没有监听局域网 IP,而且没有 HTTPS。后续我把后端部署到测试服务器,用 Nginx 配置了 SSL 证书,才在真机上跑通。这里建议上线前早点把测试环境的 HTTPS 配好,别等到最后一刻。另外注意在小程序后台配置 request 合法域名时,域名必须是 ICP 备案过的,否则无法保存。
5.2 图片资源与内容安全
百科里会用到大量的水果图片。最开始我直接引用别人网站上的图片链接,结果出现两个问题:一是部分站点开启防盗链,小程序里图片加载失败;二是图片版权不清晰。后来我统一把所有图片下载到自己的对象存储,并通过后端接口返回自己的图片域名。这样也方便做图片压缩,减少小程序加载流量。
另外,如果后期开放用户评论、上传图片,一定要接微信的 security.msgSecCheck 文本内容安全检查接口。用户发布评论前先调用检查,不合规直接拦截并提示。这个是小程序审核和内容运营的底线,不能省。即使是个人项目,只要涉及 UGC,内容安全接口就是必备的。
5.3 部署环境的内存优化与定时任务
这个项目我部署在一台 1核2G 的云服务器上,操作系统 Ubuntu,使用 Gunicorn + Gevent 运行 Flask。Gunicorn 配置很关键,worker 数量不是越大越好,2G 内存我设置 workers=2,threads=2。如果把 worker 拉到4个,每个 worker 预加载的模型和数据库连接会让内存直接爆掉。下面是我用的启动配置:
bash复制gunicorn -w 2 -k gevent --timeout 60 -b 127.0.0.1:8000 app:app
Nginx 负责反向代理和静态资源缓存。定时采集任务不要放在 Gunicorn 进程里跑,否则每个 worker 都可能会去执行重复任务。我用系统的 crontab 单独跑一个采集脚本:
bash复制0 8 * * * cd /srv/fruit_project && /usr/bin/python3 scripts/run_spider.py
0 14 * * * cd /srv/fruit_project && /usr/bin/python3 scripts/run_spider.py
这样即使 Flask 进程重启,也不会影响价格采集任务。数据库连接池方面,SQLAlchemy 默认的连接池在 2G 内存下只要设置 pool_size=5, max_overflow=5 就足够了,过大反而容易把 MySQL 连接数占满。同时我会给 MySQL 开慢查询日志,定期看一下哪些索引没命中,这个习惯能省掉很多线上排查时间。
6. 从“鲜而廉”继续往前走:改进方向与个人体会
6.1 把项目升级为区域水果行情指数
目前的“鲜”和“廉”是针对单一水果的。做了一段时间后我发现,用户更想知道“我现在住的城市,这个季节最适合买什么水果、价格处于什么水平”。这个需求在技术上可以扩展成一个区域水果行情指数:聚合某个城市所有价格记录,按水果品类计算均价、价格分布和新鲜度均值,然后给市民一个“当前水果消费指南”。
这个功能不需要改数据库表结构,只需要新增一个聚合查询接口。但要注意城市粒度不能太细,精确到地级市就行,否则样本量不足,价格规律没有参考意义。比如武汉的荔枝价格样本可能有两三百条,但江汉区单独拿出来可能只有几条,做出来的价格指数反而会误导用户。
6.2 从百科到社区:用户贡献的闭环
用户最喜欢做的一件事是晒水果价格。我在小程序里加了一个简单的“用户报料”入口,用户可以去详情页提交自己看到的价格和“看着新鲜不新鲜”。这些数据不进入均价计算,但会在详情页的参考信息里展示,让后来的人有参考。
后续可以把这个入口升级成轻社区:用户上传水果照片、标注购买地点,其他用户点赞。关键是要控制内容安全,接微信的内容安全检查接口,并且做人工审核后台。没有审核能力的个人项目,建议只开放价格报料,不开放自由评论,否则审核压力会非常大。这也是我在第一节说的,别一上来就做社区。
6.3 做这个项目沉淀下来的几点经验
如果重新做一次,我会把顺序调整成:先跑通价格采集,再写百科页面。价格数据是整个项目的灵魂,百科内容哪怕少一点,用户也能用;反过来如果百科很丰富但价格是空的,用户很快就会流失。
小程序端要尽早统一封装请求层,不要在业务代码里到处写 wx.request,后面错误处理和登录过期会改到崩溃。后端接口返回结构一定要带统一状态码,哪怕前期看起来有点多余,数据量上来以后会省很多事。
最后,不用先追求大而全。把“查水果百科、看价格是否偏高、判断新鲜度”这三件事做稳,比堆一堆花哨功能更能留住用户。这个项目做到现在,回头率最高的恰恰是最不起眼的新鲜度提示文案和价格区间,而不是首页那些漂亮的轮播图。
