ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

校园新闻APP双端开发实战:Android+微信小程序从0到上线

校园新闻APP双端开发实战:Android+微信小程序从0到上线 校园新闻这件事很多学校做起来其实挺拧巴官网新闻没人看公众号推送被折叠进“订阅号消息”里大屏幕海报转瞬即逝。我参与的一个典型项目就是围绕这个痛点展开的——一个基于 Android 和微信小程序的校园新闻发布 APP。小程序端负责师生日常浏览、分类阅读和接收通知Android 端则承担内容生产、审核管理和更重的多媒体处理任务两端共用一套服务端接口。整个项目从需求梳理到双端上线大概用了两个多月中间踩了不少坑。这篇文章会把设计思路、业务模型、接口约定、双端实现细节和上线经验完整拆一遍给正在做校园信息化、校园媒体类项目的同学一个参考。1. 项目定位与整体架构设计1.1 这个 APP 到底解决了什么问题很多学校不是没有新闻系统而是现有的系统太“重”、太“旧”。学校官网的内容管理系统CMS发布流程复杂编辑要登录后台、传图、排版发布后手机上打开体验又差。微信公众号虽然便捷但图文消息一旦过了两三天基本就沉到列表底部师生再想找一条重要通知非常困难。这个项目要解决的核心问题有三个第一信息触达要轻。微信小程序免安装、扫码即用对师生来说几乎没有使用门槛。把日常新闻、讲座预告、失物招领这类信息放进小程序用起来比打开官网快得多。第二内容生产要顺。新闻内容不只是学生会的活动稿还有学校层面的通知、院系动态、紧急公告。这些内容需要一个独立的、能力更强的 Android 端来处理——尤其是批量传图、富文本编辑、审核发布这些“重操作”放在小程序里做体验并不好。第三闭环要完整。从投稿、编辑、审核、发布到用户评论、收藏、阅读数据统计整个链路要在一个系统里跑通而不是“内容在小程序发数据在官网后台看”这也是后来接口设计里特别强调状态机的原因。1.2 双端技术栈与目录结构这个项目最终的技术选型是小程序端微信原生小程序WXML/WXSS/JS没有用 uni-app。Android 端Kotlin JetpackViewModel、Room、PagingMVVM 架构。服务端Spring Boot 2.x MyBatis-Plus MySQL 8.0 Redis文件存储走云对象存储加 CDN。为什么选择原生而不是跨平台框架主要原因是团队里 Android 开发和小程序开发是两个人各自熟悉独立的技术栈用一个跨平台框架反而会互相拖累。而且校园项目业务不算复杂原生开发维护成本完全可接受。服务端我按功能域拆了两个上下文/api/app/**给小程序端用/api/admin/**给 Android 管理端用。这样接口权限边界清晰小程序端拿不到管理接口Android 端也碰不到用户侧的敏感数据。双端的目录结构也按这个思路对齐小程序端核心目录pages/home/index首页信息流pages/category/index分类频道pages/detail/index新闻详情pages/profile/index个人中心components/news-card新闻卡片组件utils/request.js请求封装Android 端核心目录ui/home信息流ui/publish发布编辑ui/audit审核列表data/remoteRetrofit 接口data/localRoom 数据库1.3 产品功能与迭代规划校园项目最忌讳一上来求大求全。这个项目第一版只做了六个核心功能新闻信息流、分类浏览、富文本详情、搜索、收藏、评论。Android 管理端则做发布、编辑、审核、置顶、下架。第二版才加入了订阅消息通知、话题标签、阅读统计和草稿箱。订阅消息是后补的重头戏后面会详细讲因为微信的订阅消息机制和很多人想象的完全不一样。第一版的原则就一句话让用户能用让运营能管让审核能过。功能宁可少流程必须完整。2. 服务端核心设计与数据表建模2.1 数据表设计先把状态机理清楚新闻类的项目最核心的表就是新闻表而新闻表最核心的字段是状态status。这个项目的状态机经过三轮迭代才稳定下来0草稿1待审核2已发布3已下架4审核驳回。草稿可以提交为待审核待审核可以被通过变成已发布也可以被驳回回到草稿已发布可以手动下架下架后可以重新上架。这个状态机看起来简单但踩坑的地方在于“驳回”和“下架”之后用户看到什么。我们的做法是被驳回的新闻只对作者和管理员可见普通用户在小程序里看不到已下架的新闻则可以直接从信息流里消失但保留详情页的访问能力缓存用户打开过的页面。新闻表的主要字段如下字段类型说明idbigint主键雪花 IDtitlevarchar(128)标题summaryvarchar(255)摘要列表页直接展示contentlongtext富文本内容HTML 格式covervarchar(512)封面图 URLcategory_idbigint分类 IDstatustinyint状态机字段author_idbigint作者 IDreviewer_idbigint审核人 IDpublish_timedatetime发布时间is_toptinyint是否置顶view_countint阅读数用 Redis 累加这里有一个容易被忽略的设计点summary和content分开存。列表页只需要展示摘要如果接口把整个content都返回流量和渲染压力都会变大。小程序端一次渲染几百行的 HTML 会有明显的卡顿感。view_count我做了冗余而不是实时查询。新闻阅读数是高频更新、低频精确查询的数据如果每个用户打开详情都UPDATE view_count view_count 1热点新闻会让数据库压力很大。实际方案是先用 Redis 的INCR累加每分钟同步一次到 MySQL详情页展示先读缓存。2.2 接口设计规范统一返回结构和鉴权双端项目最怕接口风格不统一。这个项目从第一天就定了接口返回结构{ code: 0, message: ok, data: {} }业务错误码单独定义1001参数错误1002未登录1003无权限1004内容不存在1005状态不允许操作。小程序端的登录逻辑比较特殊。用户打开小程序后前端调wx.login拿到临时code发给服务端服务端再调用微信接口用code换openid然后用openid签发一个自定义令牌返回给小程序。后续请求都带这个令牌服务端通过令牌识别用户身份不需要把openid暴露给前端。Android 管理端则走标准的账号密码登录返回 Token放在请求头Authorization里。管理端用户表不建议跟普通用户表混在一起因为管理员的角色、权限、审核记录和普通用户差异太大分开建表后续扩展也方便。分页接口统一用page和pageSize参数返回结构统一为{ list: [], total: 100, page: 1, hasMore: true }hasMore这个字段特别重要前端做上拉加载全靠它比前端自己用page和pageSize算是否到底要可靠得多。关键的接口列表接口方法说明/api/app/news/pageGET分页获取新闻列表/api/app/news/{id}GET获取新闻详情/api/app/category/listGET分类列表/api/app/comment/addPOST添加评论/api/app/favorite/togglePOST收藏/取消收藏/api/app/searchGET关键词搜索/api/admin/news/auditPOST审核通过/驳回/api/admin/news/publishPOST创建或更新新闻2.3 文件与图片处理内容安全的前置环节校园新闻里图片占比很高封面图、正文插图、活动海报一场活动下来轻轻松松几十张图。图片处理这块有几个经验第一上传接口必须做白名单校验。文件后缀白名单jpg、png、webp、gif只是最基础的还要校验文件头防止有人把脚本文件伪装成图片上传。第二图片压缩要两条腿走路。前端在上传前做一次压缩服务端收到后再用工具压一次。服务端压缩我用的是 Thumbnailator设置长边不超过 2000 像素、质量 0.8封面图单独再切一张 750x500 的版本。第三内容里的图片要做“宽度自适应”处理。用户在小程序详情页看到的图片必须把stylemax-width:100%拼到图片标签上否则图片超出屏幕宽度会撑破布局这个问题后面章节细讲。文件存储我直接用的云对象存储图片走 CDN 加速。上传路径按日期分目录比如news/2025/06/12/{uuid}.jpg避免单目录文件过多。3. 微信小程序端实现3.1 项目初始化与页面结构小程序端我用的是微信原生框架页面结构按微信官方规范组织。app.json里的tabBar配置了三个主页面首页、分类、我的。首页是新闻信息流分类页是频道聚合我的页面放收藏、浏览历史和个人设置。这里有个经验新闻详情页不要放 tabBar也不要在主包页面里塞太多东西。微信小程序主包不能超过 2MB所有页面加起来很容易超。我们的做法是首页、分类、我的三个 tab 页放在主包新闻详情、搜索、评论列表、订阅设置统一放到分包pages/detail-package里。这样即使后面加再多的二级页面主包体积也能控制住。页面目录结构pages/ ├── home/index ├── category/index ├── profile/index ├── detail-package/ │ ├── detail/index │ ├── search/index │ └── comments/index ├── publish-package/ │ └── publish/index3.2 首页信息流与上拉加载首页信息流是最核心的页面请求逻辑用一句话概括首次加载拉第一页下拉刷新重置到第一页上拉触底加载下一页。请求封装我写在utils/request.js里核心处理了四件事baseURL统一管理在开发者工具和正式环境之间切换。在请求头里自动带上token。统一处理code ! 0的错误提示。对401未登录做统一跳转处理。上拉加载的关键实现要点onReachBottom() { if (this.data.loading || !this.data.hasMore) return this.loadNews() }这个判断非常关键防止用户快速滑动时重复触发加载。首页的新闻卡片我用了一个自定义组件news-card传入标题、摘要、封面、发布时间、阅读数。列表渲染时用wx:keyid避免渲染告警。封面图在组件内部做modeaspectFill裁剪配合固定宽高比例整套卡片看起来就整齐。3.3 富文本详情页被图片坑了三次新闻详情页用rich-text组件渲染服务端返回的 HTML这是小程序端最容易踩坑的地方。第一次踩坑是图片宽度。服务端返回的 HTML 里图片没有style在手机端显示超出了屏幕宽度。解决办法是在服务端把图片标签统一处理为img src... stylemax-width:100% /。第二次踩坑是图片防盗链。云存储开启了防盗链后小程序端rich-text里的图片直接裂开。这个问题在开发者工具里还看不到只有真机上出现。最后是在云存储的防盗链设置里把微信小程序的合法域名加进白名单才解决。第三次踩坑是富文本里的视频。rich-text不支持video标签所以服务端把视频识别出来单独返回一个视频链接字段详情页用video组件单独渲染HTML 里只保留视频封面图。这三轮踩坑后详情页的渲染方案才算是稳定。核心原则是富文本 HTML 里的样式尽量在服务端就处理干净小程序端不要做太多字符串解析和拼装否则性能不好还容易出 bug。3.4 登录与订阅消息微信的规则必须前置了解小程序登录用wx.login换取code然后调服务端接口换取自定义令牌。这里要注意wx.login拿到的code有效期只有五分钟且只能用一次。如果用户在弱网环境下反复调用服务端会报code been used前端要做好错误处理。订阅消息是这个项目里比较特殊的功能。目标是当管理员审核通过一条新新闻后订阅过该分类的师生能收到一条微信服务通知。但微信订阅消息有两大限制必须提前知道第一订阅消息不能像 App 推送那样“默认开启”必须由用户主动触发订阅授权。也就是说用户必须在小程序里点击某个按钮比如“订阅本分类更新”才能收到对应分类的消息。第二一次性订阅消息模板只能使用一次。用户订阅一次服务端只能给他发一条消息。如果想多次发送需要用户多次点击授权或者使用长期订阅但长期订阅的类目受到非常严格的限制。这个限制对“校园新闻推送”来说其实很尴尬因为用户不可能为每一条新闻都点一次订阅。我们的折中方案是在新闻详情页放一个“订阅评论回复通知”的按钮用户点击后才在他评论的内容被回复时发送一条订阅消息。日常新闻更新不依靠订阅消息触达而是靠小程序的服务通知“提醒”。这个方案虽然保守但至少保证了合规不会因为诱导订阅被封禁。4. Android 端实现管理端才是真正的“重活”4.1 工程搭建与网络层设计Android 端一开始就是奔着“管理工具”去设计的主要使用者是学校宣传部门、新闻社成员和系统管理员。技术栈选的是 Kotlin MVVM网络层是 Retrofit OkHttp Kotlin 协程。网络层做了三层封装第一层是ApiService定义所有接口方法返回ApiResponseT的统一响应体。第二层是拦截器。AuthInterceptor从本地读取 Token 并加到请求头HttpLoggingInterceptor在 debug 模式下打印完整请求日志release 模式关闭。第三层是仓库层。网络数据和本地缓存都通过仓库层暴露给 ViewModel页面不直接调用 Retrofit。class Repository(private val api: ApiService) { suspend fun getNews(page: Int, pageSize: Int): ApiResponsePageResultNews { return api.getNews(page, pageSize) } }联调环境的地址切换也是个细节。开发时后台服务跑在电脑上Android 模拟器访问宿主机要用10.0.2.2真机要用局域网 IP。我给项目配了三个构建类型debug连测试环境、staging连预发布环境、release连正式环境通过BuildConfig.API_BASE_URL引用不会出现“忘了切地址测试数据发到生产环境”的乌龙。4.2 信息流列表与图片加载Android 端首页跟小程序端类似也是信息流列表技术选型上用的是RecyclerViewLinearLayoutManager。但有几个经验值得单独拿出来说。一是列表的更新策略。刚开始用notifyDataSetChanged()数据一多滚动就卡后来改成DiffUtil。DiffUtil比较新旧列表差异后增量更新配合ListAdapter滑动流畅度提升非常明显。二是图片加载。用的 Coil而不是 Glide因为 Coil 原生支持协程跟 Kotlin 项目集成更干净。列表里的封面图统一裁剪成固定宽高比用自定义的ImageView尺寸和scaleType配合避免每张图都走一次完整解码。三是分页加载的状态管理。列表底部有三种状态加载中、加载完成无更多、加载失败。加载失败时显示“点击重试”这几个状态用BaseAdapter的viewType统一管理比在 Item 里塞隐藏字段可靠得多。4.3 发布与审核流程的实现细节发布是 Android 端的主力功能也是和普通新闻客户端差异最大的地方。发布页的核心是一个富文本编辑器。刚开始想直接用 WebView 套一个开源的 HTML 编辑器但校园用户学生会同学对排版要求不高反而对操作流畅度要求高。最后定制的方案是标题输入框 摘要输入框 封面选择 富文本编辑区。富文本编辑区用的是 WebView 加载一个轻量编辑器工具栏提供加粗、斜体、标题、图片插入、视频插入五个按钮。图片插入时不会直接把原图传上去而是先在 Android 端压缩再上传拿到 URL 后插入到编辑器的 HTML 里。这样最后的content字段始终是一段完整的 HTML服务端直接存储即可。审核流程同样放在 Android 端。审核列表按状态分 Tab待审核、已发布、已驳回。审核操作就两个按钮通过、驳回。驳回时必须填写原因原因会通过站内消息反馈给作者。这里有一个特别注意的细节审核操作是高风险操作必须做“二次确认”弹窗防止管理员误触把未审完的稿件直接发布出去了。4.4 草稿、缓存与离线浏览校园新闻有很强的“活动性”一篇文章可能断断续续写几天。如果编辑中途退出、手机关机草稿丢了会让人崩溃。所以草稿箱是刚需。草稿数据用 Room 存在本地数据库字段包含标题、摘要、内容 HTML、封面 URL、分类、更新时间。每次编辑操作都触发自动保存用户点返回时弹窗提示“草稿已保存”不会主动删除。离线缓存针对的是首页阅读场景。师生在学校礼堂、食堂这些 Wi-Fi 信号不稳定的地方刷新闻很容易出现加载失败。我在 Room 里建了一张缓存表存首页新闻列表的 JSON。请求网络失败时先展示缓存数据再提示“当前为离线内容”。5. 双端联调与上线运营经验5.1 联调环境的坑地址、域名与数据一致性双端联调是这类项目最容易拖进度的环节主要问题集中在三个方面。第一是 baseURL 的切换。小程序开发者工具里可以勾选“不校验合法域名”但真机预览时如果不在小程序后台配request合法域名请求会被直接拦截。Android 端则要注意明文 HTTP 在 Android 9 之后默认被禁止需要在AndroidManifest.xml里配置usesCleartextTraffictrue否则连不上本地测试环境。第二是局域网 IP 变化。开发机上 IP 一变两边的前端配置都要跟着改非常痛苦。我的做法是在服务端加一个env配置前端的baseURL通过它来动态区分测试环境用局域网 IP生产环境用正式域名。第三是数据一致性问题。双端同时操作同一条新闻可能出现“小程序端显示已发布Android 端刷新还是待审核”的情况。这个问题的根因是 Redis 缓存。新闻详情接口加了缓存状态变更后没有及时清缓存。后来统一在状态变更的接口里主动删除对应新闻的缓存才彻底解决。5.2 微信小程序审核别小看“内容监管”的要求校园新闻类小程序涉及内容发布微信审核对这类应用的资质要求比较严格。最核心的一条是涉及新闻、信息发布相关服务需要提供相关资质或说明。校园项目需要上传学校相关证明文件并说明使用场景。另外有两个审核细节第一如果小程序里有用户生成内容评论、投稿审核时需要有“内容安全”能力证明。我们没有接入额外的内容安全服务而是在服务端接了一层关键词过滤并保留了“举报”和“删除”的后台操作入口审核截图里能看到这些入口才容易过。第二隐私政策必须合规。小程序后台必须配置《用户隐私保护指引》并且 App 端申请相机、相册权限时也要有对应的隐私弹窗说明。这里建议在项目启动时就写好不要等审核被拒了才补。5.3 Android 上架的流程和成本Android 端如果上架应用商店流程比小程序繁琐不少。需要的材料包括软件著作权证书、备案信息、隐私政策链接、应用签名。软件著作权申请周期大概是 30 到 45 个工作日如果学校不着急上架可以提前申请。应用商店的选择上最常用的是应用宝、华为、小米、OPPO、vivo 五大商店。每一家都要单独注册开发者账号单独提交审核部分商店还要求提供隐私检测报告。上线成本这块如果服务端部署在自己的服务器上主要成本是服务器和域名备案。小程序的类目只要是学校相关的审核成本主要在资质准备上。整体评估下来一个校园新闻双端项目的固定成本并不高大头反而是人工审核时间。5.4 上线后的监控数据小程序端的数据相对好统计直接在微信后台看“访问分析”和“用户画像”就够了。Android 端的崩溃监控我接了一个第三方的崩溃上报 SDK。这里想强调一个观点校园项目也要有监控意识不要因为用户量小就跳过。新闻详情页的阅读数、评论数、收藏数这些数据虽然少但能很直观地反映功能是否有价值。上线后一个月我发现阅读次数最高的内容不是活动新闻而是“放假通知”和“考试安排”这说明用户的核心需求是“信息查证”而不是“新闻阅读”。后来我在首页顶部加了一个“通知公告”快捷入口整体活跃度明显上升。6. 踩坑记录与高频问题排查6.1 高频问题速查表双端联调和上线过程中我整理了一份高频问题清单按出现频率排序问题现象根本原因解决方案小程序真机图片裂开云存储防盗链未放行域名在小程序合法域名和白名单中同时配置rich-text 图片超出屏幕宽度HTML 中 img 标签缺少样式服务端拼接max-width:100%Android 请求 HTTP 接口失败Android 9 默认禁明文配置usesCleartextTraffictrue上拉加载重复请求未加加载锁请求前置判断loading和hasMore审核通过后小程序端仍显示待审核Redis 缓存未清除状态变更接口主动删除缓存登录偶发失效wx.login的 code 只能使用一次前端加并发锁避免连续调用评论列表数据错乱接口分页排序不稳定明确ORDER BY id DESC首页弱网下白屏没有离线缓存Room 缓存列表数据6.2 双端一致性与版本更新管理双端最怕的不只是功能对不上而是“状态对不上”。举个例子新闻在 Android 端被管理员下架了但小程序端用户之前已经打开了详情页此时用户刷新详情页应该看到什么我们的策略是详情页接口返回status3时小程序端提示“该内容已下线”但不直接跳走保留用户阅读缓存。这样既保证信息准确又不至于让用户产生“系统出 bug”的错觉。版本更新这块小程序由微信平台统一管理不存在用户版本过期的问题但 Android 端需要考虑。我用了两个版本号versionCode和versionName。服务端接口返回最低支持的versionCode如果当前版本低于最低版本强制弹更新提示否则直接跳转应用商店。这个策略在校园场景下很有用因为很多学生并不会第一时间更新应用如果不做强制更新新接口上线后老版本很容易出问题。6.3 几个“做了才知道”的心得最后分享几个个人体会比较深的地方。第一个是接口先行。双端项目最忌讳“前端写着写着发现接口不对”。我们开发前花了两天把接口文档全部定下来前后端并行开发联调时几乎没有因为接口定义问题返工。第二个是富文本的内容清洗必须在服务端做。用户或编辑把内容粘贴进来里面可能带着 Word 的垃圾样式、多余的 class。服务端入库前要统一做一次清洗去掉无用标签只保留必要的标签和白名单样式。这个清洗如果不做后期内容多了以后前端渲染容易出现各种奇奇怪怪的样式问题。第三个是不要过早铺功能。第一版能把信息流、详情、发布、审核跑通已经是一个完整的项目了。很多功能比如大数据分析、个性推荐、多角色审批链都是在有真实使用反馈之后才值得加。校园项目的用户规模有限做得再“聪明”也不如做得“稳”。最后再分享一个小细节。新闻详情页的阅读数千万别在用户打开时直接同步写数据库。先用 Redis 累加再定时批量刷到 MySQL否则遇到一次全校通知公告的流量高峰数据库连接池很容易被打满。这个优化虽然不起眼但上线后帮我们避免了一次事故也是这类“小系统大流量”场景里最值得提前做的一步。
返回列表