图库分析
- 用户空间图库分析
- 管理员全空间分析
一、需求分析:
1、用户空间图库分析
用户可以对自己的空间图库进行分析,包括以下几个重点功能:
1)空间资源使用分析:通过统计当前空间已使用大小与总配额的比例,以及图片数量与最大允许数量的占比,帮助用户直观了解空间使用状态,及时清理或升级空间。图表形式推荐使用 仪表盘,类似进度条,可以更直观地了解比例。

2)空间图片分类分析:统计不同分类下图片的数量和总大小占比,帮助用户清晰了解各分类的资源分布,优化存储策略。由于同一个分类要展示多个信息,可以选择 分组条形图 来展示。

3)空间图片标签分析:解析用户图库中的标签,统计每个标签的关联图片数量。由于标签比较多,可以用 词云图 展示所有的标签,并突出常用标签,便于优化管理和图片搜索。

4)空间图片大小分析:按图片大小(如 <100 KB、100 KB-1 MB、>1 MB)分段统计图片数量,帮助用户识别大体积图片,合理分配存储资源。由于按图片大小分类的数量不多,可以使用 饼图 展示,能够体现每类大小图片的数量占比。

5)用户上传行为分析:统计用户每月、每周、每日上传图片的数量趋势,帮助用户识别上传高峰期并优化管理策略(虽然对目前这个阶段没有用,但之后要开发团队空间,可以给团队管理员使用)。使用 折线图 呈现时间序列趋势。

2、管理员全空间分析
管理员全空间分析的核心是面向公共图库、以及所有用户空间的统计和管理:
1)全空间资源使用分析:统计公共图库、以及系统内所有空间的总存储量和总图片数,并且也支持任意空间的图片分类、图片标签、图片大小、用户上传行为的分析,便于管理员了解系统资源分配和利用情况。
其实跟用户分析自己空间的需求一致,只不过分析的范围更大罢了。
2)空间使用排行分析:按存储使用量排序,统计占用存储最多的前 N 个空间,帮助管理员快速定位高占用空间,并识别潜在的资源滥用或异常情况。可以选用 柱状图,直观地展示排名和存储使用量。

二、需求分析:
1、分析类需求的实现流程
对于分析类需求,实现流程几乎都是一致的,包括:
- 数据采集:从数据源(比如 MySQL 数据库或者大数据仓库)获取原始数据。要提前明确涉及的表和字段,必要时采用分页查询处理大数据量。
- 数据预处理:对数据进行清洗、加工和格式化,包括过滤无效数据(比如逻辑删除或审核未通过)、解析复杂字段(比如 JSON 格式的 tags),以及通过字段关联补充上下文信息。
- 数据计算:根据需求进行分组、聚合、排序等,从而计算关键指标,比如计算空间各分类图片的占用比例、用户上传图片的时间趋势。可以根据场景调整计算方案,比如对于大数据量的计算,可以采用 Spark 之类的大数据计算组件做离线计算;对于数据实时性要求较高的实时分析场景,可以用 Flink 做流式处理。
- 数据存储(可选):针对频繁查询的分析结果,可将结数据存储为单独的表或缓存,减少重复计算,提高查询效率。
- 数据接口设计:为前端提供统一接口,从而支持查询和展示。需要考虑到数据量较大导致前端渲染卡顿的情况,可以按需精简返回的字符串、分页查询等。
- 数据可视化:通过图表直观展示分析结果,前端可以使用 Apache ECharts 等可视化库渲染。当然也可以让后端生成图表图片并返回,但这种实现方法的灵活度有限。
后续还可以根据用户的反馈持续优化分析逻辑、增加指标或改进性能。
2、智云图实现方案
通过需求分析,我们发现,管理员对公共图库及全空间的分析需求,与用户对自己空间的分析需求在本质上是相同的,唯一的区别在于图片范围的选择。
下面以 “空间图片分类分析” 为例。
1)用户分析自己的空间,SQL 示例:
SELECT category, SUM(picSize) AS totalSize
FROM picture
WHERE spaceId = xxx
GROUP BY category;
2)管理员分析公共图库,SQL 示例:
SELECT category, SUM(picSize) AS totalSize
FROM picture
WHERE spaceId IS NULL
GROUP BY category;
3)管理员分析全部空间,SQL 示例:
SELECT category, SUM(picSize) AS totalSize
FROM picture
GROUP BY category;
会发现,除了 where 查询条件不同之外,其他的计算方式都是一致的。
所以可以设计统一的接口,通过传递不同的请求参数,同时满足上述需求。参数含义和优先级如下(优先级从高到低):
- queryAll 字段:为 true 时表示查询全空间,仅管理员可使用。
- queryPublic 字段:为 true 时表示查询公共图库,仅管理员可使用。
- spaceId 字段:仅在 queryAll 和 queryPublic 均为 false 时生效,表示对特定空间进行分析,仅空间创建者和管理员可使用。
对应的后端伪代码如下,可以将这段逻辑封装为单独的方法:
// 先权限校验
// 封装查询条件
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
if (queryAll) {
// 管理员查询全空间,不添加过滤条件
} else if (queryPublic) {
// 管理员查询公共图库
queryWrapper.isNull("spaceId");
} else if (spaceId != null) {
// 用户或管理员查询特定空间
queryWrapper.eq("spaceId", spaceId);
} else {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未指定查询范围");
}
通过这种方式,就不用多针对不同的查询范围编写一套接口了,可以大幅减少重复代码。
三、后端开发
对应的后端伪代码如下,可以将这段逻辑封装为单独的方法:
// 先权限校验
// 封装查询条件
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
if (queryAll) {
// 管理员查询全空间,不添加过滤条件
} else if (queryPublic) {
// 管理员查询公共图库
queryWrapper.isNull("spaceId");
} else if (spaceId != null) {
// 用户或管理员查询特定空间
queryWrapper.eq("spaceId", spaceId);
} else {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未指定查询范围");
}
通过这种方式,就不用多针对不同的查询范围编写一套接口了,可以大幅减少重复代码。
分析需求相关的 DTO 和 VO 数据模型放到 analyze
包下

