feat: 表格分页支持统一配置并记忆各表格每页条数

- 后端新增聚合用户偏好 paginationPrefs(每页条数候选列表与默认每页条数),提供 GET/PUT /api/setting/pagination-prefs,默认候选 [10,20,50,100]、默认每页 20,非法值自动过滤并回退
- 后端新增 /category/page、/tag/page、/channel/page、/email-bill/configs/page、/email-bill/sender-rules/page 分页接口,原全量接口保持不变以免影响下拉、筛选、AI 与导出
- 前端新增共享 composables/usePageSizeOptions.ts 与 useTablePageSize.ts,所有表格统一取用候选列表并记住各自上次选择的每页条数,优先级为表格记忆 > 全局默认 > 20
- 各表格上次选择以聚合偏好 finance.pagination.tablePageSizes 存于本机 localStorage,收支记录页旧的 filterPrefs.pageSize 自动迁移
- 设置页外观设置新增「表格每页条数选项」分组,可增删候选值并配置默认每页条数,保存后全局即时生效
- 路由守卫在已登录时预加载分页偏好,保证表格页首次渲染即使用配置值
- 前端分类、标签、渠道、邮箱配置、发件人规则表格接入后端分页
- 补充中文、英文、日文三语文案
This commit is contained in:
2026-09-23 11:14:54 +08:00
parent 7172f6aa44
commit e0fa1abec5
39 changed files with 1042 additions and 62 deletions
@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.CategoryRequest;
import cn.rosercode.finance.service.CategoryService;
@@ -54,6 +55,24 @@ public class CategoryController {
return Result.success(categoryService.list(userId));
}
/**
* 分页获取分类列表
*
* @param type 分类类型(可选),用于按类型筛选
* @param page 页码
* @param size 每页大小
* @return 分页结果
*/
@GetMapping("/page")
@Operation(summary = "分页获取分类列表", description = "分页查询当前用户的分类,支持按类型筛选")
public Result<PageResult<CategoryVO>> page(
@Parameter(description = "分类类型,可选值:income/expense") @RequestParam(required = false) String type,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
Long userId = UserContext.getUserId();
return Result.success(categoryService.page(userId, type, page, size));
}
/**
* 创建分类
*
@@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.ChannelRequest;
import cn.rosercode.finance.service.ChannelService;
@@ -49,6 +51,21 @@ public class ChannelController {
return Result.success(channelService.list(UserContext.getUserId()));
}
/**
* 分页查询当前用户的自定义渠道列表
*
* @param page 页码
* @param size 每页大小
* @return 分页结果(含使用账单数)
*/
@GetMapping("/page")
@Operation(summary = "分页渠道列表", description = "分页查询当前用户的自定义交易渠道,含各自被使用的账单数")
public Result<PageResult<ChannelVO>> page(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
return Result.success(channelService.page(UserContext.getUserId(), page, size));
}
/**
* 新建自定义渠道
*
@@ -79,6 +79,22 @@ public class EmailBillController {
return Result.success(configService.list(userId));
}
/**
* 分页查询邮箱配置列表
*
* @param page 页码
* @param size 每页大小
* @return 分页结果
*/
@GetMapping("/configs/page")
@Operation(summary = "分页查询邮箱配置列表", description = "分页获取当前用户的 IMAP 邮箱配置,授权码已脱敏")
public Result<PageResult<EmailBillConfigVO>> pageConfigs(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
Long userId = UserContext.getUserId();
return Result.success(configService.page(userId, page, size));
}
/**
* 创建邮箱配置
*
@@ -164,6 +180,22 @@ public class EmailBillController {
return Result.success(senderRuleService.list(userId));
}
/**
* 分页查询发件人规则列表
*
* @param page 页码
* @param size 每页大小
* @return 分页结果
*/
@GetMapping("/sender-rules/page")
@Operation(summary = "分页查询发件人规则列表", description = "分页获取当前用户的发件人识别规则,按优先级排序")
public Result<PageResult<EmailBillSenderRuleVO>> pageSenderRules(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
Long userId = UserContext.getUserId();
return Result.success(senderRuleService.page(userId, page, size));
}
/**
* 创建发件人规则
*
@@ -13,8 +13,10 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.TagRequest;
import cn.rosercode.finance.service.TagService;
@@ -51,6 +53,22 @@ public class TagController {
return Result.success(tagService.list(userId));
}
/**
* 分页获取标签列表
*
* @param page 页码
* @param size 每页大小
* @return 分页结果
*/
@GetMapping("/page")
@Operation(summary = "分页获取标签列表", description = "分页查询当前用户的标签")
public Result<PageResult<TagVO>> page(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
Long userId = UserContext.getUserId();
return Result.success(tagService.page(userId, page, size));
}
/**
* 创建标签
*
@@ -13,15 +13,17 @@ import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.BillSourceStatsRequest;
import cn.rosercode.finance.dto.ExcludedDisplayStyleRequest;
import cn.rosercode.finance.dto.PaginationPrefsRequest;
import cn.rosercode.finance.dto.ShowHiddenDataRequest;
import cn.rosercode.finance.service.UserPreferenceService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.PaginationPrefsVO;
/**
* 用户设置控制器
*
* <p>提供按当前登录用户存储的偏好配置读写接口,包含账单来源统计默认值与
* 「不计入统计」记录的展示样式。</p>
* <p>提供按当前登录用户存储的偏好配置读写接口,包含账单来源统计默认值、
* 「不计入统计」记录的展示样式与表格分页偏好。</p>
*
* @date 2026-09-21 10:14:12
*/
@@ -111,4 +113,32 @@ public class UserSettingController {
userPreferenceService.saveShowHiddenData(userId, show);
return Result.success(userPreferenceService.getShowHiddenData(userId));
}
/**
* 查询当前用户配置的表格分页偏好。
*
* @return 分页偏好(每页条数候选列表与默认每页条数),未配置时返回系统默认
*/
@GetMapping("/pagination-prefs")
@Operation(summary = "查询分页偏好", description = "读取当前用户所有表格分页的每页条数候选列表与默认每页条数")
public Result<PaginationPrefsVO> getPaginationPrefs() {
return Result.success(userPreferenceService.getPaginationPrefs(UserContext.getUserId()));
}
/**
* 更新当前用户配置的表格分页偏好。
*
* @param request 分页偏好更新请求
* @return 更新后的分页偏好,空或非法时回退系统默认
*/
@PutMapping("/pagination-prefs")
@Operation(summary = "更新分页偏好", description = "设置当前用户所有表格分页的每页条数候选列表与默认每页条数")
public Result<PaginationPrefsVO> updatePaginationPrefs(@RequestBody PaginationPrefsRequest request) {
Long userId = UserContext.getUserId();
PaginationPrefsVO prefs = new PaginationPrefsVO();
prefs.setOptions(request.getOptions());
prefs.setDefaultSize(request.getDefaultSize());
userPreferenceService.savePaginationPrefs(userId, prefs);
return Result.success(userPreferenceService.getPaginationPrefs(userId));
}
}
@@ -0,0 +1,20 @@
package cn.rosercode.finance.dto;
import java.util.List;
import lombok.Data;
/**
* 表格分页偏好更新请求对象。
*
* @date 2026-09-23 11:05:25
*/
@Data
public class PaginationPrefsRequest {
/** 每页条数候选列表 */
private List<Integer> options;
/** 默认每页条数 */
private Integer defaultSize;
}
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.CategoryRequest;
import cn.rosercode.finance.vo.CategoryVO;
@@ -27,6 +28,17 @@ public interface CategoryService {
*/
List<CategoryVO> listByType(Long userId, String type);
/**
* 分页查询用户的分类,可按类型筛选。
*
* @param userId 用户ID
* @param type 分类类型(如 INCOME、EXPENSE),为空时查询全部
* @param page 页码,从 1 开始
* @param size 每页大小
* @return 分页结果
*/
PageResult<CategoryVO> page(Long userId, String type, int page, int size);
/**
* 创建新分类
*
@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.ChannelRequest;
import cn.rosercode.finance.vo.ChannelOptionVO;
import cn.rosercode.finance.vo.ChannelVO;
@@ -23,6 +24,16 @@ public interface ChannelService {
*/
List<ChannelVO> list(Long userId);
/**
* 分页查询当前用户的自定义渠道列表(含各自使用账单数)。
*
* @param userId 用户ID
* @param page 页码,从 1 开始
* @param size 每页大小
* @return 分页结果
*/
PageResult<ChannelVO> page(Long userId, int page, int size);
/**
* 新建自定义渠道。
*
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.EmailBillConfigRequest;
import cn.rosercode.finance.entity.EmailBillConfig;
import cn.rosercode.finance.vo.EmailBillConfigVO;
@@ -21,6 +22,16 @@ public interface EmailBillConfigService {
*/
List<EmailBillConfigVO> list(Long userId);
/**
* 分页查询当前用户的邮箱配置。
*
* @param userId 当前用户ID
* @param page 页码,从 1 开始
* @param size 每页大小
* @return 分页结果
*/
PageResult<EmailBillConfigVO> page(Long userId, int page, int size);
/**
* 新增邮箱配置。
*
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.EmailBillSenderRuleRequest;
import cn.rosercode.finance.entity.EmailBillSenderRule;
import cn.rosercode.finance.vo.EmailBillSenderRuleVO;
@@ -21,6 +22,16 @@ public interface EmailBillSenderRuleService {
*/
List<EmailBillSenderRuleVO> list(Long userId);
/**
* 分页查询当前用户的发件人规则。
*
* @param userId 当前用户ID
* @param page 页码,从 1 开始
* @param size 每页大小
* @return 分页结果,按优先级升序
*/
PageResult<EmailBillSenderRuleVO> page(Long userId, int page, int size);
/**
* 新增发件人规则。
*
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.TagRequest;
import cn.rosercode.finance.vo.TagVO;
@@ -20,6 +21,16 @@ public interface TagService {
*/
List<TagVO> list(Long userId);
/**
* 分页查询用户的标签列表。
*
* @param userId 用户ID
* @param page 页码,从 1 开始
* @param size 每页大小
* @return 分页结果
*/
PageResult<TagVO> page(Long userId, int page, int size);
/**
* 创建新标签
*
@@ -2,6 +2,8 @@ package cn.rosercode.finance.service;
import java.util.Map;
import cn.rosercode.finance.vo.PaginationPrefsVO;
/**
* 用户偏好服务接口,提供按用户存储的偏好配置读写。
*
@@ -56,4 +58,20 @@ public interface UserPreferenceService {
* @param show 是否显示
*/
void saveShowHiddenData(Long userId, boolean show);
/**
* 获取当前用户配置的表格分页偏好(每页条数候选列表与默认每页条数),未配置或非法时回退默认值。
*
* @param userId 用户ID
* @return 分页偏好
*/
PaginationPrefsVO getPaginationPrefs(Long userId);
/**
* 保存当前用户配置的表格分页偏好,空或非法时回退默认值。
*
* @param userId 用户ID
* @param prefs 分页偏好
*/
void savePaginationPrefs(Long userId, PaginationPrefsVO prefs);
}
@@ -4,11 +4,13 @@ import java.util.List;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.converter.VoConverter;
import cn.rosercode.finance.dto.CategoryRequest;
import cn.rosercode.finance.entity.Category;
@@ -60,6 +62,28 @@ public class CategoryServiceImpl implements CategoryService {
return categories.stream().map(voConverter::toCategoryVO).collect(Collectors.toList());
}
/**
* {@inheritDoc}
*
* 按排序字段和ID升序分页,type 为空时查询全部类型。
*/
@Override
public PageResult<CategoryVO> page(Long userId, String type, int page, int size) {
LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper<Category>()
.eq(Category::getUserId, userId);
if (type != null && !type.isEmpty()) {
wrapper.eq(Category::getType, type);
}
wrapper.orderByAsc(Category::getSortOrder).orderByAsc(Category::getId);
int pageNo = page < 1 ? 1 : page;
int pageSize = size < 1 ? 20 : size;
Page<Category> result = categoryMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
List<CategoryVO> records = result.getRecords().stream()
.map(voConverter::toCategoryVO)
.collect(Collectors.toList());
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
}
/**
* {@inheritDoc}
*
@@ -9,11 +9,13 @@ import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.ChannelRequest;
import cn.rosercode.finance.entity.Channel;
import cn.rosercode.finance.entity.Transaction;
@@ -69,16 +71,48 @@ public class ChannelServiceImpl implements ChannelService {
.orderByAsc(Channel::getId));
List<ChannelVO> result = new ArrayList<>();
for (Channel channel : channels) {
ChannelVO vo = new ChannelVO();
vo.setId(channel.getId());
vo.setCode(channel.getCode());
vo.setName(channel.getName());
vo.setTransactionCount(countTransactions(userId, channel.getCode()));
result.add(vo);
result.add(toChannelVO(userId, channel));
}
return result;
}
/**
* {@inheritDoc}
*
* 按排序字段和ID升序分页,并对当前页渠道逐条统计使用账单数。
*/
@Override
public PageResult<ChannelVO> page(Long userId, int page, int size) {
LambdaQueryWrapper<Channel> wrapper = new LambdaQueryWrapper<Channel>()
.eq(Channel::getUserId, userId)
.orderByAsc(Channel::getSortOrder)
.orderByAsc(Channel::getId);
int pageNo = page < 1 ? 1 : page;
int pageSize = size < 1 ? 20 : size;
Page<Channel> result = channelMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
List<ChannelVO> records = new ArrayList<>();
for (Channel channel : result.getRecords()) {
records.add(toChannelVO(userId, channel));
}
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
}
/**
* 将渠道实体转换为视图对象,并填充使用账单数。
*
* @param userId 用户ID
* @param channel 渠道实体
* @return 渠道视图对象
*/
private ChannelVO toChannelVO(Long userId, Channel channel) {
ChannelVO vo = new ChannelVO();
vo.setId(channel.getId());
vo.setCode(channel.getCode());
vo.setName(channel.getName());
vo.setTransactionCount(countTransactions(userId, channel.getCode()));
return vo;
}
/**
* {@inheritDoc}
*
@@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -17,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.converter.VoConverter;
import cn.rosercode.finance.dto.EmailBillConfigRequest;
import cn.rosercode.finance.email.EmailBillMonitorManager;
@@ -105,6 +107,26 @@ public class EmailBillConfigServiceImpl implements EmailBillConfigService {
return result;
}
/**
* {@inheritDoc}
*
* 按配置ID升序分页。
*/
@Override
public PageResult<EmailBillConfigVO> page(Long userId, int page, int size) {
LambdaQueryWrapper<EmailBillConfig> wrapper = new LambdaQueryWrapper<EmailBillConfig>()
.eq(EmailBillConfig::getUserId, userId)
.orderByAsc(EmailBillConfig::getId);
int pageNo = page < 1 ? 1 : page;
int pageSize = size < 1 ? 20 : size;
Page<EmailBillConfig> result = configMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
List<EmailBillConfigVO> records = new ArrayList<>();
for (EmailBillConfig config : result.getRecords()) {
records.add(toVO(config));
}
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
}
/**
* {@inheritDoc}
*
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -13,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.converter.VoConverter;
import cn.rosercode.finance.dto.EmailBillSenderRuleRequest;
import cn.rosercode.finance.entity.Category;
@@ -72,6 +74,27 @@ public class EmailBillSenderRuleServiceImpl implements EmailBillSenderRuleServic
return result;
}
/**
* {@inheritDoc}
*
* 按优先级和ID升序分页。
*/
@Override
public PageResult<EmailBillSenderRuleVO> page(Long userId, int page, int size) {
LambdaQueryWrapper<EmailBillSenderRule> wrapper = new LambdaQueryWrapper<EmailBillSenderRule>()
.eq(EmailBillSenderRule::getUserId, userId)
.orderByAsc(EmailBillSenderRule::getSortOrder)
.orderByAsc(EmailBillSenderRule::getId);
int pageNo = page < 1 ? 1 : page;
int pageSize = size < 1 ? 20 : size;
Page<EmailBillSenderRule> result = senderRuleMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
List<EmailBillSenderRuleVO> records = new ArrayList<>();
for (EmailBillSenderRule rule : result.getRecords()) {
records.add(toVO(rule));
}
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
}
/**
* {@inheritDoc}
*/
@@ -4,11 +4,13 @@ import java.util.List;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.converter.VoConverter;
import cn.rosercode.finance.dto.TagRequest;
import cn.rosercode.finance.entity.RuleTag;
@@ -56,6 +58,26 @@ public class TagServiceImpl implements TagService {
return tags.stream().map(voConverter::toTagVO).collect(Collectors.toList());
}
/**
* {@inheritDoc}
*
* 按排序字段和ID升序分页。
*/
@Override
public PageResult<TagVO> page(Long userId, int page, int size) {
LambdaQueryWrapper<Tag> wrapper = new LambdaQueryWrapper<Tag>()
.eq(Tag::getUserId, userId)
.orderByAsc(Tag::getSortOrder)
.orderByAsc(Tag::getId);
int pageNo = page < 1 ? 1 : page;
int pageSize = size < 1 ? 20 : size;
Page<Tag> result = tagMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
List<TagVO> records = result.getRecords().stream()
.map(voConverter::toTagVO)
.collect(Collectors.toList());
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
}
/**
* {@inheritDoc}
*
@@ -2,8 +2,10 @@ package cn.rosercode.finance.service.impl;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -19,6 +21,7 @@ import cn.rosercode.finance.entity.UserPreference;
import cn.rosercode.finance.enums.BillType;
import cn.rosercode.finance.mapper.UserPreferenceMapper;
import cn.rosercode.finance.service.UserPreferenceService;
import cn.rosercode.finance.vo.PaginationPrefsVO;
/**
* 用户偏好服务实现类,以键值形式读写每个用户的偏好配置。
@@ -40,6 +43,24 @@ public class UserPreferenceServiceImpl implements UserPreferenceService {
/** 偏好键:是否显示已隐藏备注的记录 */
private static final String KEY_SHOW_HIDDEN_DATA = "showHiddenData";
/** 偏好键:表格分页偏好(每页条数候选列表与默认每页条数) */
private static final String KEY_PAGINATION_PREFS = "paginationPrefs";
/** 默认的表格分页每页条数候选列表 */
private static final List<Integer> DEFAULT_PAGE_SIZE_OPTIONS = List.of(10, 20, 50, 100);
/** 默认每页条数 */
private static final int DEFAULT_PAGE_SIZE = 20;
/** 每页条数下限 */
private static final int MIN_PAGE_SIZE = 1;
/** 每页条数上限 */
private static final int MAX_PAGE_SIZE = 1000;
/** 每页条数候选值最大数量 */
private static final int MAX_PAGE_SIZE_OPTIONS = 10;
/** 默认计入统计的账单来源,其余来源默认不计入 */
private static final Set<String> DEFAULT_INCLUDED_SOURCES = Set.of("WECHAT", "ALIPAY");
@@ -146,6 +167,59 @@ public class UserPreferenceServiceImpl implements UserPreferenceService {
log.info("保存隐藏备注记录显示偏好: userId={}, show={}", userId, show);
}
/**
* {@inheritDoc}
*
* 结果按用户缓存,未配置或取值非法时回退默认值。
*/
@Override
@Cacheable(cacheNames = CacheConfig.USER_PREFERENCE_CACHE, key = "'paginationPrefs:' + #userId")
public PaginationPrefsVO getPaginationPrefs(Long userId) {
return normalizePaginationPrefs(parsePaginationPrefs(findValue(userId, KEY_PAGINATION_PREFS)));
}
/**
* {@inheritDoc}
*
* 保存后失效该用户的分页偏好缓存。
*/
@Override
@CacheEvict(cacheNames = CacheConfig.USER_PREFERENCE_CACHE, key = "'paginationPrefs:' + #userId")
public void savePaginationPrefs(Long userId, PaginationPrefsVO prefs) {
PaginationPrefsVO normalized = normalizePaginationPrefs(prefs);
upsert(userId, KEY_PAGINATION_PREFS, toJson(normalized));
log.info("保存表格分页偏好: userId={}, options={}, defaultSize={}",
userId, normalized.getOptions(), normalized.getDefaultSize());
}
/**
* 规范化分页偏好:候选列表过滤非正整数与越界值、去重升序并限制数量(为空回退默认);
* 默认每页条数须为候选列表内的正整数,否则回退候选列表首项。
*
* @param prefs 原始分页偏好
* @return 规范化后的分页偏好
*/
private PaginationPrefsVO normalizePaginationPrefs(PaginationPrefsVO prefs) {
List<Integer> options = prefs != null ? prefs.getOptions() : null;
List<Integer> normalizedOptions = options == null ? List.of() : options.stream()
.filter(value -> value != null && value >= MIN_PAGE_SIZE && value <= MAX_PAGE_SIZE)
.distinct()
.sorted()
.limit(MAX_PAGE_SIZE_OPTIONS)
.collect(Collectors.toList());
if (normalizedOptions.isEmpty()) {
normalizedOptions = DEFAULT_PAGE_SIZE_OPTIONS;
}
// 未配置或未传默认值时回退系统默认每页条数,再校验其是否在候选列表内
Integer rawDefault = prefs != null ? prefs.getDefaultSize() : null;
int defaultSize = rawDefault != null ? rawDefault : DEFAULT_PAGE_SIZE;
int resolvedDefault = normalizedOptions.contains(defaultSize) ? defaultSize : normalizedOptions.get(0);
PaginationPrefsVO result = new PaginationPrefsVO();
result.setOptions(List.copyOf(normalizedOptions));
result.setDefaultSize(resolvedDefault);
return result;
}
/**
* 查询指定用户的某个偏好值。
*
@@ -202,6 +276,24 @@ public class UserPreferenceServiceImpl implements UserPreferenceService {
}
}
/**
* 将 JSON 字符串解析为分页偏好,解析失败或为空时返回 null。
*
* @param json JSON 字符串
* @return 分页偏好,解析失败时为 null
*/
private PaginationPrefsVO parsePaginationPrefs(String json) {
if (json == null || json.isBlank()) {
return null;
}
try {
return objectMapper.readValue(json, PaginationPrefsVO.class);
} catch (JsonProcessingException e) {
log.warn("解析表格分页偏好失败:{}", e.getMessage());
return null;
}
}
/**
* 将对象序列化为 JSON 字符串。
*
@@ -0,0 +1,20 @@
package cn.rosercode.finance.vo;
import java.util.List;
import lombok.Data;
/**
* 表格分页用户偏好视图对象,包含每页条数候选列表与默认每页条数。
*
* @date 2026-09-23 11:05:25
*/
@Data
public class PaginationPrefsVO {
/** 每页条数候选列表,升序去重 */
private List<Integer> options;
/** 默认每页条数,必为候选列表中的值 */
private Integer defaultSize;
}