整个云图库项目的后端拆分为以下几个核心模块:
各模块之间的关系如下:
在云图库项目中,我根据业务逻辑与访问场景分析,共设计了几张关键表:用户表(user)、图片表(picture)、空间表(space)、空间成员表(space_user) 等。我在设计库表时会综合查询性能、存储空间、可读性、开发成本进行考虑,下面简单说明:
1)用户表(user)
2)图片表(picture)
3)空间表(space)
4)空间成员表(space_user)
uk_spaceId_userId,保证同一用户在同一空间下只有一条成员记录我在开始编写云图库项目后端时,先使用 Spring Initializr 在 IDE 中创建一个基础的 Spring Boot 项目,并在 pom.xml 文件中整合一些常用依赖,比如 Spring Web、MyBatis Plus、MySQL、Lombok、Knife4j 等,初步搭建起项目骨架。
接着会配置数据库连接(比如 application.yml 中指定 MySQL 地址与登录信息),并进行一些全局设置,如日志输出、逻辑删除、全局跨域配置等。然后,我会新建一个自定义异常类、全局异常处理器,以及统一的响应封装类,用来标准化接口返回格式。
最终,这些基础配置、依赖和统一处理逻辑就形成了一个 “后端项目模板”,后续在编写用户模块、图片模块、空间模块等业务功能时,我只需在这个模板之上扩展即可,避免了大量重复的初始化和基础代码编写工作。
💡 补充:如果你熟悉鱼皮提供的后端万用模板,还可以补充更多业务特性,比如提供 CRUD 操作、代码生成器、Redis 和 Elasticsearch 的整合等。
MyBatis Plus 是对 MyBatis 的增强工具,提供了丰富的 CRUD 接口、分页插件、动态查询构造器等,能大幅减少编写 SQL 的工作量并提升开发效率。
在我的云图库项目里,我首先在 pom.xml 中引入 MyBatis Plus 的依赖,配置了 @MapperScan 注解以扫描 mapper 包,之后就可以在代码中直接使用内置的 BaseMapper 方法(如 selectById、insert 等)快速实现数据增删改查。借助 MyBatis Plus 的 QueryWrapper 或 SqlRunner,还可以更灵活地构建动态查询。比如,我会在图片模块使用 lambdaQuery().eq(Picture::getSpaceId, spaceId) 的方式查询特定空间的图片列表。
为了进一步提高构建 MyBatis Plus 查询的效率,我封装了一些 getQueryWrapper 的方法,可以根据查询请求对象转化为 QueryWrapper。
为了统一管理后端异常,我在项目里封装了一个自定义异常类(BusinessException)、全局异常处理器(GlobalExceptionHandler)、并且自定义了一些错误码(ErrorCode),便于前端统一处理错误。
流程如下:
这样做可以让前端快速定位问题原因,也便于后续在不同场景下进行精细化的异常处理。
在项目开发中,接口可能会返回不同类型的结果,如果不统一格式,会给前后端对接带来混乱。为此,我自定义了一个 BaseResponse<T> 类,包含 code、data、message 字段,并提供 ResultUtils 工具类来快速生成成功或失败的响应。
举个例子,当接口正常执行时,我会使用 ResultUtils.success(data) 返回结果,如果出现业务异常或系统异常,则通过 ResultUtils.error(...) 构造标准化的错误信息。这样前端开发者只要解析 code、data、message,就能统一处理各种接口的响应,大幅提升了协作效率和可维护性。
跨域问题是指浏览器为了安全,对不同域(包含不同协议、不同端口或不同主机名)的请求进行限制,从而导致请求无法正常访问后端接口。
在云图库项目中,为了方便前后端分离部署与调试,我创建了一个全局跨域配置类(CorsConfig),在其中通过 registry.addMapping("/**") 等配置项,允许指定源的跨域请求,并支持发送 Cookie、自定义请求头等。这样一来,无论前端运行在哪个端口,都能正常请求后端,便于本地开发和后续部署上线。
Knife4j 是基于 Swagger 的一款接口文档增强工具,提供了更友好的界面和丰富的文档管理功能,比如动态参数调试、接口分组等。
在我的云图库项目中,我首先在 pom.xml 中引入了 knife4j-openapi2-spring-boot-starter 依赖,然后在 application.yml 中配置了 Knife4j 扫描的包路径和接口文档信息。
在编写 Controller 接口时,我会使用 @ApiOperation 等注解描述接口和参数。启动项目后只需要访问 /doc.html,就能看到自动生成的可视化接口文档,前后端都能根据这份文档进行测试与对接,从而大幅提高开发效率。
在云图库项目中,如果后端返回的主键 id(如 Long 类型)超过了 JavaScript 能正常表示的安全整数范围(2^53 - 1),前端在处理响应时就会出现数字精度丢失的问题。常见现象就是数字的末位被置为 0,从而导致前端拿到的数据与实际值不一致。
为了避免这种情况,我在后端的配置层中,对整个项目的 JSON 序列化做了一层自定义处理。通过编写一个全局的 Jackson 配置类,将所有返回给前端的 Long 类型转换为字符串进行输出。这样一来,前端接收到的长整型 id 就是一个字符串,就不会再出现因超出 JS 范围而丢失精度的情况。
在云图库项目的用户模块中,我对用户注册做了以下几步处理:
前端发起的注册请求,会映射到 UserController 中的 /register 接口,控制层做完参数判空后就交给 UserService 的 userRegister 方法来处理。注册成功后,会返回新用户的 id 给前端确认。
在云图库项目中,我采用了基于 Session 的登录态管理实现用户登录,实现步骤如下:
request.getSession().setAttribute(USER_LOGIN_STATE, user)。这样,当后续请求携带同一个 Session ID 时,后端即可根据 Session 判断当前用户的登录状态,从而进行权限判定。若 Session 中无登录态,则视为未登录。
云图库项目有普通用户、管理员 2 种角色,为了简化权限控制,我做了以下工作:
@AuthCheck 注解,用于标注某个接口需要的角色;然后利用 Spring AOP 写了一个拦截器(环绕通知),在接口调用前自动校验当前登录用户是否拥有足够的权限。@AuthCheck(mustRole = "admin"),就能统一检查用户是否是管理员。若校验失败,就抛出无权限异常。这样一来,权限管理逻辑不会分散在各种接口里,而是集中在一个切面类中维护,可读性高、扩展性也更好。若后期要增加新的权限等级或更细粒度的控制,也能在切面逻辑里统一加以实现。
在云图库项目中,我选择使用对象存储来存储图片文件。一方面,借助云厂商(如腾讯云 COS)提供的对象存储可以快速解决海量图片的存储、传输、扩容和备份问题;另一方面,腾讯云对象存储还提供 “数据万象” 图像处理服务,可以在图片上传时自动解析并返回图片的尺寸、大小、格式等元数据信息。
具体实现流程:
1)项目中整合对象存储:
cosClient.putObject 方法实现上传文件到存储桶。2)自动获取图片信息:
这样一来,图片实际的二进制数据并不放在后端服务器或数据库中,而是托管给对象存储服务进行管理,既省去了服务器磁盘维护的繁琐,又能利用云存储高可用的特性,提升系统的稳定性和可扩展性。
为了避免用户上传的图片内容违规或存在版权风险,给图片模块增加了 “审核功能”。
整体策略包括:
这样一来,既能保证图片内容的安全与合规,也能将违规图片拒之门外,提高系统整体质量。
此外,其实我还了解更多企业中的审核策略,比如:
在云图库项目中,我不仅支持用户上传本地图片文件,也支持直接填写网络图片的 URL 链接,后端自动下载并导入到对象存储中。大致流程如下:
HttpUtil.downloadFile 将图片下载到本地临时文件。其实步骤 4 和 5 都可以复用我已经实现的本地图片上传功能,因此我也将两种文件上传方式使用模板方法设计模式重构,减少了重复代码,并提高了代码的可扩展性和可维护性。
模板方法设计模式是一种行为型设计模式,它将某一通用流程的各个步骤抽象出来,并定义在父类的 “模板方法” 里,而将具体差异的实现细节留给子类来完成。简单来说,就是 “固定整体流程,允许子类覆盖部分步骤”。
在云图库项目中,我使用了模板方法模式来统一处理图片上传流程:
FilePictureUpload 和 UrlPictureUpload,只需要在 “校验图片”、“获取文件名”、“下载 / 处理文件” 这几个与输入源相关的步骤上实现不同的逻辑即可,其他步骤则在模板父类里复用。这样,不仅减少了大量重复代码,也让后续扩展新的文件来源时更加容易 —— 只需继承抽象类,覆盖相关方法即可,实现了对扩展开放、对修改封闭的设计思路。
核心思路是:利用 Jsoup 实现 Bing 网页图片抓取,并将图片进行批量上传和入库。
具体步骤:
通过这套批量抓取方案,管理员可一键快速引入大批外部图片,大大减少了前期项目冷启动时手动上传图片的工作量。
为提升云图库的性能并降低成本,我对图片查询、上传、加载、存储等环节进行了多方位的优化。
1)图片查询优化:
2)图片上传优化:
3)图片加载优化:
4)图片存储优化:
经过上述优化,系统整体的性能显著提升,同时有效地控制了存储与带宽成本。
Redis 是一种高性能的分布式 KV 存储,支持丰富的数据结构和高并发访问。它能把缓存数据存储在内存中,单节点读写 QPS 可达 10 万,常用于做分布式缓存或分布式锁。
Caffeine 是 Java 主流的本地缓存库,运行在 JVM 内部,访问速度比 Redis 更快,但仅能在单个服务实例内使用,无法在多台服务器之间共享数据。
我在云图库项目中将二者结合,形成多级缓存:
流程如图:
这样既保证了本地缓存的高速访问,又利用 Redis 做到跨节点数据一致和高可用,提升性能的同时减少对数据库的重复查询。
主要是利用腾讯云的数据万象服务实现。
1)转码压缩:我在后端集成了云存储服务的数据万象功能。图片上传时,借助对象存储 SDK 给出图片处理规则,如格式转换为 WebP、调整质量等,从而大幅节省带宽、加速用户加载。
2)缩略图生成:同样在上传时,利用数据万象配置额外的 “缩放规则”,再输出一份小尺寸缩略图,并将缩略图地址和原图地址一同写入数据库。 用户在图片列表页只需加载体积很小的缩略图,点击进入详情页时再加载清晰度更高的主图,从而获得更快的页面首屏速度和更好的用户体验。
通过这两步,保证了图片在存储和网络层面都尽可能地 “瘦身”,又能满足用户的浏览需求。
CDN 内容分发网络是通过将文件分发到全球或全国各地的加速节点,让用户就近从最近的节点访问资源,减少跨网络、跨地域带来的延迟,提高加载资源的速度。
在云图库项目里,我将 COS 对象存储作为 “源站”,再给图片地址配置一层 CDN,起到了这些作用:
使用 CDN 后,我也用 F12 网络控制台看了下请求资源的加载时间,确实加载更快了,也更安全稳定。
降频存储指的是将长时间不访问或访问极少的文件从 “标准存储” 自动迁移到 “低频存储” 或 “归档存储” 的过程,降低资源占用成本。
COS 对象存储提供了 “生命周期管理” 功能,可以根据设定的规则自动识别并迁移不活跃的数据。
在云图库项目中,图片在上传初期可能访问量较高,但随着时间推移,不少图片会排到后面的分页中,几乎不会有用户访问。对这类 “冷数据” 图片设置降频存储后,日常占用的存储费用会更低,帮助我们显著节省成本。只有当用户重新访问这些冷数据时才会触发取回费用,但这种情况通常很少,所以我还是开启了降频存储策略。
文件秒传主要运用于大文件上传场景,是通过 “文件指纹” 来识别文件是否已上传过的一种技术。若已存在相同文件,就无需再次传输,直接复用之前的文件存储记录,大幅减少上传时间和流量消耗。
我的实现思路:
在云图库项目中,图片文件不大,因此秒传的收益有限。但如果有用户可能频繁上传同一大文件,通过这种方式可减少重复上传,节省存储与带宽成本。
1)文件分片上传:将一个大文件拆分成多个小块(分片),逐一上传到服务器。这样可以并发上传各分片,提高带宽利用率,同时若部分分片上传失败,仅需重传失败的分片,避免整体重传。
2)断点续传:在网络中断或上传过程中出现异常时,客户端可从已上传成功的分片或位置继续上传,而不必从头再来,实现更加稳定、高效的大文件上传。
在云图库项目中,我基于 COS 对象存储服务的 SDK 来实现分片上传与断点续传。只需在调用接口时指定分片参数,后端会自动管理各分片的合并与校验。若中途中断,下次上传请求带上已上传好的分片信息,继续传剩余分片即可,从而显著提升网络不稳定场景下的用户体验和上传效率。
在云图库项目里,“空间” 相当于给用户或团队划分出的一块独立存储区域,用来存储私有或需要共享的图片。与公共图库相比,用户在自己创建的空间内对图片有更高的自主管理权限,比如无需管理员审核。同时,为了防止用户无限制占用存储资源,我给每个空间设置容量与图片数量上的额度限制。
如果没有 “空间” 的概念,可能只是在图片表里简单地通过 userId 区分用户的图片。但这样做难以实现以下场景:
因此,抽象出 “空间” 概念后,公共图库和私有空间互不干扰,也便于后续扩展团队空间和空间成员功能。
我在云图库项目采用了 加锁 + 事务 的方式来限制用户仅能创建一个私有空间:
为了防止用户无限制占用存储资源,我在云图库项目里为每个空间都配置了最大图片数量(maxCount)和最大总大小(maxSize)两种限制,并且在图片上传和图片删除环节都做了相应的更新与校验:
通过 上传前校验 + 事务内原子更新 的方式,即使在并发场景下,也能让空间用量处于可控状态,一旦某个空间达到了最大额度就禁止再传图,大幅降低系统被滥用的风险。
此外,参考大厂云服务的设计,我允许用户小幅超出额度(比如上传最后一张图时,即使会超额也允许上传),一定程度上减少了开发难度,并提升了用户体验。
在云图库项目里,我通过封装图片查询请求类,并在查询接口增加多种筛选条件,来实现多维度的图片搜索。主要思路:
queryWrapper.like(StrUtil.isNotBlank(name), "name", name) 等。前端在搜索表单里,可将这些搜索条件分为常用筛选(关键词、标签、分类)和更多筛选(日期、宽高、格式)等,实现灵活的多维度检索。
这样就能满足用户 “按关键字 + 标签 + 日期 + …” 多种组合搜索需求,大幅提升查询效率和使用体验。
我在云图库项目中采用了 实时爬虫 的方式实现以图搜图。流程如下:
在实现层面,我为这些步骤(获取页面地址、解析 HTML、拉取图片列表)各自写了一个独立的 API 类,然后用 门面模式 将它们统一封装在 ImageSearchApiFacade 类中,对外只暴露一个 searchImage 方法,简化了调用流程,用户只要传入图片 URL 就能得到以图搜图的结果。
门面模式(Facade Pattern)是一种结构型设计模式,通过提供一个统一的门面接口,把内部复杂的子系统接口隐藏起来,从而简化客户端与子系统的交互,降低耦合度。
我在云图库项目的 “以图搜图” 功能中就用到了门面模式:
ImageSearchApiFacade.searchImage(imageUrl) 就能得到最终的相似图片列表,而不必关心内部分多个 API 去爬取、解析、获取搜索结果的细节。这样做既减少了重复代码,也让功能调用更直观,符合 “高层接口简化、子系统细节隐藏” 的门面模式思想。
整体思路是:先提取和存储每张图片的主色调,再用欧氏距离算法来判断颜色相似度。
详细流程:
distance = sqrt((R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2),距离越小表示越相似。这样,前端用户只要操作一个颜色选择器,就能在空间内找出和指定颜色最接近的图片,大大提高搜图效率。
为了提高空间图片的管理效率,我提供了批量编辑功能,包括批量修改图片的分类、标签、名称等,能安全快速地完成大量图片的更新操作。
实现过程主要分两步:
通过下面几个方法,可以优化批量编辑的性能:
我在云图库项目中采用了阿里云百炼的大模型服务来实现 AI 扩图功能,流程如下:
1)选择 AI 大模型:我调研了很多国内外的 AI 绘画大模型,发现阿里云百炼提供图像扩展 API,可以通过 HTTP 接入并且响应较快,还提供了一定免费额度,所以选择它。
2)异步调用:该 AI 接口需要先创建任务,然后轮询查看任务状态,又分为 2 个步骤:
使用 AI 服务的注意事项:
在云图库项目中,我针对空间和公共图库的图片数据,开发了多种分析能力,包括空间资源使用分析、图片分类与标签分布、图片大小分布统计、用户上传行为分析,以及管理员可用的全局空间排行分析等。
对于分析类需求,开发流程都是类似的:
通过这些分析能力,用户可以清晰掌握自己空间的使用状况和图片分布,管理员也能整体观察系统资源利用率,并做好限额管控和数据运营。如果数据量大,还可进一步用 Redis / 定时任务等手段做缓存或离线计算,提升统计性能与响应速度。
我在项目里新建了一张 space_user 表,记录 spaceId 和 userId 的多对多关联关系,并附带 spaceRole 字段标识了用户在该空间的角色(viewer / editor / admin)。
成员管理操作比较简单,就是编写增删改查接口 + 必要的权限校验。需要注意的是,创建团队空间时,要先在 space 表插入团队空间记录,再自动往 space_user 表插一条数据,赋予创建者管理员角色。
在云图库项目里,空间有 “私有空间” 和 “团队空间” 两种类型,后者需要不同成员进行协同管理和操作,所以必须提供更细粒度的权限控制,比如谁能上传图片、谁能删除图片、谁能管理成员等。为此我使用基于角色的权限管理(RBAC)来实现团队空间的权限管理,并配合 Sa-Token 框架实现统一的注解式权限校验。
实现步骤:
1)空间角色与权限的定义:根据团队空间中的操作场景,设计了权限和角色。并且以 JSON 配置文件的方式存储角色和权限的关系,避免了新建数据表和关联查询。
2)空间成员管理:对于团队空间,通过空间成员关系表实现多对多关联,并标注用户在该空间的角色。
3)Sa-Token 鉴权框架:基于 Sa-Token 的 Kit 模式实现了多账号体系的 RBAC 权限控制,通过从请求上下文中获取参数实现了统一的权限校验逻辑,并运用注解合并简化了鉴权注解的使用,轻松实现方法级别的权限校验。
举个例子:
@SaSpaceCheckPermission("picture:delete") 标记敏感操作接口,让框架在调用方法前自动做校验。StpInterfaceImpl#getPermissionList(...) 方法,这里读取空间上下文信息 + space_user 表的角色,再用通过角色权限关系获取到最终的权限列表,如果不包含 "picture:delete" 就直接抛异常。这样就避免了每个接口都手写 “if-else” 或编码来判断权限。
4)前后端交互:为了前端能根据权限隐藏或显示某些按钮,我在 “获取空间详情” 和 “获取图片详情” 时,额外返回 permissionList 字段,让前端知道当前用户对该空间 / 图片拥有哪些权限,从而进行动态 UI 控制。
对于复杂的权限控制场景,可以采用经典的 RBAC 权限控制模型(基于角色的访问控制,Role-Based Access Control),核心概念包括 用户、角色、权限。
这样一来,就可以灵活地配置用户具有的权限了。
一般来说,标准的 RBAC 实现需要 5 张表:用户表、角色表、权限表、用户角色关联表、角色权限关联表,还是有一定开发成本的。由于云图库项目中,团队空间不需要那么多角色,可以简化 RBAC 的实现方式,将角色和权限直接定义到 JSON 配置文件中,在项目启动时读取配置文件到 Java 对象中,然后基于 Sa-Token 权限框架实现具体的校验逻辑。
分库分表是一种将数据拆分存储在多个库或多张表的策略,可以减小单表体量、提升高并发访问性能,并且减少单点故障。
云图库项目中,团队空间的图片可能非常多,如果全都往同一张 picture 表里插入,后期查询和维护都会遇到性能瓶颈,所以考虑把特定空间(如旗舰版团队空间)的图片单独存到分表里。
普通的静态分表要事先建好 N 张表,但项目里团队空间数量不固定,所以我基于 Apache ShardingSphere 实现动态分表。
picture_{spaceId} 表,并维护一份可用分表节点列表剩下的事情交给框架就好,会自动将 SQL 查询改写为到对应分表进行查询。
Apache ShardingSphere 是一个提供分库分表、读写分离、分布式事务等功能的开源数据库中间件,支持多种模式(ShardingSphere-JDBC、ShardingSphere-Proxy)和灵活的分片配置方式。
我选择它的 ShardingSphere-JDBC 模式实现分库分表,主要原因是:
因为 ShardingSphere 默认依赖 actual-data-nodes 配置去识别可用表,但是如果提前不知道 spaceId 列表就无法编写固定的节点配置。
在动态分表场景下,每新增一个团队空间就会生成一张新的分表 picture_{spaceId},必须通知 ShardingSphere “又多了一个可用节点”。因此需要在后端写一个分表管理器来手动维护可用的分表节点,它主要做了下面几件事:
picture_{spaceId} 分表actual-data-nodes 配置不这么做的话,ShardingSphere 并不知道新的分表存在,查询时就会报找不到分表的错误。通过手动维护可用节点,就能让 ShardingSphere 在运行中支持随时增加分表。
我在已有的图片编辑功能上,结合 WebSocket 和 事件驱动 思想,让多位团队成员能够实时查看对同一张图片的编辑变化。关键的实现点是:
WebSocket 是一种 全双工(双向)通信协议。和传统 “请求 - 响应” 的 HTTP 不同,WebSocket 在初次握手后会保留一个持久连接,客户端和服务器可彼此 随时主动 发送消息,而无需频繁重建连接。
在协同编辑场景中,需要 高频双向 的消息传输 —— 编辑者的操作需要立刻同步给其他用户。如果用 HTTP 就得不断轮询或发起请求,浪费带宽、且延迟高;而使用 WebSocket 保持长连接,可以低延迟地推送彼此的编辑信息,满足实时协同需求。
事件驱动设计通过将系统内的操作抽象成 “事件”,当事件发生时,把事件交给异步处理器或消息总线,然后通知其他需要接收的组件或用户,从而 解耦 生产者与消费者,并提高并发性能。
在我的协同编辑实现中,每次用户的编辑动作都被视作一个事件:
1)WebSocket 握手拦截器:在真正建立 WebSocket 长连接前,会进行一次 HTTP Upgrade 请求。我在握手拦截器中校验用户是否已登录、有无团队空间的编辑权限,然后把用户、图片 ID 等信息放到 Session 属性里,这样就能在后续通信中直接复用这些会话属性;如果不符合条件则拒绝握手。
2)编辑锁机制:同一时刻只有一个用户能 “进入编辑” 该图片,其余用户只能接收消息并实时浏览效果,但不能操作。这样可以从源头避免并发冲突,不需要维护复杂的实时合并算法。当持锁用户退出或断连时自动释放锁,其他人才能进入编辑状态。
Disruptor 是 LMAX 公司开源的一个 高性能无锁环形队列 框架,常用于高并发、低延迟的消息处理场景。它通过环形缓冲区 + 无锁(CAS + 内存屏障)减少线程切换,来显著提升吞吐量。
在协同编辑中,后端 WebSocket 原本同步接收和处理消息,若并发用户多、单条消息处理耗时长,就易造成阻塞,无法处理更多请求。引入 Disruptor 后,将消息生产与消息处理解耦:
这能保持 WebSocket 主线程轻量快速,不易被阻塞,也能提高实时响应能力。
我在项目中使用 Disruptor 的步骤:
优雅停机指在应用关闭前,不再接收新的请求 并 等待系统中正在进行的任务全部完成后,才真正停止进程。这样可以确保数据或状态不会在停机时刻中断或丢失,避免产生乱序或资源泄露等问题。
Disruptor 本身提供了优雅停机方法。我利用 Bean 的 PreDestroy 注解,在应用关闭前,先调用 disruptor.shutdown(),然后 Disruptor 本身会等待环形队列内的所有事件处理完才返回,同时拒绝新的编辑操作或网络请求。待队列任务都消费完成,再关闭线程池和资源连接,最后退出应用。
因为我希望快速创建一个 Vue3 + TypeScript 的前端项目,并且想整合一些常用功能(如路由、Pinia、ESLint 等),所以我在项目中直接使用了 Vue 官方推荐的 create-vue 脚手架,一键生成基础的目录结构和依赖。它的好处主要包括:
我先使用 create-vue 脚手架快速创建了基础项目结构并整合了常用的依赖和前端工程化配置,然后开发了一系列的 “万用” 功能。比如:
之后这个模板可以用到我的新项目中,不必反复从 0 开发。
TypeScript:让 JavaScript 拥有了强类型体系,能在编译阶段发现不少潜在错误,比如参数类型不匹配、属性名写错等问题,大幅减少运行时 bug。
ESLint:主要用于语法和规范校验,会检查代码风格、潜在语法错误等。例如提示未使用的变量、不安全的比较运算等。
Prettier:专门做代码格式化,统一缩进、分号、引号、换行风格等,让团队代码风格高度一致。
三者配合,TypeScript 负责类型安全,ESLint 负责语法 / 规范校验,Prettier 负责格式统一,能提升项目代码质量、可维护性和可读性。
Vue Router 是 Vue 官方提供的路由管理库,用来在单页应用中根据 url 地址来切换不同的页面。它会根据浏览器地址变化,动态加载并渲染对应的 Vue 组件,而无需整页刷新。
在云图库项目中,我编写了一个路由配置文件集中管理路由,例如:
const routes = [
{ path: '/', name: 'home', component: HomePage },
{ path: '/about', name: 'about', component: AboutPage },
]
然后为了避免重复维护路由和菜单,我选择把路由配置解析成菜单,步骤如下:
这样,新增 / 修改路由就能自动更新菜单,不用写两遍,保持一致性与可维护性。
我采用了 路由守卫 + 全局状态 的方式实现全局权限管理,步骤如下:
/admin 开头),如果用户不满足角色则跳转到登录页或者无权限页。我将上述代码封装到 access 模块中,便于集中维护。
全局状态管理就是在前端应用中,集中保存多个页面和组件都需要共享的关键数据(如登录用户、主题设置等)。这样可以在任何组件中获取和修改这些数据,并且触发 UI 的更新,避免了组件间相互传递数据。
云图库项目中,很多页面都要根据当前登录用户判断权限、显示隐藏元素,所以要用到全局状态管理。
之所以选 Pinia,首先是因为脚手架自己整合了 Pinia,当然还有更多原因:
自定义 Axios 实例,可以在发送请求前统一配置一些通用属性,而不用反复设置。比如:
我自定义的实例功能包括:
OpenAPI 是一种 API 描述规范,可以用 YAML 或 JSON 格式描述后端接口的请求 / 响应数据结构。借助它可以自动生成前端或后端的 API 调用代码,从而减少人工写接口的工作量。
在云图库项目里:
工具会自动生成所有接口调用方法、请求体 / 响应体类型定义的 TypeScript 代码。这样前端就不必手写几十个接口,还保证与后端接口的调用方式和参数强一致,极大提高开发效率并减少接口对不上的问题。
云图库项目包含了用户管理、图片管理、空间管理等多个管理页面,实现流程都是一致的。我参考官方文档使用了 Ant Design Vue 提供的 Table 组件,快速实现包含 展示 + 分页 + 搜索 + 操作 功能的管理页面。
核心流程如下:
云图库项目的前端中,我大量使用了 Ant Design Vue 提供的组件,比如:
除了 Ant Design 官方组件,我还用到了一些第三方组件,以满足不同的业务需求。比如:
dayjs(picture.createTime).format('YYYY-MM-DD HH:mm:ss')。在云图库项目中,我开发了 10 多个可复用的业务组件,举些例子:
组件开发的经验技巧:
动态路由指在 Vue Router 中,可以在路径中使用类似 :id 这样的占位符,告诉路由在访问 /xx/:id 时可以渲染同一个页面组件,但里边的内容可根据动态参数的不同进行相应的处理和展示。
在云图库项目中,典型的场景是图片详情页,我在路由配置中写:
{
path: '/picture/:id',
name: '图片详情',
component: PictureDetailPage
}
这样当用户访问 /picture/1001、/picture/123 时都能加载同一个 PictureDetailPage 组件。组件内通过 props 或 useRoute() 可以拿到 id 参数去调用后端接口获取对应的图片信息。这样就无需给每张图片都写一条路由、开发一个页面,非常灵活。
watchEffect 是 Vue3 提供的一个自动监听 “响应式变量” 变化的函数式 API。它和 computed、watch 类似,但更简洁、没有显式的依赖声明,Vue 内部会自动跟踪它使用的响应式变量,一旦变量变化,就重新运行这个函数。
在云图库里,当我想在每次响应式变量更新时立刻触发某个函数的调用,就会用到 watchEffect。比如在展示空间分析图表时,我会用 watchEffect 来监听分析参数的变化,然后自动调用接口更新分析数据。写法更方便一些:
watchEffect(() => {
fetchData()
})
我主要通过以下方式,让图片列表在各种屏幕下都能更好展示:
:grid="{ gutter:16, xs:1, md:3, lg:4, xxl:6 }",表示小屏手机上一行 1 张,中等屏一行 3 张,大屏可展示更多,自动响应式。object-fit: cover 或 contain + max-height,在不同宽度下让图片保持等比缩放,不会变形。此外,还可以使用 CSS 的媒体查询特性。针对极小 / 极大屏幕写些定制 CSS,比如 @media (max-width: 600px) { ... } 来调整列表或边距。
我在云图库项目中主要从四个方面 缩略图 + 懒加载 + CDN + 浏览器缓存 来优化图片加载,尽可能地减少加载时间、降低带宽消耗并提升用户体验:
1)缩略图:在图片上传时,除了保留压缩图外,还生成了一份缩略图,用户在列表页查看图片时只会加载缩略图,而不直接加载原图。这样做可以显著降低流量消耗,大幅加快页面首屏速度。
2)懒加载:对于图片很多的页面,可以用原生 HTML5 的 loading="lazy" 或者利用 Intersection Observer 来实现懒加载,只有当图片即将出现在视口时才触发加载。这样可以避免一次性加载所有图片,节省网络资源。
3)CDN 加速:在后端把图片都存到 COS 对象存储后,再结合 CDN(内容分发网络)进行全国分发。 用户请求图片时,会自动就近访问缓存节点,无需回源站获取;同时我还启用了防盗链、限速等功能,提升安全性。
4)浏览器缓存:对于静态资源(图片等)配置了较长时间的浏览器缓存,下次访问时直接读取本地缓存。如果图片更改了,会自动修改文件名(hash 值),浏览器也会请求新的图片,从而避免了数据不一致的问题。
在云图库的前端中,我基于 Ant Design Vue 的 Form 表单和各类表单项组件,开发了多维搜索功能。
具体做法如下:
1)封装搜索表单组件:由于搜索项较多,我新建了 PictureSearchForm 组件,集中管理搜索条件的输入,比如关键词、分类、标签、日期、名称、简介、宽高和格式等字段。组件对外暴露一个 onSearch 回调,通过该回调将用户填好的搜索条件传递给父组件。
这个组件不仅空间详情页可以用,管理页面也能用,减少重复代码。
2)搜索条件的维护:在组件内部使用 reactive 对象来存储搜索字段,然后在提交时触发 onSearch,完成 “组件内输入 => 父组件获取” 的流程。
为了简化重置操作,还提供了一个重置按钮,点击后会清空所有搜索字段并触发一次搜索。
3)动态绑定搜索字段:每个搜索字段(如名称、分类、标签、宽高等)都选用 Ant Design 的合适组件(a-input、a-select、a-input-number 等)进行输入。
其中日期区间采用 a-range-picker,用户可快速选取时间段,并在 change 事件中将时间值映射到搜索参数的 startEditTime、endEditTime。
4)前后端交互:父组件在 onSearch 回调里拿到搜索表单对象,然后将其与分页信息合并,调用后端的接口,最终返回搜索结果列表并展示到图片列表中。
我引入了第三方颜色选择器组件 vue3-colorpicker,用户在选择器中选定一个主色调后,会触发一个回调事件,把选中的十六进制色值提交到后端,后端返回一组符合条件的图片列表,展示即可。
此外,我在图片详情页补充了图片主色调的展示,用一个 16px 色块直观展示色彩。
由于项目多个页面都需要使用分享功能,所以我封装了通用的分享弹窗组件,同时支持复制链接和移动端扫码分享。我使用 Ant Design 的 Typography 组件实现了链接的复制;使用 QrCode 组件实现了移动端扫码分享,输入要分享的超链接文本,就可以生成二维码图片展示到前端。
弹窗组件为受控组件,是否可见的状态由组件内部维护,通过 defineExpose 暴露打开弹窗的函数,父组件通过 ref 调用即可打开弹窗。
我在前端使用了第三方图片裁剪库 vue-cropper,并封装成 ImageCropper 组件。组件内部通过 vue-cropper 提供的方法实现裁剪、旋转、放大、缩小操作,再通过 Ant Design 的 Modal 弹窗实现交互式编辑流程。
用户在弹窗中编辑完图片后,点击 “确认” 会调用 cropperRef.value.getCropBlob() 获取裁剪后的文件,并通过已有的上传接口提交到后端,前端同时更新图片显示,从而完成基础的图片编辑功能。
我在前端新建了一个独立的 ImageOutPainting 弹窗组件,用户点击 “AI 扩图” 后会立刻调用后端创建扩图任务的接口,并返回 taskId。由于扩图过程涉及大量计算,后端会采用 “异步任务” 方式执行,前端则通过定时器每隔几秒轮询一次任务状态。
如果状态是 “成功”,就能拿到扩图后的图片地址并显示;如果是 “失败”,则弹出相应的失败提示消息。
用户可以自主决定是否覆盖原图,而不是强制用新图片替代原图,提升了操作的灵活度。
使用 Apache Echarts 可视化库,该库提供了丰富的图表,比如分组条形图、饼图、词云图、柱状图等。
我先进入该库官网的示例页面,根据统计分析需求找到对应的图表,然后进入详情页修改 options 变量进行调试。调试完成后,编写前端页面,请求后端获取数据,并通过 Vue 的 computed 语法计算出 Echarts 需要的 options 变量,成功在页面上渲染出图表。
值得一提的是,我让 AI 根据分析需求生成了 options 变量,大幅提高了开发效率。
核心思路是根据后端返回的权限列表,结合 computed 计算属性进行动态判断。
具体流程:
SPACE_PERMISSION_ENUM,便于后续使用更方便。permissionList(如 ["picture:edit","picture:upload"])。我基于返回的权限列表写了一个通用检查函数 createPermissionChecker(permission) => computed(...),传入需要的权限,就能得到是否具有该权限的判断结果,比如 canUpload = createPermissionChecker("picture:upload")。v-if="canUpload" 等条件即可判断是否显示按钮。前端主要通过 WebSocket 实时通信来实现协同编辑,在 “图片编辑弹窗” 中让团队成员能感知并同步其他用户的编辑操作。具体做法是:
websocket.disconnect 释放连接,并重置 “正在编辑用户” 状态,防止重复占用连接或编辑锁。WebSocket 是一种在客户端和服务器之间保持 双向、实时通信 的协议。相比传统的 HTTP 单向请求 - 响应模式,WebSocket 能让服务器随时主动推送数据给客户端,或者客户端主动发送消息给服务器,无需反复创建和关闭连接,实时性更强,也减少了网络开销。
1)前端如何建立连接
先在 JavaScript 中创建一个 new WebSocket("ws://serverURL") 对象,指定后端提供的 WebSocket URL。 然后就可以监听事件并进行相应的处理了:
onopen 事件,表示连接成功后可以开始发送消息onmessage 事件,用于处理服务器推送过来的数据onclose 和 onerror 处理连接断开或异常情况2)注意事项
项目有实际上线。我是通过 本地打包 + Nginx 实现了前端页面部署。
具体过程如下:
还有其他的部署方式,比如使用 Vercel 等 Serverless 服务一键部署到第三方托管服务器,但由于本人有服务器、并且想实践下 Nginx 配置,所以没有选择这种方式。
项目分为公共图库、私有图库、团队空间三大模块,整体流程如下:
1)公共图库:任何用户都能浏览公共图库;未登录用户默认仅可查看已审核通过的图片。管理员可执行图片上传与审核,拒绝违规图片。还可以查看公共图库的图片分布情况(如分类、标签、上传趋势等)。
2)私有图库:注册用户登录后,可以将图片上传到自己的私有空间,包含多维检索、批量管理、基础编辑、AI 扩图等功能。用户还可以进行个人图库分析(图片分类、标签、大小等),从而优化存储结构或查找常用图片。
3)团队空间 企业用户或拥有付费权限的用户创建团队空间,系统会自动将创建者设为管理员;管理员可直接输入成员 ID,添加协作者并为其分配编辑者 / 浏览者角色。有编辑权限的成员可以通过 WebSocket 对图片进行协同编辑,如旋转、放大等操作会同步给其他在线成员;
主要是为了 循序渐进、分模块迭代,让项目的功能和设计在每个阶段都能相对完整地上线并可扩展。
1)最基础的公共图库场景相对简单,只需实现图片上传、审核和公开展示,适合初期快速构建 MVP(最小可行产品)。同时也能尽早验证核心功能(如图片管理、检索、审核流程),并为后续私有空间和团队空间打下基础。
2)当公共图库稳定后,再扩展出私有空间,满足个人对图片进行精细化管理、分析和编辑的需求。在此阶段引入更丰富的功能如多维检索、批量操作和 AI 扩图等,让个人用户体验并逐步沉淀项目的 “编辑能力”。
3)最后再拓展出团队空间,实现多人协作、成员管理、实时协同编辑等复杂需求。此时,前两阶段的权限设计、图片编辑和统计分析功能都可以 “复用” 到团队空间,只需整合并补充成员角色、协作流程等部分逻辑即可。
分阶段之后,既能 快速上线核心功能,又能 在每个阶段稳步迭代、持续完善项目;同时也利于团队和用户在功能逐步复杂化的过程中不断实践、反馈并优化设计。
必须的,我使用的是国内的免费 AI 编程助手 CodeGeex(或者其他家的产品),主要使用了下列功能:
可以从以上任意一道主观的面试题出发去讲,比如你是怎么实现协同编辑功能并使用 Disruptor 进行优化的?你是怎么利用 ShardingSphere 实现动态分表的?如何一步步封装了通用的文件上传模块?为什么要使用模板方法模式和门面模式优化代码等等。
送分题,直接参考鱼皮编程导航的 项目介绍。