1、通用分析请求
1)由于我们的很多分析需求都需要传递空间查询范围,可以先写一个公共的图片分析请求封装类:
@Data
public class SpaceAnalyzeRequest implements Serializable {
/**
* 空间 ID
*/
private Long spaceId;
/**
* 是否查询公共图库
*/
private boolean queryPublic;
/**
* 全空间分析
*/
private boolean queryAll;
private static final long serialVersionUID = 1L;
}
然后各个具体的分析请求封装类就能直接继承了,这样也便于后续编写通用的分析请求处理方法。
2)我们可以新建 SpaceAnalyzeService 和对应实现类,开发校验空间分析权限、根据分析范围填充查询对象这两个方法,后续的需求也都会用到。
校验空间分析权限:
private void checkSpaceAnalyzeAuth(SpaceAnalyzeRequest spaceAnalyzeRequest, User loginUser) {
// 检查权限
if (spaceAnalyzeRequest.isQueryAll() || spaceAnalyzeRequest.isQueryPublic()) {
// 全空间分析或者公共图库权限校验:仅管理员可访问
ThrowUtils.throwIf(!userService.isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR, "无权访问公共图库");
} else {
// 私有空间权限校验
Long spaceId = spaceAnalyzeRequest.getSpaceId();
ThrowUtils.throwIf(spaceId == null || spaceId <= 0, ErrorCode.PARAMS_ERROR);
Space space = spaceService.getById(spaceId);
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
spaceService.checkSpaceAuth(loginUser, space);
}
}
根据分析范围填充查询对象:
private static void fillAnalyzeQueryWrapper(SpaceAnalyzeRequest spaceAnalyzeRequest, QueryWrapper<Picture> queryWrapper) {
if (spaceAnalyzeRequest.isQueryAll()) {
return;
}
if (spaceAnalyzeRequest.isQueryPublic()) {
queryWrapper.isNull("spaceId");
return;
}
Long spaceId = spaceAnalyzeRequest.getSpaceId();
if (spaceId != null) {
queryWrapper.eq("spaceId", spaceId);
return;
}
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未指定查询范围");
}
2、需求开发
1、空间资源使用分析
1)开发请求封装类,用于接收前端请求的数据。此处直接继承通用的图片分析请求封装类即可,不需要传递其他字段:
@EqualsAndHashCode(callSuper = true)
@Data
public class SpaceUsageAnalyzeRequest extends SpaceAnalyzeRequest {
}
2)开发响应视图类,用于将分析结果返回给前端:
@Data
public class SpaceUsageAnalyzeResponse implements Serializable {
/**
* 已使用大小
*/
private Long usedSize;
/**
* 总大小
*/
private Long maxSize;
/**
* 空间使用比例
*/
private Double sizeUsageRatio;
/**
* 当前图片数量
*/
private Long usedCount;
/**
* 最大图片数量
*/
private Long maxCount;
/**
* 图片数量占比
*/
private Double countUsageRatio;
private static final long serialVersionUID = 1L;
}
3)开发 SpaceAnalyzeService 业务逻辑层
注意,如果是分析全空间或公共图库的使用情况,需要编写 “仅管理员可访问” 的权限校验逻辑,并且更改查询图片表的范围;如果只是分析单个空间的使用情况,直接从空间表查询出单个空间的数据即可。
代码如下:
/**
* 获取空间使用分析数据
*
* @param spaceUsageAnalyzeRequest SpaceUsageAnalyzeRequest 请求参数
* @param loginUser 当前登录用户
* @return SpaceUsageAnalyzeResponse 分析结果
*/
@Override
public SpaceUsageAnalyzeResponse getSpaceUsageAnalyze(SpaceUsageAnalyzeRequest spaceUsageAnalyzeRequest, User loginUser) {
ThrowUtils.throwIf(spaceUsageAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
if (spaceUsageAnalyzeRequest.isQueryAll() || spaceUsageAnalyzeRequest.isQueryPublic()) {
// 查询全部或公共图库逻辑
// 仅管理员可以访问
boolean isAdmin = userService.isAdmin(loginUser);
ThrowUtils.throwIf(!isAdmin, ErrorCode.NO_AUTH_ERROR, "无权访问空间"); //如果没抛出,说明是管理员,统计下面信息
// 统计公共图库的资源使用
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
queryWrapper.select("picSize");
if (!spaceUsageAnalyzeRequest.isQueryAll()) { //不是查询所有,那就是查询公共图库
queryWrapper.isNull("spaceId"); //空间id为空
}
List<Object> pictureObjList = pictureService.getBaseMapper().selectObjs(queryWrapper);
long usedSize = pictureObjList.stream().mapToLong(result -> result instanceof Long ? (Long) result : 0).sum();
long usedCount = pictureObjList.size();
// 封装返回结果
SpaceUsageAnalyzeResponse spaceUsageAnalyzeResponse = new SpaceUsageAnalyzeResponse();
spaceUsageAnalyzeResponse.setUsedSize(usedSize);
spaceUsageAnalyzeResponse.setUsedCount(usedCount);
// 公共图库无上限、无比例
spaceUsageAnalyzeResponse.setMaxSize(null);
spaceUsageAnalyzeResponse.setSizeUsageRatio(null);
spaceUsageAnalyzeResponse.setMaxCount(null);
spaceUsageAnalyzeResponse.setCountUsageRatio(null);
return spaceUsageAnalyzeResponse;
} else {
// 查询指定空间
Long spaceId = spaceUsageAnalyzeRequest.getSpaceId();
ThrowUtils.throwIf(spaceId == null || spaceId <= 0, ErrorCode.PARAMS_ERROR);
// 获取空间信息
Space space = spaceService.getById(spaceId);
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
// 权限校验:仅空间所有者或管理员可访问
spaceService.checkSpaceAuth(loginUser, space);
// 构造返回结果
SpaceUsageAnalyzeResponse response = new SpaceUsageAnalyzeResponse();
response.setUsedSize(space.getTotalSize());
response.setMaxSize(space.getMaxSize());
// 后端直接算好百分比,这样前端可以直接展示
double sizeUsageRatio = NumberUtil.round(space.getTotalSize() * 100.0 / space.getMaxSize(), 2).doubleValue();
response.setSizeUsageRatio(sizeUsageRatio);
response.setUsedCount(space.getTotalCount());
response.setMaxCount(space.getMaxCount());
double countUsageRatio = NumberUtil.round(space.getTotalCount() * 100.0 / space.getMaxCount(), 2).doubleValue();
response.setCountUsageRatio(countUsageRatio);
return response;
}
}

提示
这里有一个点:pictureService.getBaseMapper().selectObjs(queryWrapper);
为什么不用,pictureService.list
,因为我们只需要picSize字段,list会把所有信息(封装为 Picture 对象)都返回出来。
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
queryWrapper.select("picSize");
if (!spaceUsageAnalyzeRequest.isQueryAll()) {
queryWrapper.isNull("spaceId");
}
List<Object> pictureObjList = pictureService.getBaseMapper().selectObjs(queryWrapper);
long usedSize = pictureObjList.stream().mapToLong(result -> result instanceof Long ? (Long) result : 0).sum();
可以在 SpaceService 中封装空间权限校验方法,其他的分析需求也会用到:
/**
* 空间权限校验
*
* @param loginUser
* @param space
*/
@Override
public void checkSpaceAuth(User loginUser, Space space) {
// 仅本人或管理员可访问
if (!space.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
然后可以将 SpaceController 中编辑和删除操作的权限校验代码替换为 checkSpaceAuth 方法,统一空间校验逻辑。
4)开发 SpaceAnalyzeController 接口:
@RestController
@RequestMapping("/space/analyze")
public class SpaceAnalyzeController {
@Resource
private SpaceAnalyzeService spaceAnalyzeService;
@Resource
private UserService userService;
/**
* 获取空间使用状态
*/
@PostMapping("/usage")
public BaseResponse<SpaceUsageAnalyzeResponse> getSpaceUsageAnalyze(
@RequestBody SpaceUsageAnalyzeRequest spaceUsageAnalyzeRequest,
HttpServletRequest request
) {
ThrowUtils.throwIf(spaceUsageAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
User loginUser = userService.getLoginUser(request);
SpaceUsageAnalyzeResponse spaceUsageAnalyze = spaceAnalyzeService.getSpaceUsageAnalyze(spaceUsageAnalyzeRequest, loginUser);
return ResultUtils.success(spaceUsageAnalyze);
}
}
2、空间图片分类分析
1)开发请求封装类。分类分析只需要传递空间范围相关参数,因此可以直接继承公共的 SpaceAnalyzeRequest
:
@EqualsAndHashCode(callSuper = true)
@Data
public class SpaceCategoryAnalyzeRequest extends SpaceAnalyzeRequest {
}
2)开发响应视图类。分类分析的结果需要返回图片分类、分类图片数量和分类图片总大小:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SpaceCategoryAnalyzeResponse implements Serializable {
/**
* 图片分类
*/
private String category;
/**
* 图片数量
*/
private Long count;
/**
* 分类图片总大小
*/
private Long totalSize;
private static final long serialVersionUID = 1L;
}
3)开发 Service 服务。按照分类分组查询图片表的数据,注意查询数据库时只获取需要的字段即可:
@Override
public List<SpaceCategoryAnalyzeResponse> getSpaceCategoryAnalyze(SpaceCategoryAnalyzeRequest spaceCategoryAnalyzeRequest, User loginUser) {
ThrowUtils.throwIf(spaceCategoryAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
// 检查权限
checkSpaceAnalyzeAuth(spaceCategoryAnalyzeRequest, loginUser);
// 构造查询条件
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
// 根据分析范围补充查询条件
fillAnalyzeQueryWrapper(spaceCategoryAnalyzeRequest, queryWrapper);
// 使用 MyBatis-Plus 分组查询
queryWrapper.select("category AS category",
"COUNT(*) AS count",
"SUM(picSize) AS totalSize")
.groupBy("category");
// 查询并转换结果
return pictureService.getBaseMapper().selectMaps(queryWrapper)
.stream()
.map(result -> {
String category = result.get("category") != null ? result.get("category").toString() : "未分类";
Long count = ((Number) result.get("count")).longValue();
Long totalSize = ((Number) result.get("totalSize")).longValue();
return new SpaceCategoryAnalyzeResponse(category, count, totalSize);
})
.collect(Collectors.toList());
}
💡 建议在编写具体的代码前,先编写示例 SQL 语句,并通过数据库查询客户端来验证。
4)开发接口:
@PostMapping("/category")
public BaseResponse<List<SpaceCategoryAnalyzeResponse>> getSpaceCategoryAnalyze(@RequestBody SpaceCategoryAnalyzeRequest spaceCategoryAnalyzeRequest, HttpServletRequest request) {
ThrowUtils.throwIf(spaceCategoryAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
User loginUser = userService.getLoginUser(request);
List<SpaceCategoryAnalyzeResponse> resultList = spaceAnalyzeService.getSpaceCategoryAnalyze(spaceCategoryAnalyzeRequest, loginUser);
return ResultUtils.success(resultList);
}
3、空间图片标签分析
1)开发请求封装类,标签分析同样需要继承 SpaceAnalyzeRequest
:
@EqualsAndHashCode(callSuper = true)
@Data
public class SpaceTagAnalyzeRequest extends SpaceAnalyzeRequest {
}
2)开发响应视图类。标签分析的结果需要返回标签名称和关联的图片数量:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SpaceTagAnalyzeResponse implements Serializable {
/**
* 标签名称
*/
private String tag;
/**
* 使用次数
*/
private Long count;
private static final long serialVersionUID = 1L;
}
3)开发 Service 服务。从数据库获取标签数据,统计每个标签的图片数量,并按使用次数降序排序:
@Override
public List<SpaceTagAnalyzeResponse> getSpaceTagAnalyze(SpaceTagAnalyzeRequest spaceTagAnalyzeRequest, User loginUser) {
ThrowUtils.throwIf(spaceTagAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
// 检查权限
checkSpaceAnalyzeAuth(spaceTagAnalyzeRequest, loginUser);
// 构造查询条件
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
fillAnalyzeQueryWrapper(spaceTagAnalyzeRequest, queryWrapper);
// 查询所有符合条件的标签
queryWrapper.select("tags");
List<String> tagsJsonList = pictureService.getBaseMapper().selectObjs(queryWrapper)
.stream()
.filter(ObjUtil::isNotNull)
.map(Object::toString)
.collect(Collectors.toList());
// 合并所有标签并统计使用次数
Map<String, Long> tagCountMap = tagsJsonList.stream()
.flatMap(tagsJson -> JSONUtil.toList(tagsJson, String.class).stream())
.collect(Collectors.groupingBy(tag -> tag, Collectors.counting()));
// 转换为响应对象,按使用次数降序排序
return tagCountMap.entrySet().stream()
.sorted((e1, e2) -> Long.compare(e2.getValue(), e1.getValue())) // 降序排列
.map(entry -> new SpaceTagAnalyzeResponse(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
4)开发接口:
@PostMapping("/tag")
public BaseResponse<List<SpaceTagAnalyzeResponse>> getSpaceTagAnalyze(@RequestBody SpaceTagAnalyzeRequest spaceTagAnalyzeRequest, HttpServletRequest request) {
ThrowUtils.throwIf(spaceTagAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
User loginUser = userService.getLoginUser(request);
List<SpaceTagAnalyzeResponse> resultList = spaceAnalyzeService.getSpaceTagAnalyze(spaceTagAnalyzeRequest, loginUser);
return ResultUtils.success(resultList);
}
4、空间图片大小分析
1)开发请求封装类。图片大小分析也继承 SpaceAnalyzeRequest
:
@EqualsAndHashCode(callSuper = true)
@Data
public class SpaceSizeAnalyzeRequest extends SpaceAnalyzeRequest {
}
2)开发响应视图类。大小分析结果需要返回图片大小范围和对应的图片数量:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SpaceSizeAnalyzeResponse implements Serializable {
/**
* 图片大小范围
*/
private String sizeRange;
/**
* 图片数量
*/
private Long count;
private static final long serialVersionUID = 1L;
}
3)开发 Service 服务,分段统计图片大小:
@Override
public List<SpaceSizeAnalyzeResponse> getSpaceSizeAnalyze(SpaceSizeAnalyzeRequest spaceSizeAnalyzeRequest, User loginUser) {
ThrowUtils.throwIf(spaceSizeAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
// 检查权限
checkSpaceAnalyzeAuth(spaceSizeAnalyzeRequest, loginUser);
// 构造查询条件
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
fillAnalyzeQueryWrapper(spaceSizeAnalyzeRequest, queryWrapper);
// 查询所有符合条件的图片大小
queryWrapper.select("picSize");
List<Long> picSizes = pictureService.getBaseMapper().selectObjs(queryWrapper)
.stream()
.map(size -> ((Number) size).longValue())
.collect(Collectors.toList());
// 定义分段范围,注意使用有序 Map
Map<String, Long> sizeRanges = new LinkedHashMap<>();
sizeRanges.put("<100KB", picSizes.stream().filter(size -> size < 100 * 1024).count());
sizeRanges.put("100KB-500KB", picSizes.stream().filter(size -> size >= 100 * 1024 && size < 500 * 1024).count());
sizeRanges.put("500KB-1MB", picSizes.stream().filter(size -> size >= 500 * 1024 && size < 1 * 1024 * 1024).count());
sizeRanges.put(">1MB", picSizes.stream().filter(size -> size >= 1 * 1024 * 1024).count());
// 转换为响应对象
return sizeRanges.entrySet().stream() //取出这个map的每个entry
.map(entry -> new SpaceSizeAnalyzeResponse(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
上述代码其实还可以进一步优化,只需要遍历一次 picSizes 列表就可以按大小分别统计了。
4)开发接口:
@PostMapping("/size")
public BaseResponse<List<SpaceSizeAnalyzeResponse>> getSpaceSizeAnalyze(@RequestBody SpaceSizeAnalyzeRequest spaceSizeAnalyzeRequest, HttpServletRequest request) {
ThrowUtils.throwIf(spaceSizeAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
User loginUser = userService.getLoginUser(request);
List<SpaceSizeAnalyzeResponse> resultList = spaceAnalyzeService.getSpaceSizeAnalyze(spaceSizeAnalyzeRequest, loginUser);
return ResultUtils.success(resultList);
}
5、用户上传行为分析
1)开发请求封装类。用户上传行为分析需要增加时间维度(日、周、月)和用户 ID 参数,支持只分析某个用户上传图片的情况。
@EqualsAndHashCode(callSuper = true)
@Data
public class SpaceUserAnalyzeRequest extends SpaceAnalyzeRequest {
/**
* 用户 ID
*/
private Long userId;
/**
* 时间维度:day / week / month
*/
private String timeDimension;
}
2)开发响应视图类。用户行为分析结果需要返回时间区间和对应的图片数量:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SpaceUserAnalyzeResponse implements Serializable {
/**
* 时间区间
*/
private String period;
/**
* 上传数量
*/
private Long count;
private static final long serialVersionUID = 1L;
}
3)开发 Service 服务,基于图片的创建时间维度统计用户的上传行为,并按照时间升序排序:
@Override
public List<SpaceUserAnalyzeResponse> getSpaceUserAnalyze(SpaceUserAnalyzeRequest spaceUserAnalyzeRequest, User loginUser) {
ThrowUtils.throwIf(spaceUserAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
// 检查权限
checkSpaceAnalyzeAuth(spaceUserAnalyzeRequest, loginUser);
// 构造查询条件
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
Long userId = spaceUserAnalyzeRequest.getUserId();
queryWrapper.eq(ObjUtil.isNotNull(userId), "userId", userId);
fillAnalyzeQueryWrapper(spaceUserAnalyzeRequest, queryWrapper);
// 分析维度:每日、每周、每月
String timeDimension = spaceUserAnalyzeRequest.getTimeDimension();
switch (timeDimension) {
case "day":
queryWrapper.select("DATE_FORMAT(createTime, '%Y-%m-%d') AS period", "COUNT(*) AS count");
break;
case "week":
queryWrapper.select("YEARWEEK(createTime) AS period", "COUNT(*) AS count");
break;
case "month":
queryWrapper.select("DATE_FORMAT(createTime, '%Y-%m') AS period", "COUNT(*) AS count");
break;
default:
throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的时间维度");
}
// 分组和排序
queryWrapper.groupBy("period").orderByAsc("period");
// 查询结果并转换
List<Map<String, Object>> queryResult = pictureService.getBaseMapper().selectMaps(queryWrapper);
return queryResult.stream()
.map(result -> {
String period = result.get("period").toString();
Long count = ((Number) result.get("count")).longValue();
return new SpaceUserAnalyzeResponse(period, count);
})
.collect(Collectors.toList());
}
上述代码中,我们使用 MySQL 的日期时间函数对图片的创建时间进行了格式化,使得同一天(周 / 月)的值相同,就能够统一按照一个字段(period)进行分组和排序了。
4)开发接口:
@PostMapping("/user")
public BaseResponse<List<SpaceUserAnalyzeResponse>> getSpaceUserAnalyze(@RequestBody SpaceUserAnalyzeRequest spaceUserAnalyzeRequest, HttpServletRequest request) {
ThrowUtils.throwIf(spaceUserAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
User loginUser = userService.getLoginUser(request);
List<SpaceUserAnalyzeResponse> resultList = spaceAnalyzeService.getSpaceUserAnalyze(spaceUserAnalyzeRequest, loginUser);
return ResultUtils.success(resultList);
}
上述的这些需求,可以同时给用户和管理员使用,已经满足了管理员 “全空间资源使用分析” 的需求。接下来我们只需要单独开发一个 仅管理员可使用的功能 —— 空间使用排行分析。
6、空间使用排行分析
该功能仅管理员可使用,返回值就是前 N 个空间的信息。由于已经有现成的 Space 空间对象,就不用编写响应视图类了。
1)开发请求封装类。空间使用排行需要接收一个参数 topN
,指定要返回的前 N 名空间信息,默认值为 10:
@Data
public class SpaceRankAnalyzeRequest implements Serializable {
/**
* 排名前 N 的空间
*/
private Integer topN = 10;
private static final long serialVersionUID = 1L;
}
2)开发 Service 服务,按存储使用量排序查询前 N 个空间。注意,只有管理员可以查看空间排行:
@Override
public List<Space> getSpaceRankAnalyze(SpaceRankAnalyzeRequest spaceRankAnalyzeRequest, User loginUser) {
ThrowUtils.throwIf(spaceRankAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
// 仅管理员可查看空间排行
ThrowUtils.throwIf(!userService.isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR, "无权查看空间排行");
// 构造查询条件
QueryWrapper<Space> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id", "spaceName", "userId", "totalSize")
.orderByDesc("totalSize")
.last("LIMIT " + spaceRankAnalyzeRequest.getTopN()); // 取前 N 名
// 查询结果
return spaceService.list(queryWrapper);
}
3)开发接口:
@PostMapping("/rank")
public BaseResponse<List<Space>> getSpaceRankAnalyze(@RequestBody SpaceRankAnalyzeRequest spaceRankAnalyzeRequest, HttpServletRequest request) {
ThrowUtils.throwIf(spaceRankAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
User loginUser = userService.getLoginUser(request);
List<Space> resultList = spaceAnalyzeService.getSpaceRankAnalyze(spaceRankAnalyzeRequest, loginUser);
return ResultUtils.success(resultList);
}
至此,分析需求的后端接口就开发完成了
3、扩展知识 - 自定义 SQL
上述的需求我们是通过 MyBatis Plus 提供的方法实现数据库的分组排序查询,对于更复杂多样的分析需求,其实我们还可以自己在代码中编写 SQL 语句。
可能有部分同学会好奇,MyBatis 还能自定义 SQL?不都是直接调用 xxx.select
之类的方法么?
这就是典型的“用框架习惯了”,其实为了提高开发效率、避免自己写 SQL,我们之前一直使用的是 MyBatis Plus 框架。但别忘了,MyBatis Plus 是 MyBatis 的增强版,本质还是基于 MyBatis 的一些能力进行的一些封装简化,自定义 SQL 可是 MyBatis 最最最基础的能力之一。
在 MyBatis 一般会以两种方式来实现自定义 SQL :
1、Java 注解实现
基于 Java 注解写在 xxxMapper.java 中。
注解使用很简单, 在 mapper 层的接口类方法利用 @Select、@Update、@Insert、@Delete
等注解,在注解内填写自定义 SQL 语句,即可实现查询、更新、存储、删除。
例如下面两个方法:
public interface SpaceMapper extends BaseMapper<Space> {
/**
* 获取存储使用量排名前 N 的空间
* @param topN 排名前 N
* @return List<Space>
*/
@Select("SELECT id, spaceName, userId, totalSize " +
"FROM space " +
"ORDER BY totalSize DESC " +
"LIMIT #{topN}")
List<Space> getTopNSpaceUsage(int topN);
/**
* 删除某用户的所有空间
*
* @param userId 用户 ID
* @return 删除的记录数
*/
@Delete("DELETE FROM space WHERE userId = #{userId}")
int deleteByUserId(Long userId);
}
完整语句 = SQL 语句模板 + 设置动态参数。方法的参数可以作为动态参数自动填充到 SQL 模板中,得到最终的 SQL 语句,结果也会自动转成方法返回值的 Java 类型。
💡 通过 #{} 和 ${} 都可以实现 SQL 参数绑定,但是两者是有区别的。#{} 是预编译参数,可以防止 SQL 注入,而 ${} 是直接替换,会导致 SQL 注入。
2、XML 配置实现
基于 XML 配置文件写在 xxxMapper.xml 中。
之前通过代码生成器,项目里面已经有很多 xxxMapper.xml 配置文件了。比如 SpaceMapper.xml,里面定义了表和 Java 类的字段映射、SQL 字段列表片段。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yupi.yupicturebackend.mapper.SpaceMapper">
<resultMap id="BaseResultMap" type="com.yupi.yupicturebackend.model.entity.Space">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="spaceName" column="spaceName" jdbcType="VARCHAR"/>
<result property="spaceLevel" column="spaceLevel" jdbcType="INTEGER"/>
<result property="maxSize" column="maxSize" jdbcType="BIGINT"/>
<result property="maxCount" column="maxCount" jdbcType="BIGINT"/>
<result property="totalSize" column="totalSize" jdbcType="BIGINT"/>
<result property="totalCount" column="totalCount" jdbcType="BIGINT"/>
<result property="userId" column="userId" jdbcType="BIGINT"/>
<result property="createTime" column="createTime" jdbcType="TIMESTAMP"/>
<result property="editTime" column="editTime" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="updateTime" jdbcType="TIMESTAMP"/>
<result property="isDelete" column="isDelete" jdbcType="TINYINT"/>
</resultMap>
<sql id="Base_Column_List">
id,spaceName,spaceLevel,
maxSize,maxCount,totalSize,
totalCount,userId,createTime,
editTime,updateTime,isDelete
</sql>
</mapper>
不用自己新建,仅需在里面添加自定义的 SQL 代码即可。
跟注解类似,MyBatis XML 中提供了 <select>
、<update>
、<insert>
、<delete>
等语法,在内部添加自定义 SQL ,即可实现查询、更新、存储、删除。
移除上述 Mapper 的 SQL 注解,然后在 XML 文件中编写 SQL 片段,示例代码如下:
<!-- 获取存储使用量排名前 N 的空间 -->
<select id="getTopNSpaceUsage" resultType="com.yupi.Space">
SELECT id, spaceName, userId, totalSize
FROM space
ORDER BY totalSize DESC
LIMIT #{topN}
</select>
<!-- 删除某用户的所有空间 -->
<delete id="deleteByUserId">
DELETE FROM space WHERE userId = #{userId}
</delete>
需要注意 2 点:
- Mapper 接口中的方法名称必须与 XML 文件中定义的 SQL 片段的 id 对应,MyBatis 才能正确解析和匹配方法。
- Mapper 接口方法的返回类型需要与 XML 文件中 resultType(或 resultMap)的定义保持一致,以确保查询结果能够正确映射到返回对象。
这样一来,MyBatis 在运行时会根据 Mapper 接口解析对应的 XML 文件,通过动态代理机制,将接口方法与 SQL 执行逻辑关联起来。
4、扩展知识 - 查询加速
数据分析通常有 2 种处理方式,实时分析和离线分析。
实时分析是指在数据生成的同时,立即对其进行处理和分析,以提供即时的结果,这种方式适用于需要快速决策的场景,比如监控系统中的异常检测或电商的实时推荐;离线数据分析则是在批量收集和存储数据后,进行复杂计算和深度分析,适合数据量极大、不需要即时结果的场景,比如生成历史报表或挖掘数据中的潜在特征。
即使我们没学过大数据技术,也可以通过业务逻辑层的编码加速数据查询和分析,典型的解决方案就是缓存。利用 Redis 分布式缓存或本地缓存来存储往期的查询结果,并设定一定的过期时间,就能避免重复计算并快速响应。
当然,对于定期的分析诉求(比如计算每日的排行榜)还有一种典型的方案,是通过定时任务计算每日的结果并存储在数据库中,之后就可以按照日期来直接查询某天的结果了。
比如上述需求中的 “用户上传行为分析”,就可以每日计算某个空间的用户上传情况,查询时直接范围查询日期。
排名 | 统计日期 | 空间 ID | 空间名称 | 用户 ID | 总大小 (MB) |
---|---|---|---|---|---|
1 | 2024-12-13 | 1001 | 鱼皮的个人空间 | 2001 | 2048 |
2 | 2024-12-13 | 1002 | 张三的个人空间 | 2002 | 1832 |
3 | 2024-12-13 | 1003 | 李四的个人空间 | 2003 | 1456 |
4 | 2024-12-13 | 1004 | 孙五的个人空间 | 2004 | 1387 |
5 | 2024-12-13 | 1005 | 老六的个人空间 | 2005 | 1203 |