feat: 初始化收支管理系统

This commit is contained in:
wangshuo
2026-08-30 14:36:22 +08:00
commit 733ed4bb86
79 changed files with 5808 additions and 0 deletions
@@ -0,0 +1,54 @@
package com.example.finance.service;
import com.example.finance.dto.CategoryRequest;
import com.example.finance.vo.CategoryVO;
import java.util.List;
/**
* 分类服务接口,提供收支分类的增删改查操作。
*/
public interface CategoryService {
/**
* 获取用户的所有分类列表
*
* @param userId 用户ID
* @return 分类列表
*/
List<CategoryVO> list(Long userId);
/**
* 按类型(收入/支出)获取用户的分类列表
*
* @param userId 用户ID
* @param type 分类类型(如 INCOME、EXPENSE)
* @return 分类列表
*/
List<CategoryVO> listByType(Long userId, String type);
/**
* 创建新分类
*
* @param userId 用户ID
* @param request 分类请求参数(名称、类型、图标等)
*/
void create(Long userId, CategoryRequest request);
/**
* 更新分类信息
*
* @param userId 用户ID
* @param id 分类ID
* @param request 分类请求参数
*/
void update(Long userId, Long id, CategoryRequest request);
/**
* 删除分类
*
* @param userId 用户ID
* @param id 分类ID
*/
void delete(Long userId, Long id);
}
@@ -0,0 +1,41 @@
package com.example.finance.service;
import com.example.finance.dto.StatisticsQuery;
import com.example.finance.vo.CategoryStatisticsVO;
import com.example.finance.vo.MonthlyStatisticsVO;
import com.example.finance.vo.TrendStatisticsVO;
import java.util.List;
/**
* 统计服务接口,提供月度收支统计、分类统计和趋势分析功能。
*/
public interface StatisticsService {
/**
* 获取月度收支统计(总收入、总支出、结余、交易笔数)
*
* @param userId 用户ID
* @param query 统计查询条件(年份、月份)
* @return 月度统计结果
*/
MonthlyStatisticsVO getMonthlyStatistics(Long userId, StatisticsQuery query);
/**
* 获取分类维度的统计数据(各分类的金额、笔数、占比)
*
* @param userId 用户ID
* @param query 统计查询条件(年份、月份)
* @return 分类统计列表
*/
List<CategoryStatisticsVO> getCategoryStatistics(Long userId, StatisticsQuery query);
/**
* 获取每日收支趋势统计数据
*
* @param userId 用户ID
* @param query 统计查询条件(年份、月份)
* @return 按日排列的趋势统计列表
*/
List<TrendStatisticsVO> getTrendStatistics(Long userId, StatisticsQuery query);
}
@@ -0,0 +1,67 @@
package com.example.finance.service;
import com.example.finance.common.PageResult;
import com.example.finance.dto.TransactionQuery;
import com.example.finance.dto.TransactionRequest;
import com.example.finance.vo.TransactionExportVO;
import com.example.finance.vo.TransactionVO;
import java.util.List;
/**
* 交易记录服务接口,提供收支记录的分页查询、增删改操作。
*/
public interface TransactionService {
/**
* 分页查询交易记录
*
* @param userId 用户ID
* @param query 查询条件(类型、分类、时间范围、分页参数等)
* @return 分页结果
*/
PageResult<TransactionVO> page(Long userId, TransactionQuery query);
/**
* 创建交易记录
*
* @param userId 用户ID
* @param request 交易请求参数(金额、分类、类型等)
*/
void create(Long userId, TransactionRequest request);
/**
* 更新交易记录
*
* @param userId 用户ID
* @param id 交易记录ID
* @param request 交易请求参数
*/
void update(Long userId, Long id, TransactionRequest request);
/**
* 删除交易记录
*
* @param userId 用户ID
* @param id 交易记录ID
*/
void delete(Long userId, Long id);
/**
* 导出交易记录
*
* @param userId 用户ID
* @param query 查询条件(类型、分类、时间范围等)
* @return 导出视图对象列表
*/
List<TransactionExportVO> export(Long userId, TransactionQuery query);
/**
* 切换单条交易记录的"计入统计"标记
*
* @param userId 用户ID
* @param id 交易记录ID
* @param includeInStats 新的标记值
*/
void toggleStats(Long userId, Long id, Boolean includeInStats);
}
@@ -0,0 +1,35 @@
package com.example.finance.service;
import com.example.finance.dto.LoginRequest;
import com.example.finance.dto.RegisterRequest;
import com.example.finance.vo.LoginVO;
import com.example.finance.vo.UserVO;
/**
* 用户服务接口,提供用户登录、注册和信息查询等操作。
*/
public interface UserService {
/**
* 用户登录
*
* @param request 登录请求参数(用户名、密码)
* @return 登录结果,包含 Token 和用户信息
*/
LoginVO login(LoginRequest request);
/**
* 用户注册
*
* @param request 注册请求参数(用户名、密码、昵称等)
*/
void register(RegisterRequest request);
/**
* 获取用户信息
*
* @param userId 用户ID
* @return 用户信息视图对象
*/
UserVO getUserInfo(Long userId);
}
@@ -0,0 +1,129 @@
package com.example.finance.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.finance.common.BusinessException;
import com.example.finance.dto.CategoryRequest;
import com.example.finance.entity.Category;
import com.example.finance.mapper.CategoryMapper;
import com.example.finance.service.CategoryService;
import com.example.finance.vo.CategoryVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 分类服务实现类,处理收支分类的增删改查业务逻辑。
*/
@Service
public class CategoryServiceImpl implements CategoryService {
private final CategoryMapper categoryMapper;
public CategoryServiceImpl(CategoryMapper categoryMapper) {
this.categoryMapper = categoryMapper;
}
/**
* {@inheritDoc}
*
* 查询用户所有分类,按排序字段和ID升序排列。
*/
@Override
public List<CategoryVO> list(Long userId) {
List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<Category>()
.eq(Category::getUserId, userId)
.orderByAsc(Category::getSortOrder)
.orderByAsc(Category::getId));
return categories.stream().map(this::toVO).collect(Collectors.toList());
}
/**
* {@inheritDoc}
*
* 按分类类型筛选用户分类列表,按排序字段和ID升序排列。
*/
@Override
public List<CategoryVO> listByType(Long userId, String type) {
List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<Category>()
.eq(Category::getUserId, userId)
.eq(Category::getType, type)
.orderByAsc(Category::getSortOrder)
.orderByAsc(Category::getId));
return categories.stream().map(this::toVO).collect(Collectors.toList());
}
/**
* {@inheritDoc}
*
* 创建新分类,若未指定排序值则默认为0。
*/
@Override
@Transactional
public void create(Long userId, CategoryRequest request) {
Category category = new Category();
category.setUserId(userId);
category.setName(request.getName());
category.setType(request.getType());
category.setIcon(request.getIcon());
// 排序值未提供时默认为0
category.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0);
categoryMapper.insert(category);
}
/**
* {@inheritDoc}
*
* 更新分类信息,需校验分类归属当前用户。
*/
@Override
@Transactional
public void update(Long userId, Long id, CategoryRequest request) {
Category category = categoryMapper.selectById(id);
// 校验分类是否存在且属于当前用户
if (category == null || !category.getUserId().equals(userId)) {
throw new BusinessException("分类不存在");
}
category.setName(request.getName());
category.setType(request.getType());
category.setIcon(request.getIcon());
if (request.getSortOrder() != null) {
category.setSortOrder(request.getSortOrder());
}
categoryMapper.updateById(category);
}
/**
* {@inheritDoc}
*
* 删除分类,需校验分类归属当前用户。
*/
@Override
@Transactional
public void delete(Long userId, Long id) {
Category category = categoryMapper.selectById(id);
// 校验分类是否存在且属于当前用户
if (category == null || !category.getUserId().equals(userId)) {
throw new BusinessException("分类不存在");
}
categoryMapper.deleteById(id);
}
/**
* 将分类实体转换为视图对象
*
* @param category 分类实体
* @return 分类视图对象
*/
private CategoryVO toVO(Category category) {
CategoryVO vo = new CategoryVO();
vo.setId(category.getId());
vo.setName(category.getName());
vo.setType(category.getType());
vo.setIcon(category.getIcon());
vo.setSortOrder(category.getSortOrder());
vo.setCreatedAt(category.getCreatedAt());
return vo;
}
}
@@ -0,0 +1,189 @@
package com.example.finance.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.finance.dto.StatisticsQuery;
import com.example.finance.entity.Category;
import com.example.finance.entity.Transaction;
import com.example.finance.mapper.CategoryMapper;
import com.example.finance.mapper.TransactionMapper;
import com.example.finance.service.StatisticsService;
import com.example.finance.vo.CategoryStatisticsVO;
import com.example.finance.vo.MonthlyStatisticsVO;
import com.example.finance.vo.TrendStatisticsVO;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 统计服务实现类,提供月度收支汇总、分类占比分析和每日收支趋势统计等业务逻辑。
*/
@Service
public class StatisticsServiceImpl implements StatisticsService {
private final TransactionMapper transactionMapper;
private final CategoryMapper categoryMapper;
public StatisticsServiceImpl(TransactionMapper transactionMapper, CategoryMapper categoryMapper) {
this.transactionMapper = transactionMapper;
this.categoryMapper = categoryMapper;
}
/**
* {@inheritDoc}
*
* 遍历指定月份的所有交易记录,分别累加收入和支出,计算结余和交易笔数。
*/
@Override
public MonthlyStatisticsVO getMonthlyStatistics(Long userId, StatisticsQuery query) {
List<Transaction> transactions = getMonthTransactions(userId, query);
MonthlyStatisticsVO vo = new MonthlyStatisticsVO();
BigDecimal totalIncome = BigDecimal.ZERO;
BigDecimal totalExpense = BigDecimal.ZERO;
// 按交易类型分别累加收入和支出
for (Transaction t : transactions) {
if ("INCOME".equals(t.getType())) {
totalIncome = totalIncome.add(t.getAmount());
} else {
totalExpense = totalExpense.add(t.getAmount());
}
}
vo.setTotalIncome(totalIncome);
vo.setTotalExpense(totalExpense);
vo.setBalance(totalIncome.subtract(totalExpense));
vo.setTransactionCount(transactions.size());
return vo;
}
/**
* {@inheritDoc}
*
* 按分类ID分组统计交易金额、笔数,并计算各分类占总金额的百分比,结果按金额降序排列。
*/
@Override
public List<CategoryStatisticsVO> getCategoryStatistics(Long userId, StatisticsQuery query) {
List<Transaction> transactions = getMonthTransactions(userId, query);
// 按分类ID分组
Map<Long, List<Transaction>> grouped = transactions.stream()
.collect(Collectors.groupingBy(Transaction::getCategoryId));
// 批量查询分类信息
List<Long> categoryIds = new ArrayList<>(grouped.keySet());
Map<Long, Category> categoryMap = Map.of();
if (!categoryIds.isEmpty()) {
List<Category> categories = categoryMapper.selectBatchIds(categoryIds);
categoryMap = categories.stream().collect(Collectors.toMap(Category::getId, c -> c));
}
// 计算总金额,用于后续计算百分比
BigDecimal totalAmount = transactions.stream()
.map(Transaction::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 构建每个分类的统计结果
Map<Long, Category> finalCategoryMap = categoryMap;
return grouped.entrySet().stream()
.map(entry -> {
List<Transaction> list = entry.getValue();
Category category = finalCategoryMap.get(entry.getKey());
BigDecimal amount = list.stream()
.map(Transaction::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
CategoryStatisticsVO vo = new CategoryStatisticsVO();
vo.setCategoryId(entry.getKey());
vo.setCategoryName(category != null ? category.getName() : "未知");
vo.setCategoryIcon(category != null ? category.getIcon() : "");
vo.setType(category != null ? category.getType() : "");
vo.setAmount(amount);
vo.setCount(list.size());
// 计算占比百分比,保留两位小数,四舍五入
if (totalAmount.compareTo(BigDecimal.ZERO) > 0) {
vo.setPercentage(amount.multiply(BigDecimal.valueOf(100))
.divide(totalAmount, 2, RoundingMode.HALF_UP));
} else {
vo.setPercentage(BigDecimal.ZERO);
}
return vo;
})
// 按金额降序排列
.sorted((a, b) -> b.getAmount().compareTo(a.getAmount()))
.collect(Collectors.toList());
}
/**
* {@inheritDoc}
*
* 统计指定月份每天的收入和支出,返回按日期排列的趋势数据列表。
*/
@Override
public List<TrendStatisticsVO> getTrendStatistics(Long userId, StatisticsQuery query) {
YearMonth yearMonth = YearMonth.of(query.getYear(), query.getMonth());
int daysInMonth = yearMonth.lengthOfMonth();
// 初始化每日收入和支出为零
Map<Integer, BigDecimal> incomeMap = new HashMap<>();
Map<Integer, BigDecimal> expenseMap = new HashMap<>();
for (int i = 1; i <= daysInMonth; i++) {
incomeMap.put(i, BigDecimal.ZERO);
expenseMap.put(i, BigDecimal.ZERO);
}
// 累加每日交易金额
List<Transaction> transactions = getMonthTransactions(userId, query);
for (Transaction t : transactions) {
int day = t.getTransactionTime().getDayOfMonth();
if ("INCOME".equals(t.getType())) {
incomeMap.merge(day, t.getAmount(), BigDecimal::add);
} else {
expenseMap.merge(day, t.getAmount(), BigDecimal::add);
}
}
// 组装每日趋势数据
List<TrendStatisticsVO> result = new ArrayList<>();
for (int i = 1; i <= daysInMonth; i++) {
TrendStatisticsVO vo = new TrendStatisticsVO();
vo.setDate(String.format("%d-%02d-%02d", query.getYear(), query.getMonth(), i));
vo.setIncome(incomeMap.get(i));
vo.setExpense(expenseMap.get(i));
result.add(vo);
}
return result;
}
/**
* 查询指定月份内用户的所有交易记录(仅包含计入统计的记录)
*
* @param userId 用户ID
* @param query 统计查询条件(年份、月份)
* @return 该月份的交易记录列表
*/
private List<Transaction> getMonthTransactions(Long userId, StatisticsQuery query) {
YearMonth yearMonth = YearMonth.of(query.getYear(), query.getMonth());
// 计算月份的起止时间
LocalDateTime startTime = yearMonth.atDay(1).atStartOfDay();
LocalDateTime endTime = yearMonth.atEndOfMonth().atTime(LocalTime.MAX);
return transactionMapper.selectList(new LambdaQueryWrapper<Transaction>()
.eq(Transaction::getUserId, userId)
.ge(Transaction::getTransactionTime, startTime)
.le(Transaction::getTransactionTime, endTime)
// 仅统计计入统计的记录(排除借款、账户间转账等)
.and(w -> w.eq(Transaction::getIncludeInStats, true)
.or().isNull(Transaction::getIncludeInStats)));
}
}
@@ -0,0 +1,318 @@
package com.example.finance.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.finance.common.BusinessException;
import com.example.finance.common.PageResult;
import com.example.finance.dto.TransactionQuery;
import com.example.finance.dto.TransactionRequest;
import com.example.finance.entity.Category;
import com.example.finance.entity.Transaction;
import com.example.finance.mapper.CategoryMapper;
import com.example.finance.mapper.TransactionMapper;
import com.example.finance.service.TransactionService;
import com.example.finance.vo.TransactionExportVO;
import com.example.finance.vo.TransactionVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 交易记录服务实现类,处理收支记录的分页查询、创建、更新和删除等业务逻辑。
*/
@Service
public class TransactionServiceImpl implements TransactionService {
private final TransactionMapper transactionMapper;
private final CategoryMapper categoryMapper;
public TransactionServiceImpl(TransactionMapper transactionMapper, CategoryMapper categoryMapper) {
this.transactionMapper = transactionMapper;
this.categoryMapper = categoryMapper;
}
/**
* {@inheritDoc}
*
* 支持按类型、分类、时间范围进行条件查询,结果按交易时间降序排列,
* 同时批量关联分类信息以填充分类名称和图标。
*/
@Override
public PageResult<TransactionVO> page(Long userId, TransactionQuery query) {
LambdaQueryWrapper<Transaction> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Transaction::getUserId, userId);
// 按交易类型筛选(多选)
if (query.getTypes() != null && !query.getTypes().isEmpty()) {
wrapper.in(Transaction::getType, query.getTypes());
}
// 按分类ID筛选(多选)
if (query.getCategoryIds() != null && !query.getCategoryIds().isEmpty()) {
wrapper.in(Transaction::getCategoryId, query.getCategoryIds());
}
// 按起始时间筛选
if (StringUtils.hasText(query.getStartTime())) {
wrapper.ge(Transaction::getTransactionTime,
LocalDateTime.parse(query.getStartTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
// 按结束时间筛选
if (StringUtils.hasText(query.getEndTime())) {
wrapper.le(Transaction::getTransactionTime,
LocalDateTime.parse(query.getEndTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
// 按支付方式筛选(多选)
if (query.getPaymentMethods() != null && !query.getPaymentMethods().isEmpty()) {
wrapper.in(Transaction::getPaymentMethod, query.getPaymentMethods());
}
// 按是否计入统计筛选
if (query.getIncludeInStats() != null) {
wrapper.eq(Transaction::getIncludeInStats, query.getIncludeInStats());
}
// 按交易时间降序排列
wrapper.orderByDesc(Transaction::getTransactionTime);
Page<Transaction> page = new Page<>(query.getPage(), query.getSize());
Page<Transaction> result = transactionMapper.selectPage(page, wrapper);
// 批量查询关联的分类信息,避免 N+1 查询问题
List<Long> categoryIds = result.getRecords().stream()
.map(Transaction::getCategoryId)
.distinct()
.collect(Collectors.toList());
Map<Long, Category> categoryMap = Map.of();
if (!categoryIds.isEmpty()) {
List<Category> categories = categoryMapper.selectBatchIds(categoryIds);
categoryMap = categories.stream().collect(Collectors.toMap(Category::getId, c -> c));
}
// 将交易记录转换为视图对象,并填充分类名称和图标
Map<Long, Category> finalCategoryMap = categoryMap;
List<TransactionVO> voList = result.getRecords().stream()
.map(t -> toVO(t, finalCategoryMap.get(t.getCategoryId())))
.collect(Collectors.toList());
return new PageResult<>(voList, result.getTotal(), result.getCurrent(), result.getSize());
}
/**
* {@inheritDoc}
*
* 创建交易记录前需校验分类是否存在且归属当前用户。
*/
@Override
@Transactional
public void create(Long userId, TransactionRequest request) {
// 校验分类是否存在且属于当前用户
Category category = categoryMapper.selectById(request.getCategoryId());
if (category == null || !category.getUserId().equals(userId)) {
throw new BusinessException("分类不存在");
}
Transaction transaction = new Transaction();
transaction.setUserId(userId);
transaction.setCategoryId(request.getCategoryId());
transaction.setType(request.getType());
transaction.setAmount(request.getAmount());
transaction.setPaymentMethod(request.getPaymentMethod());
transaction.setRemark(request.getRemark());
transaction.setTransactionTime(request.getTransactionTime());
transaction.setIncludeInStats(request.getIncludeInStats() == null ? Boolean.TRUE : request.getIncludeInStats());
transactionMapper.insert(transaction);
}
/**
* {@inheritDoc}
*
* 更新交易记录前需校验记录归属和分类归属。
*/
@Override
@Transactional
public void update(Long userId, Long id, TransactionRequest request) {
// 校验交易记录是否存在且属于当前用户
Transaction transaction = transactionMapper.selectById(id);
if (transaction == null || !transaction.getUserId().equals(userId)) {
throw new BusinessException("记录不存在");
}
// 校验分类是否存在且属于当前用户
Category category = categoryMapper.selectById(request.getCategoryId());
if (category == null || !category.getUserId().equals(userId)) {
throw new BusinessException("分类不存在");
}
transaction.setCategoryId(request.getCategoryId());
transaction.setType(request.getType());
transaction.setAmount(request.getAmount());
transaction.setPaymentMethod(request.getPaymentMethod());
transaction.setRemark(request.getRemark());
transaction.setTransactionTime(request.getTransactionTime());
transaction.setIncludeInStats(request.getIncludeInStats() == null ? Boolean.TRUE : request.getIncludeInStats());
transactionMapper.updateById(transaction);
}
/**
* {@inheritDoc}
*
* 删除交易记录前需校验记录归属当前用户。
*/
@Override
@Transactional
public void delete(Long userId, Long id) {
Transaction transaction = transactionMapper.selectById(id);
if (transaction == null || !transaction.getUserId().equals(userId)) {
throw new BusinessException("记录不存在");
}
transactionMapper.deleteById(id);
}
/**
* {@inheritDoc}
*
* 切换单条交易记录的"计入统计"标记,用于表格中直接开关。
*/
@Override
@Transactional
public void toggleStats(Long userId, Long id, Boolean includeInStats) {
Transaction transaction = transactionMapper.selectById(id);
if (transaction == null || !transaction.getUserId().equals(userId)) {
throw new BusinessException("记录不存在");
}
transaction.setIncludeInStats(includeInStats == null ? Boolean.TRUE : includeInStats);
transactionMapper.updateById(transaction);
}
/**
* 将交易实体和关联分类转换为视图对象
*
* @param t 交易实体
* @param category 关联的分类实体(可为 null)
* @return 交易视图对象
*/
private TransactionVO toVO(Transaction t, Category category) {
TransactionVO vo = new TransactionVO();
vo.setId(t.getId());
vo.setCategoryId(t.getCategoryId());
vo.setCategoryName(category != null ? category.getName() : "");
vo.setCategoryIcon(category != null ? category.getIcon() : "");
vo.setType(t.getType());
vo.setAmount(t.getAmount());
vo.setPaymentMethod(t.getPaymentMethod());
vo.setRemark(t.getRemark());
vo.setTransactionTime(t.getTransactionTime());
vo.setIncludeInStats(t.getIncludeInStats() == null ? Boolean.TRUE : t.getIncludeInStats());
vo.setCreatedAt(t.getCreatedAt());
return vo;
}
/**
* {@inheritDoc}
*
* 根据查询条件导出交易记录,不分页,返回所有匹配的记录。
*/
@Override
public List<TransactionExportVO> export(Long userId, TransactionQuery query) {
LambdaQueryWrapper<Transaction> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Transaction::getUserId, userId);
// 按交易类型筛选(多选)
if (query.getTypes() != null && !query.getTypes().isEmpty()) {
wrapper.in(Transaction::getType, query.getTypes());
}
// 按分类ID筛选(多选)
if (query.getCategoryIds() != null && !query.getCategoryIds().isEmpty()) {
wrapper.in(Transaction::getCategoryId, query.getCategoryIds());
}
// 按起始时间筛选
if (StringUtils.hasText(query.getStartTime())) {
wrapper.ge(Transaction::getTransactionTime,
LocalDateTime.parse(query.getStartTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
// 按结束时间筛选
if (StringUtils.hasText(query.getEndTime())) {
wrapper.le(Transaction::getTransactionTime,
LocalDateTime.parse(query.getEndTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
// 按支付方式筛选(多选)
if (query.getPaymentMethods() != null && !query.getPaymentMethods().isEmpty()) {
wrapper.in(Transaction::getPaymentMethod, query.getPaymentMethods());
}
// 按是否计入统计筛选
if (query.getIncludeInStats() != null) {
wrapper.eq(Transaction::getIncludeInStats, query.getIncludeInStats());
}
// 按交易时间降序排列
wrapper.orderByDesc(Transaction::getTransactionTime);
List<Transaction> transactions = transactionMapper.selectList(wrapper);
// 批量查询关联的分类信息
List<Long> categoryIds = transactions.stream()
.map(Transaction::getCategoryId)
.distinct()
.collect(Collectors.toList());
Map<Long, Category> categoryMap = Map.of();
if (!categoryIds.isEmpty()) {
List<Category> categories = categoryMapper.selectBatchIds(categoryIds);
categoryMap = categories.stream().collect(Collectors.toMap(Category::getId, c -> c));
}
// 转换为导出视图对象
Map<Long, Category> finalCategoryMap = categoryMap;
return transactions.stream()
.map(t -> toExportVO(t, finalCategoryMap.get(t.getCategoryId())))
.collect(Collectors.toList());
}
/**
* 将交易实体和关联分类转换为导出视图对象
*
* @param t 交易实体
* @param category 关联的分类实体(可为 null)
* @return 交易导出视图对象
*/
private TransactionExportVO toExportVO(Transaction t, Category category) {
TransactionExportVO vo = new TransactionExportVO();
Boolean include = t.getIncludeInStats() == null ? Boolean.TRUE : t.getIncludeInStats();
if (include) {
vo.setType(t.getType().equals("INCOME") ? "收入" : "支出");
} else {
vo.setType("不计入统计");
}
vo.setCategoryName(category != null ? category.getName() : "");
vo.setAmount(String.format("%.2f", t.getAmount()));
vo.setPaymentMethod(getPaymentMethodLabel(t.getPaymentMethod()));
vo.setRemark(t.getRemark() != null ? t.getRemark() : "");
vo.setTransactionTime(t.getTransactionTime() != null
? t.getTransactionTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
: "");
vo.setIncludeInStats(include ? "是" : "否");
return vo;
}
/**
* 将支付方式编码转换为中文标签
*
* @param code 支付方式编码
* @return 中文标签
*/
private String getPaymentMethodLabel(String code) {
if (code == null) return "";
return switch (code) {
case "WECHAT" -> "微信";
case "ALIPAY" -> "支付宝";
case "CASH" -> "现金";
case "BANK" -> "银行卡";
default -> code;
};
}
}
@@ -0,0 +1,114 @@
package com.example.finance.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.finance.common.BusinessException;
import com.example.finance.dto.LoginRequest;
import com.example.finance.dto.RegisterRequest;
import com.example.finance.entity.User;
import com.example.finance.mapper.UserMapper;
import com.example.finance.service.UserService;
import com.example.finance.util.JwtUtil;
import com.example.finance.vo.LoginVO;
import com.example.finance.vo.UserVO;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户服务实现类,处理用户登录认证、注册和信息查询等业务逻辑。
*/
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final JwtUtil jwtUtil;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public UserServiceImpl(UserMapper userMapper, JwtUtil jwtUtil) {
this.userMapper = userMapper;
this.jwtUtil = jwtUtil;
}
/**
* {@inheritDoc}
*
* 根据用户名查询用户,校验密码后生成 JWT Token 返回。
*/
@Override
public LoginVO login(LoginRequest request) {
// 根据用户名查询用户
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, request.getUsername()));
if (user == null) {
throw new BusinessException("用户名或密码错误");
}
// 校验密码是否匹配
if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
throw new BusinessException("用户名或密码错误");
}
// 生成 JWT Token
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
LoginVO vo = new LoginVO();
vo.setToken(token);
vo.setUser(toUserVO(user));
return vo;
}
/**
* {@inheritDoc}
*
* 校验用户名是否已存在,不存在则创建新用户并对密码进行 BCrypt 加密。
*/
@Override
@Transactional
public void register(RegisterRequest request) {
// 检查用户名是否已被注册
User existing = userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, request.getUsername()));
if (existing != null) {
throw new BusinessException("用户名已存在");
}
// 构建新用户并加密密码
User user = new User();
user.setUsername(request.getUsername());
user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
// 如果未提供昵称,则使用用户名作为默认昵称
user.setNickname(request.getNickname() != null ? request.getNickname() : request.getUsername());
userMapper.insert(user);
}
/**
* {@inheritDoc}
*
* 根据用户ID查询用户信息,不存在则抛出异常。
*/
@Override
public UserVO getUserInfo(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
return toUserVO(user);
}
/**
* 将用户实体转换为视图对象
*
* @param user 用户实体
* @return 用户视图对象
*/
private UserVO toUserVO(User user) {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setNickname(user.getNickname());
vo.setCreatedAt(user.getCreatedAt());
return vo;
}
}