抬头看看,你有多久没有认认真真看一次星星了?作为一名业余天文爱好者,同时又是个写代码的,这个问题一直在我脑子里打转——市区光污染严重、天气预报只告诉你会不会下雨、星图App和社交平台又是割裂的……想看星星的人找不到合适的时间和地点,看到了想分享也找不到同好。这个项目就是在这个背景下做的:一个面向天文爱好者的观星交流系统,后端用 Python 搭 API,前端用 uniapp 开发,最终承载平台选的是微信小程序,核心解决三个问题——今晚能不能看、去哪里看、看完和谁聊。
一年做下来,踩坑无数,也总结了不少实战经验。这篇文章不聊虚的,直接把整个系统的设计思路、模块拆解、核心代码、排坑记录全部摊开讲,适合三类人读:想用 uniapp + Python 做微信小程序全栈开发的、对天文数据集成感兴趣的、以及想找一个完整毕业设计/项目源码做参考的同学。
1. 项目整体设计与技术选型思路
1.1 为什么是 Python + uniapp,而不是别的组合
先说说技术选型的底层逻辑。前端选择 uniapp,最直接的原因是我当时既要做微信小程序,又不能放弃后续上架安卓和 iOS 的可能。uniapp 是基于 Vue 语法的跨端框架,一套代码可以编译成微信小程序、支付宝小程序、H5、iOS App、Android App。这种“一次编写、多端运行”的能力,对有个人项目规划的人来说非常划算——我先用小程序验证需求,验证通了再编译成 App,不用重写前端。
有一个很现实的问题值得拿出来说:很多同学纠结“学微信原生开发还是学 uniapp”。我的经历是,uniapp 对 Vue 开发者几乎没有学习成本,而且它的组件生命周期、路由、状态管理都是标准 Vue 那套,网上资料又多。微信原生开发虽然性能上限高,但对个人开发者来说开发效率偏低,而且只能专注在微信一个平台。如果你是个独立开发者,时间才是最贵的资源,uniapp 恰好是那个能帮你省时间的框架。
后端选 Python 则更直接。Python 生态里有一套天然的“天文计算工具箱”:ephem、skyfield、astral这些库可以直接计算太阳/月亮出没时间、行星位置、晨昏蒙影等,准确度高到能满足实际观测需求;requests、BeautifulSoup方便对接第三方天气和地理数据;框架层面 Flask 轻量灵活、初学者友好。之前万能的后端语言也做过,但论“出活儿速度”,Python 真的是很稳妥的选择。
1.2 系统整体架构与模块划分
整个系统的架构并不复杂,一句话概括就是“一端、一服、一库加三方服务”:小程序作为用户入口,统一通过 HTTPS 请求 Python 后端接口,后端负责业务逻辑和鉴权,数据落到 MySQL,缓存用 Redis(初期可以不上),外部依赖主要是气象数据接口和微信开放能力。
模块划分上,我把系统拆成了五大块:用户模块(登录、手机号授权、个人资料)、观星指引模块(天气数据、观星指数、光害信息)、天象日历模块(全年天文事件)、社区模块(发帖、评论、点赞、关注)、观测记录模块(打卡、照片、地点标注)。为什么按这种方式切而不是按页面切?因为后端接口的边界应该由业务域决定,页面是随时会改的,而业务域相对稳定。
每个模块之间尽量做到低耦合。举个例子:用户点“收藏某一天象事件”,操作的是天象模块的收藏表,但判断用户是否登录要走用户模块的鉴权,两者通过统一的 token 机制衔接,互不依赖内部实现。这种“高内聚、低耦合”的设计,对于后期扩展和维护极其重要——我上线后加了一个“观星打卡地图”功能,基本没动旧代码。
2. 核心功能模块设计与实现要点
2.1 用户登录与手机号授权:别踩企业认证的坑
说到微信小程序登录,新手最容易掉的坑就是“手机号授权”。微信官方在 2023 年之后把getPhoneNumber的获取规则改得非常严格:想获取用户手机号,小程序必须完成企业主体认证,个人主体的开发者是拿不到明文手机号的。这个在需求设计阶段就要心里有数。
完整的登录链路建议设计为两种方式并行:
- 静默登录:小程序端调用
wx.login()拿到临时code,上传到后端,后端拿code去微信接口换取openid,再用openid去查数据库,存在就签发 token,不存在就创建新用户。 - 手机号快捷登录:点击授权按钮触发
getPhoneNumber事件,拿到加密数据,后端配合session_key解密出手机号。这个方案建议在小程序认证通过后再启用,开发阶段先用静默登录顶着。
后端接口建议设计成这样的形式:
# Flask示例:微信登录接口 @app.route('/api/user/login', methods=['POST']) def wx_login(): code = request.json.get('code') # 用code换取openid和session_key resp = requests.get( 'https://api.weixin.qq.com/sns/jscode2session', params={ 'appid': APP_ID, 'secret': APP_SECRET, 'js_code': code, 'grant_type': 'authorization_code' } ).json() openid = resp.get('openid') if not openid: return jsonify(code=400, msg='登录失败') # 查库或创号 user = User.get_by_openid(openid) if not user: user = User.create(openid=openid, nickname=f'星友{random.randint(1000,9999)}') token = generate_jwt({'uid': user.id}) return jsonify(code=0, data={'token': token, 'user': user.to_dict()})2.2 观星指数与天象日历:把天文算法变成产品功能
这个模块是整个系统里最有“天文特色”的,也是能让你的项目在答辩或社区分享时脱颖而出的亮点。观星指数本质上是一个综合评分,建议由四个维度加权计算:云量占比(低于 30% 为佳)、大气透明度(能见度代表)、光污染等级(波特尔暗空等级,郊区一般 3-4 级,市区 8-9 级)、月亮干扰度(满月前后月光会把暗天体彻底淹没,月牙前后则是黄金窗口)。
这四个维度的数据和权重:
| 维度 | 数据来源 | 建议权重 | 说明 |
|---|---|---|---|
| 云量占比 | 和风天气/中国天气网 | 40% | 核心指标,有云一切都白搭 |
| 大气透明度 | 能见度接口(km) | 20% | 影响目视深空天体效果 |
| 光害等级 | 光害地图GeoJSON数据 | 25% | 决定你能不能看到银河 |
| 月光干扰 | 月相计算(skyfield或自定义算法) | 15% | 满月时观测条件直接打折 |
月相部分我强烈推荐用skyfield库,这个是天文计算库,由美国海军天文台的数据驱动,精度极高,而且 API 设计得很人性化。计算月出月落、月相、行星位置都可以实现。
from skyfield import api, almanac ts = api.load.timescale() t = ts.now() eph = api.load('de421.bsp') # 计算月相、日月位置等天象日历的数据来源有两种做法:一是从公开天文年历网站上爬取当年度天象事件表(流星雨极大、月食、行星合月等),二是用skyfield自己推演常规事件,再人工补充特殊事件。我实际项目里是混合方案,常规天象自己算,特殊天象(比如多少年一遇的“七星连珠”)通过人工录入。接口层面做成分页列表,支持按月份筛选。
2.3 社区交流模块:别忘了内容安全这道防线
社区是用户黏性的核心,也是这个系统里开发工作量最大的部分。核心数据模型有四张表:动态表(帖子)、评论表、点赞表、关注表。
设计信息流接口时,要注意两点。第一,分页不要用offset,数据大了之后深翻页会越来越慢,用cursor(游标分页)方式,即依据上一页最后一条帖子的id或时间戳来取下一页。第二,排序策略要有两种,“最新”按created_at倒序,“热门”按“近7天点赞数 + 评论数×2”计算热度值排序,这个公式可以按业务反馈微调。
社区内容安全必须得做,这是我在上线测试时吃过亏的地方。小程序如果被投诉“存在垃圾内容”,轻则警告,重则下架。建议从两层入手:内容发布时调微信官方的内容安全检测接口msgSecCheck(免费的,但需要在小程序后台申请开通),对文本做敏感词过滤;同时在后端对用户举报做处理队列,运营人员在后台一键删除。
2.4 观星记录与地图打卡:沉淀用户数据
这个模块是小区分度的关键。用户看完星星后可以创建一条“观星记录”,包含观测日期、地点坐标、星况描述、照片、当时的天气和月相。这些数据能帮用户在年底形成一份“个人观星年报”,也能汇聚成城市级的观星活跃热力图。
地图选型这里多说一句,小程序原生地图组件有合规要求,商业级应用建议用天地图或腾讯地图的微信小程序 SDK,别去碰没有资质的地图数据源。天地图个人开发者也能申请 key,集成方式与腾讯地图类似,支持点标记、路线规划,完全够用。
3. 关键环节实操:从零搭起核心链路
3.1 数据库设计与核心表结构
数据库设计遵循“够用就好”的原则,在数据量和个人服务器的现实之间找平衡。技术选型用 MySQL 5.7+,字符集utf8mb4,InnoDB 引擎。核心表设计如下,都是实际能落地的字段:
用户表user
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid', `nickname` varchar(64) DEFAULT '' COMMENT '昵称', `avatar_url` varchar(255) DEFAULT '' COMMENT '头像', `mobile` varchar(20) DEFAULT '' COMMENT '手机号', `level` tinyint(4) DEFAULT 1 COMMENT '等级', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;帖子表post核心字段包含:id、user_id、content、images(JSON数组)、location、latitude、longitude、view_count、like_count、comment_count、status(0正常/1删除/2违规)、created_at。
评论表comment核心字段包含:id、post_id、user_id、reply_to(回复的评论ID,支持楼中楼)、content、status、created_at。
观星记录表observation核心字段包含:id、user_id、title、content、images、latitude、longitude、address、seeing(观测质量评分)、moon_phase(当天月相,后端自动计算)、observation_time、created_at。
这几张表的关系很清晰:用户一对多帖子,帖子一对多评论,用户多对多关注(用一张follow关系表),用户多对多点赞(like_record表)。不需要搞外键约束,靠应用层逻辑维护,在个人项目里省心不少。
提示:MySQL 的
JSON类型在 5.7 之后非常好用,存图片列表、扩展字段都很方便,查询时也能用JSON_EXTRACT,比拆多张关联表省事。
3.2 后端接口规范与鉴权设计
后端接口层我强烈建议先定好“响应规范”,再开始写业务。统一的结构是:
{ "code": 0, "msg": "success", "data": {} }code为 0 表示成功,非 0 表示业务错误码(比如10001是未登录、10002是参数错误、10003是无权限)。这样做的好处是前端response拦截器可以统一处理,不用每个请求单独判断。uniapp 的uni.request封装一层,把 token 自动加到 header 里,把非 0 的 code 统一弹 toast,代码量省一大截。
鉴权用的是 JWT(JSON Web Token),流程是:用户登录成功 → 后端签发 token(设定 7 天过期)→ 小程序端存入uni.setStorageSync→ 每次请求在 header 里带Authorization: Bearer <token>→ 后端的before_request钩子里统一校验。
# Flask示例:JWT校验装饰器 import jwt def login_required(f): @wraps(f) def wrapper(*args, **kwargs): token = request.headers.get('Authorization', '').replace('Bearer ', '') try: payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) request.uid = payload['uid'] except jwt.ExpiredSignatureError: return jsonify(code=10001, msg='登录已过期') except jwt.InvalidTokenError: return jsonify(code=10001, msg='无效令牌') return f(*args, **kwargs) return wrapperJWT 不需要在后端存储 session,天然适合小程序这种无状态场景。有人说 JWT 不好“强制下线”,但个人项目完全够用,真要封号直接在库里把用户status改成禁用即可,接口每次校验时查一下也行,代价可控。
3.3 前端页面实现与微信小程序适配
前端页面规划是底部 TabBar 四个栏目:首页(观星指数、今日天象)、天象(日历时间线)、社区(信息流)、我的(个人中心、观测记录入口)。uniapp 的页面配置集中在pages.json里,最关键的是顶部导航栏和胶囊按钮的适配。
微信小程序的右上角胶囊按钮是系统级的,不吃navigationStyle: custom时会被自动挤开,所以很多开发者选择自定义导航栏来做沉浸式头部。这时候要认真处理一个细节:不同的机型胶囊按钮的位置和高度不一样。推荐使用 uniapp 官方提供的uni.getSystemInfoSync()获取状态栏高度,再动态计算导航栏高度,避免写死44px之类的固定值。
const systemInfo = uni.getSystemInfoSync() const menuButtonInfo = uni.getMenuButtonBoundingClientRect() // 导航栏高度 = (胶囊顶部 - 状态栏高度) * 2 + 胶囊高度 const navBarHeight = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height这个公式实测下来基本通吃所有机型——它的原理是胶囊按钮垂直居中于导航栏,因此导航栏总高度是“胶囊上下延伸距离加上本身高度”的两倍关系。
另一个重量级适配问题是分包。微信小程序主包体积限制 2MB,而 uniapp 打包出来动不动就超。别急,先看某个热搜词条:“source size 2612kb exceed max limit 2mb”——这个问题的标准解法就是微信的分包加载机制。把“社区”“天象”这些相对独立的页面放进分包目录,主包只保留 TabBar 页面和公共库,体积瞬间能降下来。配置方式:
// pages.json { "pages": [ { "path": "pages/index/index", "style": {} }, { "path": "pages/community/community", "style": {} } ], "subPackages": [ { "root": "pages/calendar", "pages": [ { "path": "calendar", "style": {} } ] }, { "root": "pages/profile", "pages": [ { "path": "records", "style": {} } ] } ] }分包之后,项目运行时会默认从主包找页面,找不到再去分包找,所有uni.navigateTo的跳转路径写法不变,对开发者完全透明。这是性价比最高的一个优化,强烈建议从一开始就做。
4. 实战排坑记录:那些文档里不会写的细节
4.1 微信小程序 2MB 限制与分包实战
除了分包,还要注意一个隐性体积大户——图片资源。很多开发者在本地static目录里塞了几张 UI 背景图,一张就 500KB,加上 uniapp 自带的uni-ui组件库,体积很容易就顶破 2MB。
我在项目里面临的实际案例是:主包体积 2.6MB,超了 600 多KB。处理顺序是:第一步,把背景图和图标全部换成 CDN 外链地址,本地只留启动图(经过压缩);第二步,把uni-ui改成“按需引入”,只用哪里导哪里,避免全量打包;第三步,大模块分包。三步下来主包缩到 1.4MB,加载速度也明显提升。
注意:如果你用了
uview-plus这类三方组件库,默认是全部引入的,务必改成easycom按需模式,这能省出几百KB的体积。
另一个关于体积的心得是:微信开发者工具里看到的source size是未压缩代码体积,真机上传时经过了压缩,所以本地工具提示 1.9MB 不一定超限,但 2.4MB 以上大概率还是要处理。以实际上传时提示为准,别过度焦虑。
4.2 手机号登录与定位权限的连环坑
这个坑我建议新手提前预习。手机号登录在个人主体小程序中,会一直遇到“获取手机号失败”或“该能力未开通”的报错。这个问题的根因是权限没打通:个人主体小程序无法开通获取手机号组件,必须企业主体且完成微信认证,否则只能通过wx.login静默登录去拿 openid。
定位权限则有两个坑。第一个是wx.getLocation接口在 2022 年后需要在小程序后台申请“地理位置接口”权限,审核要提交使用场景说明,内容是“用于记录观星打卡地点和推荐周边观星点”,一般一两天就能过。第二个是后台定位问题:想在地图上持续记录用户轨迹,小程序切到后台后 JS 定时器会被冻结,定位也会被迫暂停。uniapp 有uni.startLocationUpdate配合plus.geolocation.watchPosition的方案,但那是 App 端的 API,小程序端根本不支持——如果你在“小程序端”写了这行代码,它只会静默失败,实测没有任何报错,但功能就是不生效。
这类问题的排查技巧是:在小程序开发工具里打开 “真机调试”,结合wx.onLocationChange的监听事件来确认小程序端定位是否一直返回数据,再判断是 API 不支持还是被用户拒绝权限。
4.3 日志不打印与样式错位问题
“uniapp 不打印日志信息”是开发者社区高频问题,我项目里也遇到过。排查发现,console.log在微信开发者工具的“普通编译”模式下是能打印的,但如果你开了“开发环境不校验请求域名”然后又连了局域网后端,日志会被网络层拦截吞掉;更常见的是在promise失败回调里忘记打印。这不是 uniapp 的锅,更多是工具版本或缓存问题。
建议统一封装一个日志工具,把console.log替换成自己的方法,并且按照环境变量控制开关:
// utils/logger.js const isProd = process.env.NODE_ENV === 'production' export function log(...args) { if (!isProd) { console.log(...args) } }样式错位则是自动适配带来的连锁反应——小程序端rpx单位适配效果不错,但同一个 uniapp 工程编译到 App 端时,rpx的基准会变化,这时候页面经常会出现 1px 到几像素的偏差。我的经验是,核心样式统一用rpx写在公共样式中,但遇到弹窗类组件用px+flex自适应,实测在两端表现都正常。
4.4 Python 环境与天文库依赖的坑
Python 环境这一块,新手经常在“事件查看器里报缺模块”这件事上卡住。requests、flask、pymysql、skyfield这几个库如果用pip install安装遇到权限问题,建议用虚拟环境隔离项目依赖,不要把包装在全局。
最重要的是skyfield需要注意:第一次使用时它会联网下载星历文件(比如de421.bsp,大小约 17MB),服务器在国内可能下载得很慢,甚至超时。解决的办法是:提前在本地下载好星历文件,上传到项目目录或者服务器指定路径,再用绝对路径加载,避免每次都走网络。这个文件后续如果要高精度版本可以换de440s.bsp,但体积接近 100MB,普通观星场景de421.bsp完全够用。
另外提醒一句:天文算法计算出来的时间默认是 UTC,但用户看到的应该是北京时间。在接口层统一做一次时区转换,所有返回到前端的“天象事件时间”都已经是Asia/Shanghai时区,前端只管展示。如果不做这层处理,等上线后用户发现“预告的流星雨时间是凌晨 3 点,实际是下午 3 点”,口碑就崩了。
5. 上线前后必须过的三关
整个项目从能跑到能上线,中间还隔着测试、审核、运营准备三关。先说测试,微信小程序端的兼容性矩阵比想象中复杂得多——iOS 和安卓的 webview 内核不同,CSS 支持差异、API 行为差异都会造成问题。我在上线前花了整整三天做真机兼容性测试,至少覆盖 iPhone 低版本机型、老安卓机型、不同分屏比例。
审核关也很磨人。小程序审核大部分是一次过的,但有一个坎是企业认证(个人主体也可以发小程序,但缺乏很多能力),而且名称和类目必须选对。“观星交流”这种偏社区属性的类目,建议选“工具-信息查询”而不是“社交”,能显著降低审核被驳回的概率。
运营这一关容易被技术栈选手忽略。我最初的版本没有做任何运营后台,后来发现审核反馈、用户举报、内容删除都需要一个管理端,临时加了一个简单的 Flask-admin 页面接上数据库。如果要二开,建议在最初设计时就预留管理端接口,不然后面补的代码都是在刀尖上跳舞。
6. 写在项目之外
这个项目把我自己的两个兴趣真正连到了一块儿。做之前我以为技术难点在“天文算法”上,做完才发现,天文计算反而是花时间最少的部分——skyfield库解决了一切;最难啃的是微信小程序各种细碎的平台限制:体积、权限、审核、适配。这大概也是所有“第三方平台应用开发”的共性:你不仅要写好代码,更要学会在平台的规则里跳舞。
最后分享一个用起来很爽的设计:我把“观星指数”的计算规则抽成了独立配置表,前端下拉刷新时会动态拉取最新的权重配置。UGC 社区上线后收到了不少用户反馈,有人要求把月光干扰权重调低、有人希望加权“视宁度”,我直接在后台改了配置就生效,不用重发版本。这种“把规则从代码里剥离”的思路,真心建议每一个中小型项目都试试——它省下来的发版时间,远比写规则引擎的时间值。