From 2ca71b6982bd0f718d845aa6829e72da5a70e209 Mon Sep 17 00:00:00 2001 From: ni ziyi <310925901@qq.com> Date: Sat, 13 Dec 2025 22:33:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E9=A2=84=E7=AE=97=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=96=B0=E5=A2=9E)=201=E3=80=81=E5=AE=9E=E7=8E=B0=E6=9C=88?= =?UTF-8?q?=E5=BA=A6=E9=A2=84=E7=AE=97=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=AF=8F?= =?UTF-8?q?=E4=B8=AA=E7=94=A8=E6=88=B7=E6=AF=8F=E4=B8=AA=E6=9C=88=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E8=AE=BE=E7=BD=AE=E4=B8=80=E4=B8=AA=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E3=80=82=E5=9C=A8=E9=A6=96=E9=A1=B5=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E9=A2=84=E7=AE=97=E7=9B=B8=E5=85=B3=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=8F=90=E4=BE=9B=E9=A2=84=E7=AE=97=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=8A=9F=E8=83=BD=E3=80=82=E6=AF=8F=E6=9C=88=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E6=89=93=E5=BC=80=E5=BA=94=E7=94=A8=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=BB=93=E7=AE=97=E4=B8=8A=E6=9C=88=E9=A2=84?= =?UTF-8?q?=E7=AE=97=E5=B9=B6=E5=BC=B9=E7=AA=97=E9=80=9A=E7=9F=A5=E7=94=A8?= =?UTF-8?q?=E6=88=B7=EF=BC=8C=E5=90=8C=E6=97=B6=E5=BC=80=E5=90=AF=E6=9C=AC?= =?UTF-8?q?=E6=9C=88=E9=A2=84=E7=AE=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BudgetController.java | 67 ++++++ .../com/accounting/dto/BudgetRequest.java | 15 ++ .../com/accounting/dto/BudgetResponse.java | 23 +++ .../dto/BudgetSettlementResponse.java | 23 +++ .../java/com/accounting/entity/Budget.java | 29 +++ .../com/accounting/mapper/BudgetMapper.java | 10 + .../com/accounting/service/BudgetService.java | 191 ++++++++++++++++++ src/main/resources/db/schema.sql | 16 ++ 8 files changed, 374 insertions(+) create mode 100644 src/main/java/com/accounting/controller/BudgetController.java create mode 100644 src/main/java/com/accounting/dto/BudgetRequest.java create mode 100644 src/main/java/com/accounting/dto/BudgetResponse.java create mode 100644 src/main/java/com/accounting/dto/BudgetSettlementResponse.java create mode 100644 src/main/java/com/accounting/entity/Budget.java create mode 100644 src/main/java/com/accounting/mapper/BudgetMapper.java create mode 100644 src/main/java/com/accounting/service/BudgetService.java diff --git a/src/main/java/com/accounting/controller/BudgetController.java b/src/main/java/com/accounting/controller/BudgetController.java new file mode 100644 index 0000000..0f96580 --- /dev/null +++ b/src/main/java/com/accounting/controller/BudgetController.java @@ -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 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 getBudgetSettlement(Authentication authentication) { + Long userId = getUserId(authentication); + BudgetSettlementResponse response = budgetService.getLastMonthSettlement(userId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "设置/更新预算") + @PutMapping + public ResponseEntity 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() + .eq(User::getUsername, username) + ); + return user != null ? user.getId() : null; + } +} + diff --git a/src/main/java/com/accounting/dto/BudgetRequest.java b/src/main/java/com/accounting/dto/BudgetRequest.java new file mode 100644 index 0000000..6ef59b9 --- /dev/null +++ b/src/main/java/com/accounting/dto/BudgetRequest.java @@ -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; +} + diff --git a/src/main/java/com/accounting/dto/BudgetResponse.java b/src/main/java/com/accounting/dto/BudgetResponse.java new file mode 100644 index 0000000..2612e25 --- /dev/null +++ b/src/main/java/com/accounting/dto/BudgetResponse.java @@ -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; // 剩余日均 +} + diff --git a/src/main/java/com/accounting/dto/BudgetSettlementResponse.java b/src/main/java/com/accounting/dto/BudgetSettlementResponse.java new file mode 100644 index 0000000..47215e0 --- /dev/null +++ b/src/main/java/com/accounting/dto/BudgetSettlementResponse.java @@ -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) +} + diff --git a/src/main/java/com/accounting/entity/Budget.java b/src/main/java/com/accounting/entity/Budget.java new file mode 100644 index 0000000..e004322 --- /dev/null +++ b/src/main/java/com/accounting/entity/Budget.java @@ -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; +} + diff --git a/src/main/java/com/accounting/mapper/BudgetMapper.java b/src/main/java/com/accounting/mapper/BudgetMapper.java new file mode 100644 index 0000000..bd4cbdc --- /dev/null +++ b/src/main/java/com/accounting/mapper/BudgetMapper.java @@ -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 { +} + diff --git a/src/main/java/com/accounting/service/BudgetService.java b/src/main/java/com/accounting/service/BudgetService.java new file mode 100644 index 0000000..f89e0d6 --- /dev/null +++ b/src/main/java/com/accounting/service/BudgetService.java @@ -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() + .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() + .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); + } +} + diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index 2ab340f..3140a6e 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -67,6 +67,22 @@ CREATE TABLE IF NOT EXISTS `account` ( 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记录表 CREATE TABLE IF NOT EXISTS `ocr_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'OCR记录ID',