Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ca71b6982 | |||
| 3df3aea533 |
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.accounting.controller;
|
||||||
|
|
||||||
|
import com.accounting.dto.BudgetRequest;
|
||||||
|
import com.accounting.dto.BudgetResponse;
|
||||||
|
import com.accounting.dto.BudgetSettlementResponse;
|
||||||
|
import com.accounting.entity.User;
|
||||||
|
import com.accounting.mapper.UserMapper;
|
||||||
|
import com.accounting.service.BudgetService;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Tag(name = "预算管理", description = "预算管理接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/budgets")
|
||||||
|
public class BudgetController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BudgetService budgetService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
|
@Operation(summary = "获取当前月份的预算信息")
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<BudgetResponse> getBudget(Authentication authentication) {
|
||||||
|
Long userId = getUserId(authentication);
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
BudgetResponse response = budgetService.getBudgetWithStatistics(userId, today.getYear(), today.getMonthValue());
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取上月预算结算信息")
|
||||||
|
@GetMapping("/settlement")
|
||||||
|
public ResponseEntity<BudgetSettlementResponse> getBudgetSettlement(Authentication authentication) {
|
||||||
|
Long userId = getUserId(authentication);
|
||||||
|
BudgetSettlementResponse response = budgetService.getLastMonthSettlement(userId);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设置/更新预算")
|
||||||
|
@PutMapping
|
||||||
|
public ResponseEntity<BudgetResponse> setBudget(
|
||||||
|
@RequestBody BudgetRequest request,
|
||||||
|
Authentication authentication) {
|
||||||
|
Long userId = getUserId(authentication);
|
||||||
|
BudgetResponse response = budgetService.setBudget(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
13
src/main/java/com/accounting/dto/AccountRequest.java
Normal file
13
src/main/java/com/accounting/dto/AccountRequest.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
26
src/main/java/com/accounting/dto/AccountResponse.java
Normal file
26
src/main/java/com/accounting/dto/AccountResponse.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
15
src/main/java/com/accounting/dto/BudgetRequest.java
Normal file
15
src/main/java/com/accounting/dto/BudgetRequest.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package com.accounting.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BudgetRequest {
|
||||||
|
private Integer year;
|
||||||
|
|
||||||
|
private Integer month;
|
||||||
|
|
||||||
|
private BigDecimal amount;
|
||||||
|
}
|
||||||
|
|
||||||
23
src/main/java/com/accounting/dto/BudgetResponse.java
Normal file
23
src/main/java/com/accounting/dto/BudgetResponse.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package com.accounting.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BudgetResponse {
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Integer year;
|
||||||
|
|
||||||
|
private Integer month;
|
||||||
|
|
||||||
|
private BigDecimal amount; // 预算金额
|
||||||
|
|
||||||
|
private BigDecimal usedAmount; // 已用金额(本月支出)
|
||||||
|
|
||||||
|
private BigDecimal remainingAmount; // 剩余预算
|
||||||
|
|
||||||
|
private BigDecimal remainingDaily; // 剩余日均
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.accounting.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BudgetSettlementResponse {
|
||||||
|
private Integer year; // 年份
|
||||||
|
|
||||||
|
private Integer month; // 月份
|
||||||
|
|
||||||
|
private BigDecimal budgetAmount; // 预算金额
|
||||||
|
|
||||||
|
private BigDecimal actualExpense; // 实际支出
|
||||||
|
|
||||||
|
private Boolean isOverBudget; // 是否超支
|
||||||
|
|
||||||
|
private BigDecimal overAmount; // 超支金额(如果超支)
|
||||||
|
|
||||||
|
private BigDecimal completionRate; // 完成率(实际支出/预算金额,如果预算为0则为null)
|
||||||
|
}
|
||||||
|
|
||||||
27
src/main/java/com/accounting/entity/Account.java
Normal file
27
src/main/java/com/accounting/entity/Account.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@ -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;
|
||||||
|
|||||||
29
src/main/java/com/accounting/entity/Budget.java
Normal file
29
src/main/java/com/accounting/entity/Budget.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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("budget")
|
||||||
|
public class Budget {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private Integer year;
|
||||||
|
|
||||||
|
private Integer month;
|
||||||
|
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
10
src/main/java/com/accounting/mapper/AccountMapper.java
Normal file
10
src/main/java/com/accounting/mapper/AccountMapper.java
Normal 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> {
|
||||||
|
}
|
||||||
|
|
||||||
10
src/main/java/com/accounting/mapper/BudgetMapper.java
Normal file
10
src/main/java/com/accounting/mapper/BudgetMapper.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.accounting.mapper;
|
||||||
|
|
||||||
|
import com.accounting.entity.Budget;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface BudgetMapper extends BaseMapper<Budget> {
|
||||||
|
}
|
||||||
|
|
||||||
150
src/main/java/com/accounting/service/AccountService.java
Normal file
150
src/main/java/com/accounting/service/AccountService.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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());
|
||||||
|
|||||||
191
src/main/java/com/accounting/service/BudgetService.java
Normal file
191
src/main/java/com/accounting/service/BudgetService.java
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package com.accounting.service;
|
||||||
|
|
||||||
|
import com.accounting.dto.BudgetRequest;
|
||||||
|
import com.accounting.dto.BudgetResponse;
|
||||||
|
import com.accounting.dto.BudgetSettlementResponse;
|
||||||
|
import com.accounting.dto.StatisticsResponse;
|
||||||
|
import com.accounting.entity.Budget;
|
||||||
|
import com.accounting.mapper.BudgetMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class BudgetService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BudgetMapper budgetMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StatisticsService statisticsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建预算(如果不存在则创建默认0)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Budget getOrCreateBudget(Long userId, int year, int month) {
|
||||||
|
Budget budget = budgetMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Budget>()
|
||||||
|
.eq(Budget::getUserId, userId)
|
||||||
|
.eq(Budget::getYear, year)
|
||||||
|
.eq(Budget::getMonth, month)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (budget == null) {
|
||||||
|
budget = new Budget();
|
||||||
|
budget.setUserId(userId);
|
||||||
|
budget.setYear(year);
|
||||||
|
budget.setMonth(month);
|
||||||
|
budget.setAmount(BigDecimal.ZERO);
|
||||||
|
budgetMapper.insert(budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
return budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预算
|
||||||
|
*/
|
||||||
|
public Budget getBudget(Long userId, int year, int month) {
|
||||||
|
return budgetMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Budget>()
|
||||||
|
.eq(Budget::getUserId, userId)
|
||||||
|
.eq(Budget::getYear, year)
|
||||||
|
.eq(Budget::getMonth, month)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置/更新预算
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public BudgetResponse setBudget(Long userId, BudgetRequest request) {
|
||||||
|
int year = request.getYear();
|
||||||
|
int month = request.getMonth();
|
||||||
|
BigDecimal amount = request.getAmount();
|
||||||
|
|
||||||
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
throw new RuntimeException("预算金额必须大于等于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Budget budget = getOrCreateBudget(userId, year, month);
|
||||||
|
budget.setAmount(amount);
|
||||||
|
budgetMapper.updateById(budget);
|
||||||
|
|
||||||
|
return getBudgetWithStatistics(userId, year, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预算及统计信息
|
||||||
|
*/
|
||||||
|
public BudgetResponse getBudgetWithStatistics(Long userId, int year, int month) {
|
||||||
|
Budget budget = getOrCreateBudget(userId, year, month);
|
||||||
|
|
||||||
|
BudgetResponse response = new BudgetResponse();
|
||||||
|
response.setId(budget.getId());
|
||||||
|
response.setYear(budget.getYear());
|
||||||
|
response.setMonth(budget.getMonth());
|
||||||
|
response.setAmount(budget.getAmount());
|
||||||
|
|
||||||
|
// 获取本月支出统计
|
||||||
|
StatisticsResponse stats = statisticsService.getMonthlyStatistics(userId, year, month);
|
||||||
|
BigDecimal usedAmount = stats.getTotalExpense() != null ? stats.getTotalExpense() : BigDecimal.ZERO;
|
||||||
|
response.setUsedAmount(usedAmount);
|
||||||
|
|
||||||
|
// 计算剩余预算
|
||||||
|
BigDecimal remainingAmount = budget.getAmount().subtract(usedAmount);
|
||||||
|
response.setRemainingAmount(remainingAmount);
|
||||||
|
|
||||||
|
// 计算剩余日均
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDate monthStart = LocalDate.of(year, month, 1);
|
||||||
|
LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth());
|
||||||
|
|
||||||
|
int remainingDays = 0;
|
||||||
|
if (year == today.getYear() && month == today.getMonthValue()) {
|
||||||
|
// 当前月份
|
||||||
|
remainingDays = monthEnd.getDayOfMonth() - today.getDayOfMonth() + 1;
|
||||||
|
if (remainingDays < 0) {
|
||||||
|
remainingDays = 0;
|
||||||
|
}
|
||||||
|
} else if (year < today.getYear() || (year == today.getYear() && month < today.getMonthValue())) {
|
||||||
|
// 过去的月份
|
||||||
|
remainingDays = 0;
|
||||||
|
} else {
|
||||||
|
// 未来的月份
|
||||||
|
remainingDays = monthEnd.getDayOfMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingDays > 0) {
|
||||||
|
BigDecimal remainingDaily = remainingAmount.divide(BigDecimal.valueOf(remainingDays), 2, RoundingMode.HALF_UP);
|
||||||
|
response.setRemainingDaily(remainingDaily);
|
||||||
|
} else {
|
||||||
|
response.setRemainingDaily(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上月预算结算信息
|
||||||
|
*/
|
||||||
|
public BudgetSettlementResponse getLastMonthSettlement(Long userId) {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDate lastMonth = today.minusMonths(1);
|
||||||
|
int year = lastMonth.getYear();
|
||||||
|
int month = lastMonth.getMonthValue();
|
||||||
|
|
||||||
|
Budget budget = getBudget(userId, year, month);
|
||||||
|
BigDecimal budgetAmount = budget != null && budget.getAmount() != null ? budget.getAmount() : BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// 获取上月支出统计
|
||||||
|
StatisticsResponse stats = statisticsService.getMonthlyStatistics(userId, year, month);
|
||||||
|
BigDecimal actualExpense = stats.getTotalExpense() != null ? stats.getTotalExpense() : BigDecimal.ZERO;
|
||||||
|
|
||||||
|
BudgetSettlementResponse response = new BudgetSettlementResponse();
|
||||||
|
response.setYear(year);
|
||||||
|
response.setMonth(month);
|
||||||
|
response.setBudgetAmount(budgetAmount);
|
||||||
|
response.setActualExpense(actualExpense);
|
||||||
|
|
||||||
|
// 判断是否超支
|
||||||
|
boolean isOverBudget = actualExpense.compareTo(budgetAmount) > 0;
|
||||||
|
response.setIsOverBudget(isOverBudget);
|
||||||
|
|
||||||
|
if (isOverBudget) {
|
||||||
|
BigDecimal overAmount = actualExpense.subtract(budgetAmount);
|
||||||
|
response.setOverAmount(overAmount);
|
||||||
|
} else {
|
||||||
|
response.setOverAmount(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算完成率
|
||||||
|
if (budgetAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
BigDecimal completionRate = actualExpense.divide(budgetAmount, 4, RoundingMode.HALF_UP)
|
||||||
|
.multiply(BigDecimal.valueOf(100));
|
||||||
|
response.setCompletionRate(completionRate);
|
||||||
|
} else {
|
||||||
|
response.setCompletionRate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并创建本月预算(如果不存在则创建默认0)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void checkAndCreateCurrentMonthBudget(Long userId) {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
int year = today.getYear();
|
||||||
|
int month = today.getMonthValue();
|
||||||
|
|
||||||
|
getOrCreateBudget(userId, year, month);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
@ -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,44 @@ 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='账户表';
|
||||||
|
|
||||||
|
-- 预算表
|
||||||
|
CREATE TABLE IF NOT EXISTS `budget` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '预算ID',
|
||||||
|
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
`year` INT NOT NULL COMMENT '年份',
|
||||||
|
`month` INT NOT NULL COMMENT '月份(1-12)',
|
||||||
|
`amount` 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`),
|
||||||
|
INDEX `idx_year_month` (`year`, `month`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY `uk_user_year_month` (`user_id`, `year`, `month`) 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',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user