feat: 初始化收支管理系统
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user