1. 家庭影像管理系统到底在解决什么问题
“基于Spring Boot+UNIAPP的家庭影像管理系统”这个题目,第一次看到会觉得它太朴素了:无非是上传照片、展示相册、刷一刷时间轴。但真正把一个家庭场景的影像管理系统做完就会发现,它几乎把后端文件处理、数据库设计、跨端兼容和各种真机兼容问题全都串起来了。Spring Boot负责服务端接口、权限校验和存储管理,UNIAPP负责把照片墙、上传、视频播放和分享功能一次覆盖到H5、微信小程序和App三种端,这正是大多数个人项目最需要的能力模型。
这个系统不是做一个简单的“网盘换皮”。家庭影像有非常具体的用户画像:爸妈手机里存了十几年照片,微信传图会被压缩,网盘要么收费要么担心隐私,让长辈学会用NAS又太难。孩子在外地工作,想随时翻到小时候的影像,最方便的分发方式其实就是手机小程序或App。做这套系统,核心是把“按上传时间堆文件”的思路,升级成“按拍摄时间、人物、地点、相册来组织家庭记忆”的私有影像库。
如果你正在找毕设题目,或者学完Spring Boot和Vue之后想找一个能把前后端完整串起来的练手项目,这类系统比“图书管理系统”“商城系统”更值得做。因为图书和商城的数据模型相对规则,而影像系统要处理大文件上传、异步任务、分页列表、多端预览、隐私分享,每个环节都有可以深挖的细节,写进简历也有话可说。
1.1 家庭影像数据到底“乱”在哪里
我先描述一个真实痛点。家庭影像散落得非常严重,手机相册里有一部分,家庭群聊里有压缩过的图,旧电脑里混着好几个文件夹,还有在单反、无人机和运动相机里导出来的原始照片。如果只是存文件,那问题很简单,买块硬盘或者开个网盘会员就行。但“管理”意味着要能找回来,比如奶奶六十大寿那天的合影、孩子第一次走路的视频、全家去年在某个海边城市度假的照片。这些内容最可靠的记忆锚点是“拍摄时间”和“人物”,而不是文件名。
一旦按这个要求去设计,系统的业务模型就要比普通文件系统复杂不少。一个家庭里多个成员都要上传和查看,每个人都有自己的权限诉求;一张照片可能横跨几年都要用;视频体积比照片大几个数量级,后端存储策略不能一刀切;手机端要照顾iOS和Android差异,视频编码不对就会黑屏或没声音。很多初写这个题目的人会把大量时间耗在写页面样式上,结果一测数据流就发现上传接口返回慢、视频播放卡顿、相册时间轴排序不对,本质都是数据模型和管理链路没有先想清楚。
1.2 功能边界:先跑通主线,再补齐支线
我设计和实现这套系统时的习惯是:先把核心链路走通,再考虑加分项。核心链路就是“家庭成员登录进来 -> 上传一组照片/视频 -> 后端落库并把对象存储地址存下来 -> 首页按拍摄时间倒序展示 -> 点击某一天进入详情”。这个链路完整可用之后,相册、回收站、人脸识别、分享等等功能都是在它上面做加法。
主线功能一般包括这几块:
- 账号体系:手机号注册登录,支持用户创建家庭空间并邀请家庭成员加入。
- 上传管理:批量选择照片和视频,展示上传进度,后端读取EXIF拍摄时间并生成缩略图。
- 内容组织:家庭空间下按相册维护,首页提供时间轴聚合,用户可自定义标签。
- 浏览检索:照片瀑布流、视频列表、按时间/相册/人物标签筛选。
- 防误删与回收站:删除不直接清空,而是进入回收站。
- 分享与隐私:照片可以被分享给家庭空间内的成员,或者生成一个有时效性的临时分享链接。
加分项可以放在二期,比如人脸聚类、智能生成年度回顾、视频自动转码压缩、自动备份手机相册等。这些功能听起来很吸引人,但一旦作为第一版的核心需求,整个系统会变得非常臃肿,最后要么找不到文件在哪个服务,要么流程没闭环,评分和实用性都不好。
1.3 技术方案为什么是Spring Boot加UNIAPP
很多人会问,既然做App,直接用原生Swift和Kotlin各写一套不更顺吗?对商业团队可以,但个人项目周期不够。家庭场景本身有一个特点:家人用的设备非常杂,有人是iPhone,有人是安卓千元机,有人不愿意装新App只愿意用微信里的小程序。UNIAPP的价值就在于用一套Vue代码同时编译到iOS App、Android App、微信小程序和H5,后端接口不需要变。使用Vue语法来写前端,对于一个已经熟悉前端生态的人来讲,上手成本在同类跨端方案里确实算低的。
后端选Spring Boot,最直接的理由是生态成熟。文件上传、分页、接口鉴权、定时任务这些问题在Java服务端都有很成熟的资料和中间件可以接。而且做毕业设计或项目复盘时,Spring Boot MVC的分层结构很容易讲清楚,Controller-Service-Mapper三层拆开,每个人都能看懂系统在干什么。用MySQL存业务表,用MinIO或云对象存储存文件,再用Redis处理部分异步任务,就足够支撑整个家庭影像系统的数据流。
这套方案肯定不是万能的。UNIAPP在渲染特别复杂的沉浸式页面时,多端表现会有差异;后端选Java虽然稳定,但启动内存比Go和Node服务要高。对于一个以家庭私有场景为核心的管理系统,这些代价换来的是开发效率和可维护性,这是值得的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端设计与数据建模:核心先做对
这套系统的后端结构推荐按模块拆包,而不是把所有类丢在同一个目录下。我实际做的时候用了Maven构建,项目结构大概是:
text复制family-album-server/
├── src/main/java/com/family/album/
│ ├── common 统一返回体、异常处理、鉴权拦截器
│ ├── config MyBatis-Plus、Redis、对象存储配置
│ ├── controller REST接口层
│ ├── service 业务逻辑层
│ ├── mapper 数据库Mapper
│ ├── entity 数据库实体
│ ├── dto 接口入参和出参封装
│ └── utils EXIF读取、缩略图生成、时间解析工具
数据模型是整个后端的设计核心,我不建议拿到题目就直接建一张photo表和一个user表,而是应该先抽象出家庭空间。因为影像管理不是一个人自嗨,而是全家人的共享行为;如果没有“家庭空间”这一层概念,后面做成员管理、相册权限隔离、分享范围控制会很难受。个人项目最怕的就是后期发现所有表都缺一个family_space_id字段,然后到处修补。
2.1 核心数据模型:空间-相册-影像三层结构
空间、相册、影像这三层是最推荐的分层方式。一个用户可以创建或加入多个家庭空间,比如“王家老宅”“李家大院”;每个空间下可以建多个相册,比如“2024年春节”“孩子成长记录”;影像记录挂载在相册下,同时也属于家庭空间。核心表设计参考如下。
family_space表存空间信息:id、owner_user_id、space_name、invite_code、create_time。invite_code是给家庭成员加入用的短码,比搜索手机号加好友更方便,也更贴合家庭场景。
user表是最基本的用户账号表:id、phone、password、nickname、avatar、create_time。登录尽量用手机号加密码,登录成功后后端签发JWT token,前端后续请求在Header里带token访问接口。
member表用来记录用户和家庭空间的关系,字段包括id、family_space_id、user_id、role、joined_time。role先区分owner和member就够了。一个用户如果参与了多个家庭空间,就能通过member表关联多行。
album表结构推荐为:id、family_space_id、album_name、cover_url、type、remark、create_time、sort_order。相册封面不要用单独字段存冗余,直接取封面字节数最小的图片,列表页加载更快。
media_asset表是真正的核心。字段包括:id、family_space_id、album_id、uploader_id、media_type(photo或video)、original_name、file_url、thumb_url、shoot_time、shoot_location、width、height、duration、file_size、md5、status、create_time。这里我把照片和视频放在同一张表里,是因为它们的管理行为高度相似:都要上传、建缩略图、设相册、筛时间。如果拆成photo和video两张表,首页做统一时间轴会变得很麻烦。以后如果出现完全不同类型的内容,比如电子相框的特殊格式,再扩展type字段或拆子表也来得及。
这组表之间的关系很好理解:一个家庭空间拥有多个相册和多条影像,一个用户通过member表进入空间后,按空间维度去隔离数据。SQL查询都要求带family_space_id,避免跨家庭串数据。
2.2 影像资源的存储与访问设计
初学者最容易犯的错是把文件直接存到服务器本地目录,然后数据库里存一个http://localhost:8080/upload/xxx.jpg的完整地址。开发环境下这样其实能跑通,照片也能看到,但一旦换机器、重启服务或者用Nginx部署,文件路径就乱了,而且后端和文件存储耦合在一起,扩容时处理起来很有风险。
我在这个项目里建议用对象存储来装原文件和缩略图。最省事的方案是注册一个云OSS,但个人学习环境更推荐用MinIO自建。MinIO部署起来很轻,只要Docker跑一个容器,设置好AccessKey就可以用类似OSS的SDK上传下载。数据库里不存完整访问URL,而是存对象key,比如albums/2024/02/14/avatar_001.jpg。访问的时候,后端根据角色权限临时生成预签名URL返回给前端,URL有效期设几分钟到几小时。
这样设计有个非常实际的收益:影像文件是私密的,绝不能直接把桶设置成公开读。使用临时URL后,没有登录的人拿不到访问地址,拿到的地址也很快过期;同时如果以后要换存储服务商,只要数据库里的key不变,后端切换SDK配置就行。放在真实项目中,这就是“文件存储与服务解耦”的基本功。
还有一点要强调:md5字段要留着,并且在(uploader_id, md5)上建唯一索引。家庭用户之间经常互相传同一张照片,同一部手机的照片被不同成员重复上传的概率很高。上传时先按md5查一次,如果存在就直接提示“这张照片相册里已经有了”,可以避免大量重复存储。
2.3 接口规划:按业务主线切分
后端接口尽量遵循REST风格,统一返回体Result
| 模块 | 接口路径 | 说明 |
|---|---|---|
| 用户 | POST /api/auth/login | 手机号密码登录,返回token |
| 用户 | POST /api/auth/register | 注册并创建默认家庭空间 |
| 空间 | POST /api/space/create | 创建新家庭空间 |
| 空间 | POST /api/space/join | 填写邀请码加入空间 |
| 相册 | POST /api/album | 新建相册 |
| 相册 | GET /api/album/list | 查询当前空间下相册列表 |
| 影像 | POST /api/media/upload | 上传单张图片或视频 |
| 影像 | POST /api/media/upload/batch | 批量上传 |
| 影像 | GET /api/media/page | 分页查询影像列表,可传albumId、时间范围、标签 |
| 影像 | GET /api/media/timeline | 按拍摄日期聚合时间轴 |
| 影像 | PUT /api/media | 修改影像归属相册、标签等 |
| 影像 | DELETE /api/media | 删除影像,逻辑删除进入回收站 |
| 影像 | POST /api/media/share-url | 生成临时分享链接 |
一个容易被忽略的设计是分页查询。家庭照片量上来以后,如果用List返回所有记录,前端在低端安卓机上很容易卡死,因此必须支持分页。我推荐用MyBatis-Plus,分页插件使用起来非常简单,将IPage返回给前端时统一转成自定义PageVO,而不是把数据库实体直接暴露给前端。
3. Spring Boot里几个关键实现
后端设计讲得再多,落到关键功能时还是有几个容易卡住的实现点。我挑四个最核心的部分拆开来讲,包括大文件上传、时间轴聚合、异步任务解耦,以及几个和Spring Boot版本直接相关的坑。
3.1 影像上传链路:从MultipartFile到落库
上传接口本身不复杂,核心逻辑是先收文件、再做校验、最后落库。上传接口的Controller写法也比较直观。
java复制@PostMapping("/media/upload")
public Result<MediaUploadVO> upload(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "albumId", required = false) Long albumId,
@RequestParam(value = "spaceId") Long spaceId) {
UserSession user = UserContext.get();
MediaAssetVO vo = mediaService.upload(file, spaceId, albumId, user.getUserId());
return Result.ok(vo);
}
真正需要关注的是MediaService里的处理顺序。上传接口完成后,不能只保存一个MultipartFile到对象存储,还要解析出原始元数据再入库。我实现的service核心流程是:
- 校验文件类型和大小,图片限制单张20MB,视频限制单文件200MB。
- 计算文件md5,先查一次是否重复,如果重复则直接返回已有记录并提示。
- 如果文件是图片,读取EXIF信息获取拍摄时间、设备型号和GPS坐标;如果读取不到,就尝试从文件名解析,例如IMG_20240214_183022.jpg对应2024年2月14日18点30分。
- 图片生成一套缩略图,视频则用ffmpeg截取第一帧作为封面,存到thumb对象。
- 原文件和缩略图都上传到对象存储,拿到key后插入media_asset表。
- 异步发送消息,通知人脸聚类或视频转码服务。
第3步很容易被人忽略,但家庭影像系统里拍摄时间实在太重要了。很多老照片扫描进系统时根本没有EXIF,文件名可能是时间戳,也可能是“过年全家福.jpg”,所以要做一个多策略的时间解析工具。解析不到拍摄时间时,就回退到文件的上传时间,总之不能让shoot_time为空,因为首页时间轴完全依赖这个字段排序。
MultipartFile写到对象存储时不要直接往桶里流,先落到本地临时目录,这样能读到文件大小和元数据,更稳。还要注意代码里的finally块要清理临时文件,不然持续上传跑几天后,服务器临时目录会残留大量垃圾文件。
3.2 时间轴聚合SQL推导
时间轴主页展示的是“某年某月某日”下面有多少张照片或视频,相当于在分页之外再做一个按date分组聚合的查询。这里不能用传统的GROUP BY加子查询硬做,因为还需要取当天的一张封面缩略图以及媒体数量。
我实现时写过一个类似的查询,先把当天的影像记录找出来,再关联封面:
sql复制SELECT DATE(a.shoot_time) AS shoot_date,
COUNT(*) AS media_count,
MAX(a.id) AS latest_id
FROM media_asset a
WHERE a.space_id = #{spaceId}
AND a.status = 0
AND a.shoot_time IS NOT NULL
GROUP BY DATE(a.shoot_time)
ORDER BY shoot_date DESC
LIMIT #{offset}, #{pageSize};
然后根据latest_id去查media_asset找到封面图。这里有一个常见的坑:不要直接查MAX的cover_url,因为在同一天多张照片里取哪张作为封面应该有明确策略。使用MAX(id)定位最新上传的那张作为封面,对用户来说反而更符合“最近回忆”的直觉。
另外一个数据量大之后肯定会遇到的问题是按DATE(shoot_time)分组导致索引失效。对于这个量级的个人项目,只要在shoot_time列建索引,前几万条记录跑起来都不慢;真到几十万张再考虑按年分表或另外建日期汇总表。不要一开始就过度设计。
3.3 用Redis Stream解耦人脸识别任务
家庭影像系统如果只做增删改查,项目亮点会少一些。人脸识别是这类系统最自然的加分项,但要做得顺畅,就要考虑异步任务。你想一下:用户上传完一个相册,里面有60张照片,后端如果同步调用人脸识别模型来聚类,用户会看到上传接口一直转圈,体验很糟糕,而且一旦识别服务超时,上传接口也跟着失败。把识别请求丢到消息队列里异步消费,是更稳妥的做法。
在这个项目里我选择用Redis Stream来当轻量消息队列,而不额外接RabbitMQ或Kafka。Redis在项目中本来就要用来做验证码缓存和登录token管理,再兼做Stream不增加运维负担。Spring Data Redis从某个版本开始支持StreamTemplate,核心思路是事务里把mediaId、albumId等消息体扔到stream里,后台用一个独立的消费者线程去读取并处理,这张图片就会被人脸模型处理写入face_group_id字段。
如果觉得引入Redis Stream有点重,一开始用JDK的CompletableFuture也能演示异步效果,但那种异步任务没有持久化能力,服务一重启任务就丢了。Redis Stream的好处是消息不会轻易丢,多个消费者还能分摊处理压力。对一个成绩管理系统或者家庭影像项目来说,把这个点讲清楚,会比单纯写CRUD有深度得多。
3.4 接口参数、跨域与接口文档的常见坑
接口写完不等于能用,我调试过程中遇到过几个典型坑。第一个坑是LocalDateTime序列化问题。前端默认收到的是“2024-02-14T18:30:00”这种ISO格式,但小程序端很多组件解析不了带T的字符串。要在application.yml里统一配置:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
第二个坑是跨域。开发环境下,H5运行在localhost:5173,后端在localhost:8080;微信小程序虽然不存在浏览器跨域,但真机调试请求仍可能走代理;App端则要看是否配置了合法域名。为了开发方便,我通常在WebMvcConfigurer里配置全局CORS,只允许自己前端的来源,线上再收紧。
第三个坑也是网络搜索里高频出现的:springfox 3.0.0和Spring Boot 2.6以上的版本不兼容。Spring Boot 2.6开始把Spring MVC默认的路径匹配策略从AntPathMatcher改成了PathPatternParser,而springfox 3.0.0还依赖旧策略,启动时会报空指针。解决方法是:
yaml复制spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
不过只写在这有些治标不治本,新项目更建议直接用springdoc-openapi,依赖引入简单,和Spring Boot 3也更契合。如果坚持用Spring Boot 2.7加springfox,那就在配置里加这个参数。
上传文件大小限制也经常在上线后成为隐形问题。Spring Boot默认单文件上传最大只有1MB,视频根本传不上去,所以配置里需要设置为:
yaml复制spring:
servlet:
multipart:
max-file-size: 200MB
max-request-size: 500MB
4. UNIAPP多端前端:一套代码适配三种端
UNIAPP是整个项目里离用户最近的部分,页面效果和交互直接影响家人愿不愿意用。这里需要先想清楚一个问题:手机端的主要入口是“浏览”还是“上传”?我的答案是浏览优先。长辈打开App第一件事是想看最近家里人传了什么,然后才是自己上传照片。因此首页的时间轴必须流畅,照片墙要懒加载,进入详情页看原图要快。
使用HBuilderX创建uni-app项目后,前端的核心工程目录一般是这样:
text复制family-album-app/
├── pages/
│ ├── index/index 首页,时间轴照片流
│ ├── album/list 相册列表
│ ├── album/detail 相册详情
│ ├── upload/upload 上传页
│ ├── user/login 登录页
│ ├── user/mine 我的页
│ └── media/detail 影像详情与大图预览
├── static/ 静态资源
├── utils/request.js 请求封装
├── App.vue
├── main.js
└── pages.json 路由和TabBar配置
4.1 页面路由和TabBar怎么配置
pages.json和Web项目的路由概念不完全一样。页面要显示在底部TabBar里,需要在pages.json的tabBar.list里配置,list最少2项、最多5项,我设置成首页、相册、上传、我的四个Tab。但有一个细节:在App端,中间“上传”按钮往往希望突出样式,变成一个凸起按钮,这时不能简单放在tabBar里,而是需要自定义tabbar或者用cover-view实现。如果只想快速跑通,把上传放到TabBar里也无妨,注意图标尺寸按官方建议准备81x81像素的PNG。
路由传参会有人问“uniapp中获取路由参数怎么拿”,页面跳转这么写即可:
js复制uni.navigateTo({
url: '/pages/album/detail?albumId=123&name=2024春节'
})
目标页面在onLoad(options)中直接取options.albumId和options.name。但如果参数太长,或包含特殊字符,建议先encodeURIComponent。传对象时不要试图直接拼到url里,最好把对象缓存在globalData或storage里,url只传id。
4.2 瀑布流照片墙与滚动加载
打开首页的体验,很大程度上取决于列表页怎么渲染照片。如果一次性拿100条数据,然后循环渲染多个image标签,每条图片原图都来自对象存储URL,在低端安卓机上页面会卡死甚至白屏。这里的正确做法是:后端分页只返回缩略图URL(thumb_url),点击图片再加载原图。图片标签要配合lazy-load属性,并且用mode="aspectFill"来统一裁切。
瀑布流可以用左右两列来模拟。请求到一页数据后,比较左右两列当前累计高度,把新图片放到较矮的一侧,这样视觉上不会出现某列无穷长的情况。在uni-app中不需要操作真实DOM,通过v-for渲染数组即可。我实现的伪代码思路是:
js复制let list = []
function appendPage(items) {
let leftHeight = 0
let rightHeight = 0
let leftCol = []
let rightCol = []
items.forEach(item => {
if (leftHeight <= rightHeight) {
leftCol.push(item)
leftHeight += item.height || 200
} else {
rightCol.push(item)
rightHeight += item.height || 200
}
})
this.leftList = this.leftList.concat(leftCol)
this.rightList = this.rightList.concat(rightCol)
}
页面滚动到底部时,通过onReachBottom触发下一页加载,这个方法在uni-app中只要页面配置了scroll-view或整个页面滚动到底就能触发,不需要自己写滚动监听。上拉加载时记得加一个isLoading标志位,防止onReachBottom连续触发多次请求。
4.3 视频播放限制与滑出可视区自动暂停
如果视频列表允许同时播放多个,安卓端很容易出现背景多个声音叠加的惨剧,因此要限制同一时间只能播放一个视频,并且当视频滑出可视区域后自动暂停。这里的关键是拿到当前视频组件的context,然后调用pause方法。
示例代码如下,假设页面中每个视频都带一个video-id,用一个currentVideoId记录正在播放的视频:
js复制onPlay(e) {
const videoId = e.currentTarget.dataset.id
if (this.currentVideoId && this.currentVideoId !== videoId) {
this.stopAllVideo()
}
this.currentVideoId = videoId
}
stopAllVideo() {
const query = uni.createSelectorQuery().in(this)
query.selectAll('.video-item').boundingClientRect(rects => {
// rects 是所有视频节点,找到可视区域外的节点做pause
}).exec()
// 或者循环遍历通过uni.createVideoContext暂停
}
更稳妥的做法是通过页面滚动来暂停离屏视频。在scroll-view或页面的@scroll事件里,获取到这个视频元素距离视口的位置,如果rect.bottom小于0或rect.top大于屏幕高度,说明已经滑出可视区,立即执行videoContext.pause()。这段逻辑要注意节流,否则滚动过程会反复触发导致卡顿。真机上测过之后,视频播放体验基本能达到正常使用标准。
还有一个容易被忽略的点:视频源文件的编码。如果是手机直接拍摄的视频,大部分MP4格式可以正常播放;但有些相机录制的视频编码不是H.264,在部分安卓WebView上会显示黑屏或无法播放。建议后端在异步任务里将上传的视频统一转成H.264编码的MP4格式,并限制分辨率到一个合理的水平,比如1080p。这个点我放到后端异步任务里处理,但你在做页面兼容时要提前知道核心原因。
4.4 manifest配置与打包细节
uni-app的manifest.json配置好不好,直接影响打包后的App能否正常使用相机和相册权限。里面需要重点检查的是App模块配置和隐私协议弹窗。iOS上架商店要求首次启动时展示隐私政策,而且必须让用户有“同意”和“不同意”的选项,如果用户不同意,需要退出App而不是继续使用。Android敏感权限(拍照、读取相册、录音)也必须在有明确业务场景时再申请,不能一进App就把所有权限要一遍。
在打包前,要生成数字证书。Android证书用keytool生成jks文件;云打包或本地打包时需要填写证书别名和密码;iOS则需要p12证书和描述文件。用HBuilderX的云打包确实方便,但App运行在部分安卓机上会对广告SDK有要求,如果包体里带了推送或统计模块,上架前需要确保隐私政策写得足够清楚。
发行到安卓应用市场时,最常见的驳回原因是隐私政策没有在启动时弹出,或者权限说明与实际功能不匹配。解决方法是配一个“隐私政策弹窗”页面,在App.vue的onLaunch里判断是否首次启动,首次启动先弹窗展示协议,用户点击同意后,才执行后续的登录跳转和权限申请。
5. 部署、监控与上线:从开发到可演示
很多项目的开发代码写得很顺,但一到“部署到服务器让别人演示”就出问题。这个部分我把整个项目从命令行运行到上线监控的路径完整梳理一遍,照着做基本能避免最基本的部署事故。
5.1 Maven方式运行与Tomcat部署
后端我使用Maven构建,这在IntelliJ IDEA里非常顺。如果想命令行启动开发环境:
bash复制mvn clean install
mvn spring-boot:run
大多数时候开发用IDEA里Run Application类就够了,但如果你想把后端跑在服务器上,应该打成一个可执行jar包:
bash复制mvn clean package -DskipTests
java -jar target/family-album-0.0.1-SNAPSHOT.jar
Spring Boot默认使用内嵌Tomcat,因此只要服务器装了JDK 8或17(取决于Spring Boot版本),不需要再单独装Tomcat,直接java -jar就能跑起来。有人问Spring Boot项目怎么以外置Tomcat部署,也可以,但需要额外做两步:把pom.xml的packaging改成war,并且让启动类继承SpringBootServletInitializer并重写configure方法。我通常会建议直接用内嵌Tomcat,因为维护成本低。
如果服务器上想用域名加HTTPS,我不会建议直接改Spring Boot的server.ssl配置,而是让Nginx监听443端口做SSL终结,再反向代理到本机的8080端口。Nginx层还可以配置上传文件大小限制和图片缓存,Object Storage的URL也可以走CDN。
5.2 用Actuator和Micrometer盯着运行状态
家庭影像系统跑在服务器上后,不能等到用户说“页面打不开”才去登录服务器看日志。引入Spring Boot Actuator和Micrometer是最快能获得基础可观测性的方式。pom.xml里加依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
配置文件里开放health和metrics端点:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
启动后,访问/actuator/health能看到服务是否存活,访问/actuator/prometheus能获取Prometheus格式的指标。虽然家庭系统不一定真去搭一套Grafana大屏,但熟悉这个链路对以后工作很有帮助。Micrometer的意义在于它提供了一套统一度量标准,Spring Boot的自动配置能自动采集JVM内存、线程池、Tomcat连接数等数据;配合Actuator可以直接在调用链里看到服务状态。
如果你只用Actuator,不需要Micrometer也能做健康检查。但使用Micrometer可以把指标用Prometheus拉走,这是目前Java服务可观测性标准玩法。
5.3 uniapp安卓上架应用市场的注意事项
上架应用市场这个话题,很多热词里也会出现。光会云打包出一个apk还不行,国内安卓市场审核基本要求:
- App具备软件著作权或相关资质。
- 隐私政策网址可访问,并且能对应到App内的隐私弹窗内容。
- App内用户协议、隐私政策、权限说明必须完整。
- targetSdkVersion尽量保持较高,低版本不满足新机适配要求。
上架流程上,先去各应用市场注册开发者账号,然后上传APK或AAB,填写应用名称、图标、截图、应用介绍等。部分市场会要求提供测试账号,因为系统需要登录后才能使用,所以要准备一个演示账号给审核人员。要注意审核人员在测试时如果通过App首次启动就访问了大量权限,很可能被判定违规,因此登录页和应用首页尽量做到不用权限,把相册权限延后到真正上传时才申请。
打包时如果下载云打包的apk没签名,会无法安装或上架失败,所以签名文件要妥善保存。HBuilderX有“使用云端证书”选项,但为了以后可持续升级,还是建议生成自己的本地证书,并将证书密码记录在安全的位置。个人开发者的证书一旦丢失,后面应用升级会很难处理。
6. 我在这个项目里踩过的坑和心得
一路做下来,遇到的问题比开始预想的多很多。把这些问题和排查方法分享出来,比贴一堆成功代码更能帮人少走弯路。
6.1 我在照片和时间上踩过的三个坑
第一个坑是重复上传。家里人很喜欢同一张照片发到家庭群,再由几个人分别传到系统。后来我在media_asset表加上md5字段,上传前先查一遍,从源头过滤重复,存储空间没有白白浪费。
第二个坑是时区。服务器在日本或欧洲时区跑的时候,后台看shoot_time还算正常,前端却显示差8小时。因为手机传上来的时间戳是UTC,后端入库转换时没指定时区。最后在Jackson配置和JDBC连接串里都显式指定了serverTimezone=Asia/Shanghai,问题解决。家庭系统虽然用户都在国内,但很多服务器默认可能是UTC,不指定时区一定会出问题。
第三个坑是大批量删除。批量删除影像时有的人喜欢循环调用delete接口删个几十次,这样事务效率很低。后来我批量删除接口改成传入id列表,用一条UPDATE把status置为回收站,而不是物理DELETE。这样既保留恢复能力,又不需要在用户后悔时找备份。
6.2 如果想继续扩展,值得先做的三件事
把这个项目继续做深,我建议优先做三件事:一是人脸聚类,上传后自动识别出人物并为人物建立独立相册,这项功能最能体现“智能”两个字;二是自动备份,App端可以引导用户开启自动备份相册功能,后台监听新增图片并上传;三是智能剪辑,按年、按人物、按地点自动生成短视频回顾。
人脸聚类接入不需要从零训练模型,可以使用开源模型或云服务接口,只要在后端异步任务里处理好结果写入即可。自动备份要注意iOS后台限制和用户隐私授权问题,不要偷偷上传,必须有明确的开关和提示。智能剪辑可以在服务端用ffmpeg拼视频,难度也不在算法而在素材筛选逻辑。
最后分享一点体会。这类家庭系统的核心不是功能炫酷,而是数据可靠性。做了一堆功能,结果上传的照片因为文件路径不对、对象存储key写错或者数据库逻辑删除误操作导致丢失,这是致命的。所以我个人会把大部分时间花在数据校验、事务边界、唯一索引、回收站这四个地方。它们看起来没有界面效果那么直观,但在真实使用时才能真正决定家人愿不愿意长期把回忆托付给这个系统。
