feat(账户功能新增)

1、实现单个账户功能,支持账户余额自动计算(初始余额 + 收入 - 支出),创建单独的账户管理页面,账单自动关联到默认账户
This commit is contained in:
ni ziyi 2025-12-13 21:55:55 +08:00
parent a8889cf432
commit 3df3aea533
10 changed files with 317 additions and 3 deletions

View File

@ -0,0 +1,55 @@
package com.accounting.controller;
import com.accounting.dto.AccountRequest;
import com.accounting.dto.AccountResponse;
import com.accounting.entity.User;
import com.accounting.mapper.UserMapper;
import com.accounting.service.AccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@Tag(name = "账户管理", description = "账户管理接口")
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
@Autowired
private UserMapper userMapper;
@Operation(summary = "获取账户信息")
@GetMapping
public ResponseEntity<AccountResponse> getAccount(Authentication authentication) {
Long userId = getUserId(authentication);
AccountResponse response = accountService.getAccountBalance(userId);
return ResponseEntity.ok(response);
}
@Operation(summary = "更新账户信息")
@PutMapping
public ResponseEntity<AccountResponse> updateAccount(
@RequestBody AccountRequest request,
Authentication authentication) {
Long userId = getUserId(authentication);
AccountResponse response = accountService.updateAccount(userId, request);
return ResponseEntity.ok(response);
}
private Long getUserId(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
User user = userMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
);
return user != null ? user.getId() : null;
}
}

View File

@ -0,0 +1,13 @@
package com.accounting.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class AccountRequest {
private String name;
private BigDecimal initialBalance;
}

View File

@ -0,0 +1,26 @@
package com.accounting.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class AccountResponse {
private Long id;
private String name;
private BigDecimal initialBalance;
private BigDecimal balance; // 计算后的余额初始余额 + 收入 - 支出
private BigDecimal totalIncome; // 总收入
private BigDecimal totalExpense; // 总支出
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,27 @@
package com.accounting.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("account")
public class Account {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String name;
private BigDecimal initialBalance;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@ -17,6 +17,8 @@ public class Bill {
private Long userId; private Long userId;
private Long accountId;
private Long categoryId; private Long categoryId;
private BigDecimal amount; private BigDecimal amount;

View File

@ -0,0 +1,10 @@
package com.accounting.mapper;
import com.accounting.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}

View File

@ -0,0 +1,150 @@
package com.accounting.service;
import com.accounting.dto.AccountRequest;
import com.accounting.dto.AccountResponse;
import com.accounting.entity.Account;
import com.accounting.entity.Bill;
import com.accounting.mapper.AccountMapper;
import com.accounting.mapper.BillMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private BillMapper billMapper;
/**
* 获取或创建用户账户如果不存在则创建默认账户
*/
@Transactional
public Account getOrCreateAccount(Long userId) {
Account account = accountMapper.selectOne(
new LambdaQueryWrapper<Account>()
.eq(Account::getUserId, userId)
);
if (account == null) {
account = new Account();
account.setUserId(userId);
account.setName("默认账户");
account.setInitialBalance(BigDecimal.ZERO);
accountMapper.insert(account);
}
return account;
}
/**
* 获取用户账户
*/
public Account getAccount(Long userId) {
return accountMapper.selectOne(
new LambdaQueryWrapper<Account>()
.eq(Account::getUserId, userId)
);
}
/**
* 更新账户信息主要是初始余额
*/
@Transactional
public AccountResponse updateAccount(Long userId, AccountRequest request) {
Account account = getOrCreateAccount(userId);
if (request.getName() != null) {
account.setName(request.getName());
}
if (request.getInitialBalance() != null) {
account.setInitialBalance(request.getInitialBalance());
}
accountMapper.updateById(account);
return getAccountBalance(userId);
}
/**
* 计算账户余额初始余额 + 收入总额 - 支出总额
*/
public BigDecimal calculateBalance(Long accountId) {
Account account = accountMapper.selectById(accountId);
if (account == null) {
return BigDecimal.ZERO;
}
BigDecimal initialBalance = account.getInitialBalance() != null ? account.getInitialBalance() : BigDecimal.ZERO;
// 查询该账户的所有账单
List<Bill> bills = billMapper.selectList(
new LambdaQueryWrapper<Bill>()
.eq(Bill::getAccountId, accountId)
);
BigDecimal totalIncome = BigDecimal.ZERO;
BigDecimal totalExpense = BigDecimal.ZERO;
for (Bill bill : bills) {
if (bill.getType() != null && bill.getAmount() != null) {
if (bill.getType() == 2) { // 收入
totalIncome = totalIncome.add(bill.getAmount());
} else if (bill.getType() == 1) { // 支出
totalExpense = totalExpense.add(bill.getAmount().abs());
}
}
}
return initialBalance.add(totalIncome).subtract(totalExpense);
}
/**
* 获取账户余额信息包含余额总收入总支出
*/
public AccountResponse getAccountBalance(Long userId) {
Account account = getOrCreateAccount(userId);
AccountResponse response = new AccountResponse();
BeanUtils.copyProperties(account, response);
BigDecimal balance = calculateBalance(account.getId());
response.setBalance(balance);
// 计算总收入
List<Bill> incomeBills = billMapper.selectList(
new LambdaQueryWrapper<Bill>()
.eq(Bill::getAccountId, account.getId())
.eq(Bill::getType, 2) // 收入
);
BigDecimal totalIncome = incomeBills.stream()
.map(Bill::getAmount)
.filter(amount -> amount != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
response.setTotalIncome(totalIncome);
// 计算总支出
List<Bill> expenseBills = billMapper.selectList(
new LambdaQueryWrapper<Bill>()
.eq(Bill::getAccountId, account.getId())
.eq(Bill::getType, 1) // 支出
);
BigDecimal totalExpense = expenseBills.stream()
.map(Bill::getAmount)
.map(BigDecimal::abs)
.filter(amount -> amount != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
response.setTotalExpense(totalExpense);
return response;
}
}

View File

@ -2,6 +2,7 @@ package com.accounting.service;
import com.accounting.dto.BillRequest; import com.accounting.dto.BillRequest;
import com.accounting.dto.BillResponse; import com.accounting.dto.BillResponse;
import com.accounting.entity.Account;
import com.accounting.entity.Bill; import com.accounting.entity.Bill;
import com.accounting.entity.Category; import com.accounting.entity.Category;
import com.accounting.mapper.BillMapper; import com.accounting.mapper.BillMapper;
@ -25,6 +26,9 @@ public class BillService {
@Autowired @Autowired
private CategoryMapper categoryMapper; private CategoryMapper categoryMapper;
@Autowired
private AccountService accountService;
@Transactional @Transactional
public BillResponse createBill(BillRequest request, Long userId) { public BillResponse createBill(BillRequest request, Long userId) {
// 验证分类是否存在 // 验证分类是否存在
@ -33,9 +37,13 @@ public class BillService {
throw new RuntimeException("分类不存在"); throw new RuntimeException("分类不存在");
} }
// 获取或创建账户
Account account = accountService.getOrCreateAccount(userId);
// 创建账单 // 创建账单
Bill bill = new Bill(); Bill bill = new Bill();
bill.setUserId(userId); bill.setUserId(userId);
bill.setAccountId(account.getId()); // 自动关联账户
bill.setCategoryId(request.getCategoryId()); bill.setCategoryId(request.getCategoryId());
bill.setAmount(request.getAmount()); bill.setAmount(request.getAmount());
bill.setDescription(request.getDescription()); bill.setDescription(request.getDescription());
@ -67,6 +75,12 @@ public class BillService {
throw new RuntimeException("分类不存在"); throw new RuntimeException("分类不存在");
} }
// 如果账单没有关联账户则自动关联
if (bill.getAccountId() == null) {
Account account = accountService.getOrCreateAccount(userId);
bill.setAccountId(account.getId());
}
// 更新账单 // 更新账单
bill.setCategoryId(request.getCategoryId()); bill.setCategoryId(request.getCategoryId());
bill.setAmount(request.getAmount()); bill.setAmount(request.getAmount());

File diff suppressed because one or more lines are too long

View File

@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS `category` (
CREATE TABLE IF NOT EXISTS `bill` ( CREATE TABLE IF NOT EXISTS `bill` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '账单ID', `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '账单ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID', `user_id` BIGINT NOT NULL COMMENT '用户ID',
`account_id` BIGINT COMMENT '账户ID',
`category_id` BIGINT NOT NULL COMMENT '分类ID', `category_id` BIGINT NOT NULL COMMENT '分类ID',
`amount` DECIMAL(10,2) NOT NULL COMMENT '金额', `amount` DECIMAL(10,2) NOT NULL COMMENT '金额',
`description` VARCHAR(255) COMMENT '描述', `description` VARCHAR(255) COMMENT '描述',
@ -44,12 +45,28 @@ CREATE TABLE IF NOT EXISTS `bill` (
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`), INDEX `idx_user_id` (`user_id`),
INDEX `idx_account_id` (`account_id`),
INDEX `idx_category_id` (`category_id`), INDEX `idx_category_id` (`category_id`),
INDEX `idx_bill_date` (`bill_date`), INDEX `idx_bill_date` (`bill_date`),
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE RESTRICT,
FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE RESTRICT FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账单表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账单表';
-- 账户表
CREATE TABLE IF NOT EXISTS `account` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '账户ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`name` VARCHAR(50) NOT NULL DEFAULT '默认账户' COMMENT '账户名称',
`initial_balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '初始余额',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`),
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
UNIQUE KEY `uk_user_account` (`user_id`) COMMENT '每个用户只有一个账户'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='账户表';
-- OCR记录表 -- OCR记录表
CREATE TABLE IF NOT EXISTS `ocr_record` ( CREATE TABLE IF NOT EXISTS `ocr_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'OCR记录ID', `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'OCR记录ID',