refactor: 统一用户上下文、导入顺序与日志规范

- 控制器移除 HttpServletRequest/HttpServletResponse 参数,新增 util/UserContext 统一获取当前用户 ID 与响应对象,导出接口改用 UserContext.getResponse()
- 引入 Spotless 统一 Java 导入顺序(java/javax → 第三方库 → cn.rosercode),并全量重排
- 日志统一改用 Lombok @Slf4j,替换手写 LoggerFactory
- 补充关键业务日志:登录/注册、交易增删改与批量操作、账单解析导入、DeepSeek 调用耗时与失败、AI 配置与连接测试、通知发送、分类/标签/规则变更
- 全局异常处理补充日志,未捕获异常由 e.printStackTrace() 改为 log.error 记录堆栈
- README 补充功能特性、技术栈与目录结构说明
This commit is contained in:
2026-09-19 14:51:46 +08:00
parent 196afb682d
commit dedf30c74d
147 changed files with 1046 additions and 872 deletions
+74 -14
View File
@@ -1,15 +1,66 @@
# 收支管理系统 # 收支管理系统
个人收支管理系统,支持收入/支出记录管理、分类管理、月度统计与图表展示。 个人收支管理系统,支持收入/支出记录管理、分类与标签管理、多来源账单导入、自定义归类规则、AI 智能记账、月度统计与图表展示,并提供定时账单导入提醒与系统日志查看。
## 功能特性
### 收支记录
- 收支记录的增删改查、分页与多条件筛选
- 批量设置分类、批量设置标签、批量删除
- 单条记录的「计入统计」开关与「备注隐藏」开关
- 按当前筛选条件导出 Excel
### 分类与标签
- 分类、标签的增删改查,新注册用户自动获得一套默认数据
- 分类图标支持 AI 推荐 Emoji,标签颜色支持 AI 推荐十六进制色值
### 统计与图表
- 月度收支统计、分类占比、收支趋势、分组统计与概览汇总
- 基于 ECharts 的可视化图表展示
### 账单导入
- 支持支付宝(CSV)、微信(xlsx)、美团、交通银行、招商银行(PDF)账单解析
- 支持 zip 压缩包(含 ZipCrypto / AES 加密压缩包)上传
- 「解析预览 → 确认导入」两步流程,按交易号去重
### 自定义规则
- 基于关键词(匹配备注 / 交易对方 / 商品名称)、收支类型、金额区间组合的条件规则
- 命中后自动应用分类与标签,支持优先级排序与启用 / 停用
- 账单导入预览阶段展示命中的规则与建议分类 / 标签
### AI 能力(DeepSeek)
- 交易记录分类与标签的批量 / 单条智能建议,支持一键采纳
- 自然语言查询解析与自然语言记账解析
- AI 生成分类、标签,以及 Emoji / 颜色推荐
- AI 调用用量统计与明细查看
### 定时提醒与通知
- 按每天 / 每周 / 每月周期提醒导入新账单(默认每月 1 日)
- 多渠道推送:邮箱 SMTP(如 QQ 邮箱)、Server 酱(推送到个人微信)
- 渠道管理、测试发送与发送日志查看
### 系统日志
- 接口访问日志、登录日志的持久化与分页查询
- 后端运行日志在线查看,支持基于 SSE 的实时推送与筛选
### 账号与多语言
- JWT 登录认证,注册功能可开关
- 运行时数据库配置与切换(SQLite / MySQL),无需启动前准备
- 界面支持中文、英文、日文三种语言
## 技术栈 ## 技术栈
| 层级 | 技术 | | 层级 | 技术 |
|---|---| |---|---|
| 后端 | Spring Boot 3.3.6 + Java 21 + MyBatis-Plus + MySQL / SQLite | | 后端 | Spring Boot 3.3.6 + Java 21 + MyBatis-Plus 3.5.12 + MySQL / SQLite |
| 前端 | Vue 3 + TypeScript + Vite + Naive UI + ECharts | | 前端 | Vue 3 + TypeScript + Vite + Naive UI + ECharts + Pinia |
| 构建 | Gradle 8.13 (后端) / npm (前端) | | 构建 | Gradle 8.13 (后端) / npm (前端) |
| 认证 | JWT | | 认证 | JWT |
| 对象映射 | MapStruct 1.6.3 |
| 账单解析 | EasyExcel 4.0.3 + PDFBox 3.0.3 + Zip4j 2.11.5 |
| AI 能力 | DeepSeek |
| 通知渠道 | 邮箱 SMTP / Server 酱 |
| 多语言 | 中文 / 英文 / 日文 (vue-i18n) |
| API 文档 | Knife4j (OpenAPI 3) | | API 文档 | Knife4j (OpenAPI 3) |
| JDK 版本 | 21.0.12.1 (Temurin LTS) | | JDK 版本 | 21.0.12.1 (Temurin LTS) |
| Node.js 版本 | v24.0.0 | | Node.js 版本 | v24.0.0 |
@@ -78,23 +129,32 @@ java -jar build/libs/finance-app-0.0.1-SNAPSHOT.jar --server.port=18081
finance-app/ finance-app/
├── build.gradle ├── build.gradle
├── src/main/java/cn/rosercode/finance/ ├── src/main/java/cn/rosercode/finance/
│ ├── controller/ # 接口层 │ ├── client/ # 外部服务客户端(DeepSeek)
│ ├── service/ # 业务层
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ ├── dto/ # 请求对象
│ ├── vo/ # 响应对象
│ ├── config/ # 配置类
│ ├── common/ # 公共类 │ ├── common/ # 公共类
│ ├── interceptor/ # 拦截器 │ ├── config/ # 配置类(应用配置、动态数据源)
│ └── util/ # 工具类 │ ├── controller/ # 接口层
│ ├── converter/ # MapStruct 对象转换器
│ ├── dto/ # 请求对象
│ ├── entity/ # 实体类
│ ├── enums/ # 枚举
│ ├── exporter/ # 导出处理
│ ├── importer/ # 账单解析器(支付宝/微信/美团/银行)
│ ├── interceptor/ # 拦截器(JWT、访问日志)
│ ├── mapper/ # 数据访问层
│ ├── notification/ # 通知渠道发送器
│ ├── scheduler/ # 定时任务
│ ├── service/ # 业务层(含 impl)
│ ├── util/ # 工具类
│ └── vo/ # 响应对象
├── src/main/resources/ ├── src/main/resources/
│ ├── application.yml │ ├── application.yml
│ ├── mapper/ │ ├── logback-spring.xml
│ └── sql/init.sql │ ├── mapper/ # MyBatis XML
│ └── sql/ # 建表脚本(init.sql / schema-sqlite.sql 等)
└── frontend/ # Vue 3 前端 └── frontend/ # Vue 3 前端
└── src/ └── src/
├── api/ # API 请求封装 ├── api/ # API 请求封装
├── locales/ # 多语言文案
├── router/ # 路由配置 ├── router/ # 路由配置
├── store/ # 状态管理 ├── store/ # 状态管理
└── views/ # 页面组件 └── views/ # 页面组件
+11
View File
@@ -2,6 +2,7 @@ plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.3.6' id 'org.springframework.boot' version '3.3.6'
id 'io.spring.dependency-management' version '1.1.7' id 'io.spring.dependency-management' version '1.1.7'
id 'com.diffplug.spotless' version '8.10.2'
} }
group = 'cn.rosercode' group = 'cn.rosercode'
@@ -67,6 +68,16 @@ tasks.named('test') {
useJUnitPlatform() useJUnitPlatform()
} }
// ===== 代码格式规范 =====
// 统一 Java 导入顺序:java/javax -> 第三方库 -> 项目包,组间空行分隔
spotless {
java {
target 'src/main/java/**/*.java', 'src/test/java/**/*.java'
// '|' 表示同组导入不插入空行;'' 为未显式指定导入的兜底组(即所有第三方库)
importOrder('java|javax', '', 'cn.rosercode')
}
}
// ===== 前端构建与打包 ===== // ===== 前端构建与打包 =====
def frontendDir = file("${projectDir}/frontend") def frontendDir = file("${projectDir}/frontend")
def frontendDistDir = file("${frontendDir}/dist") def frontendDistDir = file("${frontendDir}/dist")
+12
View File
@@ -1 +1,13 @@
pluginManagement {
resolutionStrategy {
eachPlugin {
// Spotless 的插件标记只发布在 Gradle 插件门户,本地 Nexus 未代理;
// 这里改为直接按 Maven 坐标解析插件实现(Maven Central 有该构件)
if (requested.id.id == 'com.diffplug.spotless') {
useModule("com.diffplug.spotless:spotless-plugin-gradle:${requested.version}")
}
}
}
}
rootProject.name = 'finance-app' rootProject.name = 'finance-app'
@@ -1,11 +1,5 @@
package cn.rosercode.finance.client; package cn.rosercode.finance.client;
import cn.rosercode.finance.common.BusinessException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@@ -17,12 +11,21 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import cn.rosercode.finance.common.BusinessException;
/** /**
* DeepSeek 客户端,封装 Chat Completions 接口的 HTTP 调用与响应解析。 * DeepSeek 客户端,封装 Chat Completions 接口的 HTTP 调用与响应解析。
* *
* @date 2026-09-17 17:52:17 * @date 2026-09-17 17:52:17
*/ */
@Component @Component
@Slf4j
public class DeepSeekClient { public class DeepSeekClient {
/** 单次请求的读取超时时间 */ /** 单次请求的读取超时时间 */
@@ -63,6 +66,7 @@ public class DeepSeekClient {
// 兼容以斜杠结尾的 baseUrl // 兼容以斜杠结尾的 baseUrl
String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
String url = normalized + "/chat/completions"; String url = normalized + "/chat/completions";
long start = System.currentTimeMillis();
// 构造请求体,强制返回 JSON 对象 // 构造请求体,强制返回 JSON 对象
Map<String, Object> body = new LinkedHashMap<>(); Map<String, Object> body = new LinkedHashMap<>();
@@ -79,6 +83,7 @@ public class DeepSeekClient {
try { try {
requestBody = objectMapper.writeValueAsString(body); requestBody = objectMapper.writeValueAsString(body);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
log.error("构建 AI 请求体失败: model={}, error={}", model, e.getMessage());
throw new BusinessException("构建 AI 请求失败: " + e.getMessage()); throw new BusinessException("构建 AI 请求失败: " + e.getMessage());
} }
@@ -93,14 +98,18 @@ public class DeepSeekClient {
try { try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
} catch (IOException e) { } catch (IOException e) {
log.error("调用 DeepSeek 失败: model={}, error={}", model, e.getMessage());
throw new BusinessException("调用 DeepSeek 失败: " + e.getMessage()); throw new BusinessException("调用 DeepSeek 失败: " + e.getMessage());
} catch (InterruptedException e) { } catch (InterruptedException e) {
// 恢复中断标记,避免吞掉中断信号 // 恢复中断标记,避免吞掉中断信号
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
log.warn("调用 DeepSeek 被中断: model={}", model);
throw new BusinessException("调用 DeepSeek 被中断"); throw new BusinessException("调用 DeepSeek 被中断");
} }
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
log.warn("DeepSeek 返回异常状态: model={}, status={}, body={}",
model, response.statusCode(), truncate(response.body()));
throw new BusinessException("DeepSeek 返回异常(" + response.statusCode() + "): " + truncate(response.body())); throw new BusinessException("DeepSeek 返回异常(" + response.statusCode() + "): " + truncate(response.body()));
} }
@@ -119,6 +128,8 @@ public class DeepSeekClient {
result.setTotalTokens(usage.path("total_tokens").asInt(0)); result.setTotalTokens(usage.path("total_tokens").asInt(0));
// DeepSeek 用 prompt_cache_hit_tokens 表示缓存命中的输入 token // DeepSeek 用 prompt_cache_hit_tokens 表示缓存命中的输入 token
result.setCachedTokens(usage.path("prompt_cache_hit_tokens").asInt(0)); result.setCachedTokens(usage.path("prompt_cache_hit_tokens").asInt(0));
log.info("DeepSeek 调用完成: model={}, 耗时={}ms, tokens={}", model,
System.currentTimeMillis() - start, result.getTotalTokens());
return result; return result;
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new BusinessException("解析 DeepSeek 响应失败: " + e.getMessage()); throw new BusinessException("解析 DeepSeek 响应失败: " + e.getMessage());
@@ -1,9 +1,11 @@
package cn.rosercode.finance.common; package cn.rosercode.finance.common;
import cn.rosercode.finance.config.SpaRouteSupport; import java.io.IOException;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -11,13 +13,14 @@ import org.springframework.web.context.request.async.AsyncRequestNotUsableExcept
import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.io.IOException; import cn.rosercode.finance.config.SpaRouteSupport;
/** /**
* 全局异常处理器。 * 全局异常处理器。
* <p>统一捕获并处理控制器层抛出的各类异常,将其转换为标准化的 JSON 响应返回给前端。</p> * <p>统一捕获并处理控制器层抛出的各类异常,将其转换为标准化的 JSON 响应返回给前端。</p>
*/ */
@RestControllerAdvice @RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
/** /**
@@ -28,6 +31,7 @@ public class GlobalExceptionHandler {
*/ */
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) { public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常[{}]: {}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage()); return Result.error(e.getCode(), e.getMessage());
} }
@@ -53,6 +57,7 @@ public class GlobalExceptionHandler {
public Result<Void> handleValidationException(MethodArgumentNotValidException e) { public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
// 提取第一个校验错误信息 // 提取第一个校验错误信息
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
log.warn("参数校验失败: {}", message);
return Result.error(400, message); return Result.error(400, message);
} }
@@ -108,8 +113,8 @@ public class GlobalExceptionHandler {
if (notConfigured != null) { if (notConfigured != null) {
return Result.error(notConfigured.getCode(), notConfigured.getMessage()); return Result.error(notConfigured.getCode(), notConfigured.getMessage());
} }
// 打印异常堆栈信息,便于排查问题 // 记录异常堆栈,便于排查问题
e.printStackTrace(); log.error("服务器内部错误: {}", e.getMessage(), e);
return Result.error(500, "服务器内部错误: " + e.getMessage()); return Result.error(500, "服务器内部错误: " + e.getMessage());
} }
@@ -1,9 +1,9 @@
package cn.rosercode.finance.common; package cn.rosercode.finance.common;
import lombok.Data;
import java.util.List; import java.util.List;
import lombok.Data;
/** /**
* 分页查询结果封装类。 * 分页查询结果封装类。
* <p>用于封装分页查询的返回结果,包含数据列表、总记录数、当前页码和每页大小。</p> * <p>用于封装分页查询的返回结果,包含数据列表、总记录数、当前页码和每页大小。</p>
@@ -1,8 +1,8 @@
package cn.rosercode.finance.config; package cn.rosercode.finance.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -1,5 +1,9 @@
package cn.rosercode.finance.config; package cn.rosercode.finance.config;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.RequestDispatcher; import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@@ -9,10 +13,6 @@ import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
/** /**
* 前端单页应用路由回退控制器。 * 前端单页应用路由回退控制器。
* *
@@ -1,12 +1,13 @@
package cn.rosercode.finance.config; package cn.rosercode.finance.config;
import cn.rosercode.finance.interceptor.AccessLogInterceptor;
import cn.rosercode.finance.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.rosercode.finance.interceptor.AccessLogInterceptor;
import cn.rosercode.finance.interceptor.JwtInterceptor;
/** /**
* Web MVC 配置类。 * Web MVC 配置类。
* <p>配置拦截器、跨域策略等 Web 层相关设置。</p> * <p>配置拦截器、跨域策略等 Web 层相关设置。</p>
@@ -1,15 +1,14 @@
package cn.rosercode.finance.config.app; package cn.rosercode.finance.config.app;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/** /**
* 应用级运行时配置的本地文件存储。 * 应用级运行时配置的本地文件存储。
* *
@@ -19,10 +18,9 @@ import java.nio.file.Paths;
* @date 2026-09-18 12:09:22 * @date 2026-09-18 12:09:22
*/ */
@Component @Component
@Slf4j
public class AppConfigStore { public class AppConfigStore {
private static final Logger log = LoggerFactory.getLogger(AppConfigStore.class);
/** 配置目录 */ /** 配置目录 */
private static final String CONFIG_DIR = "config"; private static final String CONFIG_DIR = "config";
@@ -1,10 +1,11 @@
package cn.rosercode.finance.config.datasource; package cn.rosercode.finance.config.datasource;
import cn.rosercode.finance.service.DatabaseService;
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import cn.rosercode.finance.service.DatabaseService;
/** /**
* 数据库启动引导器。 * 数据库启动引导器。
* *
@@ -1,16 +1,15 @@
package cn.rosercode.finance.config.datasource; package cn.rosercode.finance.config.datasource;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Optional; import java.util.Optional;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/** /**
* 数据库配置的本地文件存储。 * 数据库配置的本地文件存储。
* *
@@ -20,10 +19,9 @@ import java.util.Optional;
* @date 2026-09-18 11:00:14 * @date 2026-09-18 11:00:14
*/ */
@Component @Component
@Slf4j
public class DatabaseConfigStore { public class DatabaseConfigStore {
private static final Logger log = LoggerFactory.getLogger(DatabaseConfigStore.class);
/** 配置文件相对工作目录的位置 */ /** 配置文件相对工作目录的位置 */
private static final String CONFIG_DIR = "config"; private static final String CONFIG_DIR = "config";
@@ -1,13 +1,12 @@
package cn.rosercode.finance.config.datasource; package cn.rosercode.finance.config.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/** /**
* 动态数据源,支持在运行时切换底层真实数据源。 * 动态数据源,支持在运行时切换底层真实数据源。
@@ -19,10 +18,9 @@ import java.util.concurrent.ConcurrentHashMap;
* *
* @date 2026-09-18 11:00:28 * @date 2026-09-18 11:00:28
*/ */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource { public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
/** 自行维护的目标数据源映射,用于运行时增量注册与切换 */ /** 自行维护的目标数据源映射,用于运行时增量注册与切换 */
private final Map<Object, Object> targetDataSources = new ConcurrentHashMap<>(); private final Map<Object, Object> targetDataSources = new ConcurrentHashMap<>();
@@ -1,13 +1,12 @@
package cn.rosercode.finance.config.datasource; package cn.rosercode.finance.config.datasource;
import org.slf4j.Logger; import javax.sql.DataSource;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/** /**
* 数据库表结构初始化器。 * 数据库表结构初始化器。
* *
@@ -17,10 +16,9 @@ import javax.sql.DataSource;
* @date 2026-09-18 11:00:46 * @date 2026-09-18 11:00:46
*/ */
@Component @Component
@Slf4j
public class SchemaInitializer { public class SchemaInitializer {
private static final Logger log = LoggerFactory.getLogger(SchemaInitializer.class);
/** MySQL 建表脚本 */ /** MySQL 建表脚本 */
private static final String MYSQL_SCRIPT = "sql/init.sql"; private static final String MYSQL_SCRIPT = "sql/init.sql";
@@ -1,12 +1,12 @@
package cn.rosercode.finance.config.datasource; package cn.rosercode.finance.config.datasource;
import cn.rosercode.finance.common.DatabaseNotConfiguredException;
import javax.sql.DataSource;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.sql.DataSource;
import cn.rosercode.finance.common.DatabaseNotConfiguredException;
/** /**
* 未配置数据库时使用的占位数据源。 * 未配置数据库时使用的占位数据源。
@@ -1,5 +1,18 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.Result; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.AiApplyRequest; import cn.rosercode.finance.dto.AiApplyRequest;
import cn.rosercode.finance.dto.AiCategoryRequest; import cn.rosercode.finance.dto.AiCategoryRequest;
@@ -18,6 +31,7 @@ import cn.rosercode.finance.service.AiRecordService;
import cn.rosercode.finance.service.AiSuggestionService; import cn.rosercode.finance.service.AiSuggestionService;
import cn.rosercode.finance.service.AiTagService; import cn.rosercode.finance.service.AiTagService;
import cn.rosercode.finance.service.AiUsageService; import cn.rosercode.finance.service.AiUsageService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.AiCategoryResultVO; import cn.rosercode.finance.vo.AiCategoryResultVO;
import cn.rosercode.finance.vo.AiConfigVO; import cn.rosercode.finance.vo.AiConfigVO;
import cn.rosercode.finance.vo.AiQueryResultVO; import cn.rosercode.finance.vo.AiQueryResultVO;
@@ -25,19 +39,6 @@ import cn.rosercode.finance.vo.AiRecordResultVO;
import cn.rosercode.finance.vo.AiSuggestionVO; import cn.rosercode.finance.vo.AiSuggestionVO;
import cn.rosercode.finance.vo.AiTagResultVO; import cn.rosercode.finance.vo.AiTagResultVO;
import cn.rosercode.finance.vo.AiUsageResultVO; import cn.rosercode.finance.vo.AiUsageResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* AI 控制器 * AI 控制器
@@ -77,27 +78,25 @@ public class AiController {
/** /**
* 获取当前用户的 AI 配置 * 获取当前用户的 AI 配置
* *
* @param request HTTP请求,从中获取用户ID
* @return AI 配置(API Key 脱敏) * @return AI 配置(API Key 脱敏)
*/ */
@GetMapping("/config") @GetMapping("/config")
@Operation(summary = "获取 AI 配置", description = "获取当前用户的 AI 智能建议配置,API Key 已脱敏") @Operation(summary = "获取 AI 配置", description = "获取当前用户的 AI 智能建议配置,API Key 已脱敏")
public Result<AiConfigVO> getConfig(HttpServletRequest request) { public Result<AiConfigVO> getConfig() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(aiConfigService.getConfig(userId)); return Result.success(aiConfigService.getConfig(userId));
} }
/** /**
* 保存当前用户的 AI 配置 * 保存当前用户的 AI 配置
* *
* @param request HTTP请求,从中获取用户ID
* @param configRequest 配置保存请求 * @param configRequest 配置保存请求
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/config") @PutMapping("/config")
@Operation(summary = "保存 AI 配置", description = "更新当前用户的 AI 配置,API Key 留空表示不修改") @Operation(summary = "保存 AI 配置", description = "更新当前用户的 AI 配置,API Key 留空表示不修改")
public Result<Void> saveConfig(HttpServletRequest request, @RequestBody AiConfigRequest configRequest) { public Result<Void> saveConfig(@RequestBody AiConfigRequest configRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
aiConfigService.saveConfig(userId, configRequest); aiConfigService.saveConfig(userId, configRequest);
return Result.success(); return Result.success();
} }
@@ -105,13 +104,12 @@ public class AiController {
/** /**
* 测试 DeepSeek 连通性 * 测试 DeepSeek 连通性
* *
* @param request HTTP请求,从中获取用户ID
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping("/config/test") @PostMapping("/config/test")
@Operation(summary = "测试连接", description = "使用当前配置向 DeepSeek 发送一条测试请求") @Operation(summary = "测试连接", description = "使用当前配置向 DeepSeek 发送一条测试请求")
public Result<Void> testConfig(HttpServletRequest request) { public Result<Void> testConfig() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
aiConfigService.testConnection(userId); aiConfigService.testConnection(userId);
return Result.success(); return Result.success();
} }
@@ -119,150 +117,130 @@ public class AiController {
/** /**
* 生成分类与标签建议 * 生成分类与标签建议
* *
* @param request HTTP请求,从中获取用户ID
* @param suggestRequest 建议请求,携带记录ID列表 * @param suggestRequest 建议请求,携带记录ID列表
* @return 逐条建议列表 * @return 逐条建议列表
*/ */
@PostMapping("/suggest") @PostMapping("/suggest")
@Operation(summary = "生成 AI 建议", description = "根据交易记录内容生成分类与标签建议") @Operation(summary = "生成 AI 建议", description = "根据交易记录内容生成分类与标签建议")
public Result<List<AiSuggestionVO>> suggest(HttpServletRequest request, public Result<List<AiSuggestionVO>> suggest(@RequestBody @Valid AiSuggestRequest suggestRequest) {
@RequestBody @Valid AiSuggestRequest suggestRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiSuggestionService.suggest(userId, suggestRequest.getIds(), suggestRequest.getScope())); return Result.success(aiSuggestionService.suggest(userId, suggestRequest.getIds(), suggestRequest.getScope()));
} }
/** /**
* 根据表单当前内容推荐分类与标签 * 根据表单当前内容推荐分类与标签
* *
* @param request HTTP请求,从中获取用户ID
* @param suggestOneRequest 建议请求,携带表单当前内容 * @param suggestOneRequest 建议请求,携带表单当前内容
* @return 单条建议结果 * @return 单条建议结果
*/ */
@PostMapping("/suggest-one") @PostMapping("/suggest-one")
@Operation(summary = "单条记录 AI 建议", description = "根据表单当前内容推荐分类与标签,供新增或编辑时参考") @Operation(summary = "单条记录 AI 建议", description = "根据表单当前内容推荐分类与标签,供新增或编辑时参考")
public Result<AiSuggestionVO> suggestOne(HttpServletRequest request, public Result<AiSuggestionVO> suggestOne(@RequestBody @Valid AiSuggestOneRequest suggestOneRequest) {
@RequestBody @Valid AiSuggestOneRequest suggestOneRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiSuggestionService.suggestOne(userId, suggestOneRequest)); return Result.success(aiSuggestionService.suggestOne(userId, suggestOneRequest));
} }
/** /**
* 一键应用 AI 建议 * 一键应用 AI 建议
* *
* @param request HTTP请求,从中获取用户ID
* @param applyRequest 应用请求,携带逐条建议 * @param applyRequest 应用请求,携带逐条建议
* @return 实际发生变更的记录数 * @return 实际发生变更的记录数
*/ */
@PostMapping("/apply") @PostMapping("/apply")
@Operation(summary = "应用 AI 建议", description = "覆盖分类并追加标签,一键写入建议") @Operation(summary = "应用 AI 建议", description = "覆盖分类并追加标签,一键写入建议")
public Result<Integer> apply(HttpServletRequest request, public Result<Integer> apply(@RequestBody @Valid AiApplyRequest applyRequest) {
@RequestBody @Valid AiApplyRequest applyRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiSuggestionService.apply(userId, applyRequest)); return Result.success(aiSuggestionService.apply(userId, applyRequest));
} }
/** /**
* 获取 AI 使用情况 * 获取 AI 使用情况
* *
* @param request HTTP请求,从中获取用户ID
* @param limit 最近明细条数,默认 20 * @param limit 最近明细条数,默认 20
* @return 累计汇总与最近调用明细 * @return 累计汇总与最近调用明细
*/ */
@GetMapping("/usage") @GetMapping("/usage")
@Operation(summary = "获取 AI 使用情况", description = "获取当前用户 AI 调用的累计汇总与最近调用明细") @Operation(summary = "获取 AI 使用情况", description = "获取当前用户 AI 调用的累计汇总与最近调用明细")
public Result<AiUsageResultVO> usage(HttpServletRequest request, public Result<AiUsageResultVO> usage(@RequestParam(defaultValue = "20") int limit) {
@RequestParam(defaultValue = "20") int limit) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiUsageService.getUsage(userId, limit)); return Result.success(aiUsageService.getUsage(userId, limit));
} }
/** /**
* 解析自然语言查询为筛选条件 * 解析自然语言查询为筛选条件
* *
* @param request HTTP请求,从中获取用户ID
* @param queryRequest 查询请求,携带自然语言语句 * @param queryRequest 查询请求,携带自然语言语句
* @return 结构化筛选条件 * @return 结构化筛选条件
*/ */
@PostMapping("/query") @PostMapping("/query")
@Operation(summary = "自然语言查询解析", description = "将自然语言查询解析为交易筛选条件,供用户确认后应用") @Operation(summary = "自然语言查询解析", description = "将自然语言查询解析为交易筛选条件,供用户确认后应用")
public Result<AiQueryResultVO> query(HttpServletRequest request, public Result<AiQueryResultVO> query(@RequestBody @Valid AiQueryRequest queryRequest) {
@RequestBody @Valid AiQueryRequest queryRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiQueryService.parse(userId, queryRequest.getQuery())); return Result.success(aiQueryService.parse(userId, queryRequest.getQuery()));
} }
/** /**
* 解析自然语言记账内容为交易记录 * 解析自然语言记账内容为交易记录
* *
* @param request HTTP请求,从中获取用户ID
* @param recordRequest 记账请求,携带自然语言语句 * @param recordRequest 记账请求,携带自然语言语句
* @return 结构化交易记录 * @return 结构化交易记录
*/ */
@PostMapping("/record") @PostMapping("/record")
@Operation(summary = "自然语言记账解析", description = "将自然语言记账内容解析为交易记录,供用户确认后保存") @Operation(summary = "自然语言记账解析", description = "将自然语言记账内容解析为交易记录,供用户确认后保存")
public Result<AiRecordResultVO> record(HttpServletRequest request, public Result<AiRecordResultVO> record(@RequestBody @Valid AiRecordRequest recordRequest) {
@RequestBody @Valid AiRecordRequest recordRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiRecordService.parse(userId, recordRequest.getText())); return Result.success(aiRecordService.parse(userId, recordRequest.getText()));
} }
/** /**
* 根据自然语言描述生成分类 * 根据自然语言描述生成分类
* *
* @param request HTTP请求,从中获取用户ID
* @param categoryRequest 生成请求,携带分类用途描述 * @param categoryRequest 生成请求,携带分类用途描述
* @return 分类建议 * @return 分类建议
*/ */
@PostMapping("/category") @PostMapping("/category")
@Operation(summary = "AI 生成分类", description = "根据自然语言描述生成分类名称、类型与图标") @Operation(summary = "AI 生成分类", description = "根据自然语言描述生成分类名称、类型与图标")
public Result<AiCategoryResultVO> generateCategory(HttpServletRequest request, public Result<AiCategoryResultVO> generateCategory(@RequestBody @Valid AiCategoryRequest categoryRequest) {
@RequestBody @Valid AiCategoryRequest categoryRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiCategoryService.parse(userId, categoryRequest.getText())); return Result.success(aiCategoryService.parse(userId, categoryRequest.getText()));
} }
/** /**
* 根据自然语言描述生成标签 * 根据自然语言描述生成标签
* *
* @param request HTTP请求,从中获取用户ID
* @param tagRequest 生成请求,携带标签用途描述 * @param tagRequest 生成请求,携带标签用途描述
* @return 标签建议 * @return 标签建议
*/ */
@PostMapping("/tag") @PostMapping("/tag")
@Operation(summary = "AI 生成标签", description = "根据自然语言描述生成标签名称与颜色") @Operation(summary = "AI 生成标签", description = "根据自然语言描述生成标签名称与颜色")
public Result<AiTagResultVO> generateTag(HttpServletRequest request, public Result<AiTagResultVO> generateTag(@RequestBody @Valid AiTagRequest tagRequest) {
@RequestBody @Valid AiTagRequest tagRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiTagService.parse(userId, tagRequest.getText())); return Result.success(aiTagService.parse(userId, tagRequest.getText()));
} }
/** /**
* 根据分类名称推荐 Emoji 图标 * 根据分类名称推荐 Emoji 图标
* *
* @param request HTTP请求,从中获取用户ID
* @param decorateRequest 建议请求,携带分类名称与收支类型 * @param decorateRequest 建议请求,携带分类名称与收支类型
* @return 推荐的 Emoji * @return 推荐的 Emoji
*/ */
@PostMapping("/emoji") @PostMapping("/emoji")
@Operation(summary = "AI 推荐分类图标", description = "根据分类名称与收支类型推荐一个 Emoji") @Operation(summary = "AI 推荐分类图标", description = "根据分类名称与收支类型推荐一个 Emoji")
public Result<String> suggestEmoji(HttpServletRequest request, public Result<String> suggestEmoji(@RequestBody @Valid AiDecorateRequest decorateRequest) {
@RequestBody @Valid AiDecorateRequest decorateRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiDecorationService.suggestEmoji(userId, decorateRequest.getName(), decorateRequest.getType())); return Result.success(aiDecorationService.suggestEmoji(userId, decorateRequest.getName(), decorateRequest.getType()));
} }
/** /**
* 根据标签名称推荐颜色 * 根据标签名称推荐颜色
* *
* @param request HTTP请求,从中获取用户ID
* @param decorateRequest 建议请求,携带标签名称 * @param decorateRequest 建议请求,携带标签名称
* @return 推荐的十六进制颜色 * @return 推荐的十六进制颜色
*/ */
@PostMapping("/color") @PostMapping("/color")
@Operation(summary = "AI 推荐标签颜色", description = "根据标签名称推荐一个十六进制颜色") @Operation(summary = "AI 推荐标签颜色", description = "根据标签名称推荐一个十六进制颜色")
public Result<String> suggestColor(HttpServletRequest request, public Result<String> suggestColor(@RequestBody @Valid AiDecorateRequest decorateRequest) {
@RequestBody @Valid AiDecorateRequest decorateRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(aiDecorationService.suggestColor(userId, decorateRequest.getName())); return Result.success(aiDecorationService.suggestColor(userId, decorateRequest.getName()));
} }
} }
@@ -1,10 +1,5 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.config.app.AppConfig;
import cn.rosercode.finance.config.app.AppConfigStore;
import cn.rosercode.finance.dto.LogRetentionRequest;
import cn.rosercode.finance.dto.NotificationUrlRequest;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -13,6 +8,12 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.config.app.AppConfig;
import cn.rosercode.finance.config.app.AppConfigStore;
import cn.rosercode.finance.dto.LogRetentionRequest;
import cn.rosercode.finance.dto.NotificationUrlRequest;
/** /**
* 应用配置控制器 * 应用配置控制器
* *
@@ -1,16 +1,10 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.BusinessException; import java.io.IOException;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.BillImportRequest;
import cn.rosercode.finance.enums.BillType;
import cn.rosercode.finance.service.BillImportService;
import cn.rosercode.finance.vo.BillImportResultVO;
import cn.rosercode.finance.vo.BillParseResultVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -19,7 +13,14 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.BillImportRequest;
import cn.rosercode.finance.enums.BillType;
import cn.rosercode.finance.service.BillImportService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.BillImportResultVO;
import cn.rosercode.finance.vo.BillParseResultVO;
/** /**
* 账单导入控制器 * 账单导入控制器
@@ -42,7 +43,6 @@ public class BillImportController {
/** /**
* 解析账单文件(不落库) * 解析账单文件(不落库)
* *
* @param request HTTP请求,从中获取用户ID
* @param file 账单文件(支持 csv/xlsx 或 zip 压缩包) * @param file 账单文件(支持 csv/xlsx 或 zip 压缩包)
* @param billType 账单类型,如 ALIPAY * @param billType 账单类型,如 ALIPAY
* @param password 压缩包解压密码,未加密时可不传 * @param password 压缩包解压密码,未加密时可不传
@@ -51,11 +51,10 @@ public class BillImportController {
*/ */
@PostMapping("/parse") @PostMapping("/parse")
@Operation(summary = "解析账单文件", description = "解析账单文件(支持 zip 压缩包)并返回预览数据,不写入数据库") @Operation(summary = "解析账单文件", description = "解析账单文件(支持 zip 压缩包)并返回预览数据,不写入数据库")
public Result<BillParseResultVO> parse(HttpServletRequest request, public Result<BillParseResultVO> parse(@Parameter(description = "账单文件") @RequestParam("file") MultipartFile file,
@Parameter(description = "账单文件") @RequestParam("file") MultipartFile file,
@Parameter(description = "账单类型,如 ALIPAY") @RequestParam("billType") String billType, @Parameter(description = "账单类型,如 ALIPAY") @RequestParam("billType") String billType,
@Parameter(description = "压缩包解压密码") @RequestParam(value = "password", required = false) String password) throws IOException { @Parameter(description = "压缩包解压密码") @RequestParam(value = "password", required = false) String password) throws IOException {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
if (file == null || file.isEmpty()) { if (file == null || file.isEmpty()) {
throw new BusinessException("请选择账单文件"); throw new BusinessException("请选择账单文件");
} }
@@ -65,15 +64,13 @@ public class BillImportController {
/** /**
* 确认导入用户勾选的账单行 * 确认导入用户勾选的账单行
* *
* @param request HTTP请求,从中获取用户ID * @param body 导入请求(默认分类与勾选行)
* @param body 导入请求(默认分类与勾选行)
* @return 导入结果统计 * @return 导入结果统计
*/ */
@PostMapping("/confirm") @PostMapping("/confirm")
@Operation(summary = "确认导入账单", description = "将用户勾选的账单行写入交易记录,按交易号去重") @Operation(summary = "确认导入账单", description = "将用户勾选的账单行写入交易记录,按交易号去重")
public Result<BillImportResultVO> confirm(HttpServletRequest request, public Result<BillImportResultVO> confirm(@RequestBody @Valid BillImportRequest body) {
@RequestBody @Valid BillImportRequest body) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(billImportService.confirm(userId, body)); return Result.success(billImportService.confirm(userId, body));
} }
@@ -1,13 +1,10 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result; import java.util.List;
import cn.rosercode.finance.dto.CategoryRequest;
import cn.rosercode.finance.service.CategoryService;
import cn.rosercode.finance.vo.CategoryVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -19,7 +16,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.CategoryRequest;
import cn.rosercode.finance.service.CategoryService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.CategoryVO;
/** /**
* 分类控制器 * 分类控制器
@@ -40,15 +41,13 @@ public class CategoryController {
/** /**
* 获取分类列表 * 获取分类列表
* *
* @param request HTTP请求,从中获取用户ID
* @param type 分类类型(可选),用于按类型筛选 * @param type 分类类型(可选),用于按类型筛选
* @return 分类列表 * @return 分类列表
*/ */
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "获取分类列表", description = "获取当前用户的全部分类,支持按类型筛选") @Operation(summary = "获取分类列表", description = "获取当前用户的全部分类,支持按类型筛选")
public Result<List<CategoryVO>> list(HttpServletRequest request, public Result<List<CategoryVO>> list(@Parameter(description = "分类类型,可选值:income/expense") @RequestParam(required = false) String type) {
@Parameter(description = "分类类型,可选值:income/expense") @RequestParam(required = false) String type) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
if (type != null && !type.isEmpty()) { if (type != null && !type.isEmpty()) {
return Result.success(categoryService.listByType(userId, type)); return Result.success(categoryService.listByType(userId, type));
} }
@@ -58,14 +57,13 @@ public class CategoryController {
/** /**
* 创建分类 * 创建分类
* *
* @param request HTTP请求,从中获取用户ID
* @param categoryRequest 分类请求参数 * @param categoryRequest 分类请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping @PostMapping
@Operation(summary = "创建分类", description = "为当前用户创建新的收支分类") @Operation(summary = "创建分类", description = "为当前用户创建新的收支分类")
public Result<Void> create(HttpServletRequest request, @RequestBody @Valid CategoryRequest categoryRequest) { public Result<Void> create(@RequestBody @Valid CategoryRequest categoryRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
categoryService.create(userId, categoryRequest); categoryService.create(userId, categoryRequest);
return Result.success(); return Result.success();
} }
@@ -73,17 +71,15 @@ public class CategoryController {
/** /**
* 更新分类 * 更新分类
* *
* @param request HTTP请求,从中获取用户ID
* @param id 分类ID * @param id 分类ID
* @param categoryRequest 分类请求参数 * @param categoryRequest 分类请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新分类", description = "根据分类ID更新分类信息") @Operation(summary = "更新分类", description = "根据分类ID更新分类信息")
public Result<Void> update(HttpServletRequest request, public Result<Void> update(@Parameter(description = "分类ID") @PathVariable Long id,
@Parameter(description = "分类ID") @PathVariable Long id,
@RequestBody @Valid CategoryRequest categoryRequest) { @RequestBody @Valid CategoryRequest categoryRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
categoryService.update(userId, id, categoryRequest); categoryService.update(userId, id, categoryRequest);
return Result.success(); return Result.success();
} }
@@ -91,15 +87,13 @@ public class CategoryController {
/** /**
* 删除分类 * 删除分类
* *
* @param request HTTP请求,从中获取用户ID
* @param id 分类ID * @param id 分类ID
* @return 操作结果 * @return 操作结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "删除分类", description = "根据分类ID删除指定分类") @Operation(summary = "删除分类", description = "根据分类ID删除指定分类")
public Result<Void> delete(HttpServletRequest request, public Result<Void> delete(@Parameter(description = "分类ID") @PathVariable Long id) {
@Parameter(description = "分类ID") @PathVariable Long id) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
categoryService.delete(userId, id); categoryService.delete(userId, id);
return Result.success(); return Result.success();
} }
@@ -1,15 +1,16 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.DatabaseConfigRequest;
import cn.rosercode.finance.service.DatabaseService;
import cn.rosercode.finance.vo.DatabaseStatusVO;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.DatabaseConfigRequest;
import cn.rosercode.finance.service.DatabaseService;
import cn.rosercode.finance.vo.DatabaseStatusVO;
/** /**
* 数据库配置控制器。 * 数据库配置控制器。
* *
@@ -1,5 +1,13 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import cn.rosercode.finance.common.PageResult; import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.common.Result; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.LogQuery; import cn.rosercode.finance.dto.LogQuery;
@@ -8,13 +16,6 @@ import cn.rosercode.finance.service.LogService;
import cn.rosercode.finance.vo.AppLogVO; import cn.rosercode.finance.vo.AppLogVO;
import cn.rosercode.finance.vo.HttpLogVO; import cn.rosercode.finance.vo.HttpLogVO;
import cn.rosercode.finance.vo.LoginLogVO; import cn.rosercode.finance.vo.LoginLogVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/** /**
* 系统日志控制器 * 系统日志控制器
@@ -1,17 +1,9 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result; import java.util.List;
import cn.rosercode.finance.dto.NotificationChannelRequest;
import cn.rosercode.finance.dto.NotificationConfigRequest;
import cn.rosercode.finance.service.NotificationChannelService;
import cn.rosercode.finance.service.NotificationConfigService;
import cn.rosercode.finance.service.NotificationService;
import cn.rosercode.finance.vo.NotificationChannelVO;
import cn.rosercode.finance.vo.NotificationConfigVO;
import cn.rosercode.finance.vo.NotificationLogVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -22,7 +14,16 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.NotificationChannelRequest;
import cn.rosercode.finance.dto.NotificationConfigRequest;
import cn.rosercode.finance.service.NotificationChannelService;
import cn.rosercode.finance.service.NotificationConfigService;
import cn.rosercode.finance.service.NotificationService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.NotificationChannelVO;
import cn.rosercode.finance.vo.NotificationConfigVO;
import cn.rosercode.finance.vo.NotificationLogVO;
/** /**
* 通知控制器 * 通知控制器
@@ -51,28 +52,25 @@ public class NotificationController {
/** /**
* 获取当前用户的通知配置。 * 获取当前用户的通知配置。
* *
* @param request HTTP请求,从中获取用户ID
* @return 通知配置 * @return 通知配置
*/ */
@GetMapping("/config") @GetMapping("/config")
@Operation(summary = "获取通知配置", description = "获取当前用户的账单导入提醒周期配置") @Operation(summary = "获取通知配置", description = "获取当前用户的账单导入提醒周期配置")
public Result<NotificationConfigVO> getConfig(HttpServletRequest request) { public Result<NotificationConfigVO> getConfig() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(notificationConfigService.getConfig(userId)); return Result.success(notificationConfigService.getConfig(userId));
} }
/** /**
* 保存当前用户的通知配置。 * 保存当前用户的通知配置。
* *
* @param request HTTP请求,从中获取用户ID
* @param configRequest 配置保存请求 * @param configRequest 配置保存请求
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/config") @PutMapping("/config")
@Operation(summary = "保存通知配置", description = "更新当前用户的提醒周期与发送时间") @Operation(summary = "保存通知配置", description = "更新当前用户的提醒周期与发送时间")
public Result<Void> saveConfig(HttpServletRequest request, public Result<Void> saveConfig(@RequestBody NotificationConfigRequest configRequest) {
@RequestBody NotificationConfigRequest configRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
notificationConfigService.saveConfig(userId, configRequest); notificationConfigService.saveConfig(userId, configRequest);
return Result.success(); return Result.success();
} }
@@ -80,44 +78,40 @@ public class NotificationController {
/** /**
* 查询当前用户的通知渠道。 * 查询当前用户的通知渠道。
* *
* @param request HTTP请求,从中获取用户ID
* @return 渠道列表 * @return 渠道列表
*/ */
@GetMapping("/channels") @GetMapping("/channels")
@Operation(summary = "查询通知渠道", description = "查询当前用户配置的所有推送渠道") @Operation(summary = "查询通知渠道", description = "查询当前用户配置的所有推送渠道")
public Result<List<NotificationChannelVO>> listChannels(HttpServletRequest request) { public Result<List<NotificationChannelVO>> listChannels() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(notificationChannelService.listChannels(userId)); return Result.success(notificationChannelService.listChannels(userId));
} }
/** /**
* 新增通知渠道。 * 新增通知渠道。
* *
* @param request HTTP请求,从中获取用户ID
* @param channelRequest 渠道保存请求 * @param channelRequest 渠道保存请求
* @return 新渠道ID * @return 新渠道ID
*/ */
@PostMapping("/channels") @PostMapping("/channels")
@Operation(summary = "新增通知渠道", description = "新增邮箱或 Server 酱推送渠道") @Operation(summary = "新增通知渠道", description = "新增邮箱或 Server 酱推送渠道")
public Result<Long> createChannel(HttpServletRequest request, public Result<Long> createChannel(@RequestBody NotificationChannelRequest channelRequest) {
@RequestBody NotificationChannelRequest channelRequest) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(notificationChannelService.createChannel(userId, channelRequest)); return Result.success(notificationChannelService.createChannel(userId, channelRequest));
} }
/** /**
* 修改通知渠道。 * 修改通知渠道。
* *
* @param request HTTP请求,从中获取用户ID
* @param id 渠道ID * @param id 渠道ID
* @param channelRequest 渠道保存请求 * @param channelRequest 渠道保存请求
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/channels/{id}") @PutMapping("/channels/{id}")
@Operation(summary = "修改通知渠道", description = "更新指定渠道的名称、开关与配置") @Operation(summary = "修改通知渠道", description = "更新指定渠道的名称、开关与配置")
public Result<Void> updateChannel(HttpServletRequest request, @PathVariable Long id, public Result<Void> updateChannel(@PathVariable Long id,
@RequestBody NotificationChannelRequest channelRequest) { @RequestBody NotificationChannelRequest channelRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
notificationChannelService.updateChannel(userId, id, channelRequest); notificationChannelService.updateChannel(userId, id, channelRequest);
return Result.success(); return Result.success();
} }
@@ -125,14 +119,13 @@ public class NotificationController {
/** /**
* 删除通知渠道。 * 删除通知渠道。
* *
* @param request HTTP请求,从中获取用户ID * @param id 渠道ID
* @param id 渠道ID
* @return 操作结果 * @return 操作结果
*/ */
@DeleteMapping("/channels/{id}") @DeleteMapping("/channels/{id}")
@Operation(summary = "删除通知渠道", description = "删除指定的推送渠道") @Operation(summary = "删除通知渠道", description = "删除指定的推送渠道")
public Result<Void> deleteChannel(HttpServletRequest request, @PathVariable Long id) { public Result<Void> deleteChannel(@PathVariable Long id) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
notificationChannelService.deleteChannel(userId, id); notificationChannelService.deleteChannel(userId, id);
return Result.success(); return Result.success();
} }
@@ -140,14 +133,13 @@ public class NotificationController {
/** /**
* 测试指定渠道。 * 测试指定渠道。
* *
* @param request HTTP请求,从中获取用户ID * @param id 渠道ID
* @param id 渠道ID
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping("/channels/{id}/test") @PostMapping("/channels/{id}/test")
@Operation(summary = "测试通知渠道", description = "向指定渠道发送一条测试消息") @Operation(summary = "测试通知渠道", description = "向指定渠道发送一条测试消息")
public Result<Void> testChannel(HttpServletRequest request, @PathVariable Long id) { public Result<Void> testChannel(@PathVariable Long id) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
notificationService.sendTest(userId, id); notificationService.sendTest(userId, id);
return Result.success(); return Result.success();
} }
@@ -155,28 +147,25 @@ public class NotificationController {
/** /**
* 立即发送账单导入提醒。 * 立即发送账单导入提醒。
* *
* @param request HTTP请求,从中获取用户ID
* @return 发送成功的渠道数 * @return 发送成功的渠道数
*/ */
@PostMapping("/send-now") @PostMapping("/send-now")
@Operation(summary = "立即发送提醒", description = "跳过周期设置,立即向所有已启用渠道发送账单导入提醒") @Operation(summary = "立即发送提醒", description = "跳过周期设置,立即向所有已启用渠道发送账单导入提醒")
public Result<Integer> sendNow(HttpServletRequest request) { public Result<Integer> sendNow() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(notificationService.sendReminderNow(userId)); return Result.success(notificationService.sendReminderNow(userId));
} }
/** /**
* 查询最近的发送日志。 * 查询最近的发送日志。
* *
* @param request HTTP请求,从中获取用户ID * @param limit 条数上限,默认 20
* @param limit 条数上限,默认 20
* @return 日志列表 * @return 日志列表
*/ */
@GetMapping("/logs") @GetMapping("/logs")
@Operation(summary = "查询发送日志", description = "查询当前用户最近的提醒发送结果") @Operation(summary = "查询发送日志", description = "查询当前用户最近的提醒发送结果")
public Result<List<NotificationLogVO>> listLogs(HttpServletRequest request, public Result<List<NotificationLogVO>> listLogs(@RequestParam(defaultValue = "20") int limit) {
@RequestParam(defaultValue = "20") int limit) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(notificationService.listLogs(userId, limit)); return Result.success(notificationService.listLogs(userId, limit));
} }
} }
@@ -1,13 +1,10 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result; import java.util.List;
import cn.rosercode.finance.dto.RuleRequest;
import cn.rosercode.finance.service.RuleService;
import cn.rosercode.finance.vo.RuleVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.Data; import lombok.Data;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -19,7 +16,11 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.RuleRequest;
import cn.rosercode.finance.service.RuleService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.RuleVO;
/** /**
* 规则控制器 * 规则控制器
@@ -42,27 +43,25 @@ public class RuleController {
/** /**
* 获取规则列表 * 获取规则列表
* *
* @param request HTTP请求,从中获取用户ID
* @return 规则列表 * @return 规则列表
*/ */
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "获取规则列表", description = "获取当前用户的全部规则,按优先级排序") @Operation(summary = "获取规则列表", description = "获取当前用户的全部规则,按优先级排序")
public Result<List<RuleVO>> list(HttpServletRequest request) { public Result<List<RuleVO>> list() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(ruleService.list(userId)); return Result.success(ruleService.list(userId));
} }
/** /**
* 创建规则 * 创建规则
* *
* @param request HTTP请求,从中获取用户ID
* @param ruleRequest 规则请求参数 * @param ruleRequest 规则请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping @PostMapping
@Operation(summary = "创建规则", description = "为当前用户创建新的分类与标签规则") @Operation(summary = "创建规则", description = "为当前用户创建新的分类与标签规则")
public Result<Void> create(HttpServletRequest request, @RequestBody @Valid RuleRequest ruleRequest) { public Result<Void> create(@RequestBody @Valid RuleRequest ruleRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
ruleService.create(userId, ruleRequest); ruleService.create(userId, ruleRequest);
return Result.success(); return Result.success();
} }
@@ -70,17 +69,15 @@ public class RuleController {
/** /**
* 更新规则 * 更新规则
* *
* @param request HTTP请求,从中获取用户ID
* @param id 规则ID * @param id 规则ID
* @param ruleRequest 规则请求参数 * @param ruleRequest 规则请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新规则", description = "根据规则ID更新规则信息") @Operation(summary = "更新规则", description = "根据规则ID更新规则信息")
public Result<Void> update(HttpServletRequest request, public Result<Void> update(@Parameter(description = "规则ID") @PathVariable Long id,
@Parameter(description = "规则ID") @PathVariable Long id,
@RequestBody @Valid RuleRequest ruleRequest) { @RequestBody @Valid RuleRequest ruleRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
ruleService.update(userId, id, ruleRequest); ruleService.update(userId, id, ruleRequest);
return Result.success(); return Result.success();
} }
@@ -88,15 +85,13 @@ public class RuleController {
/** /**
* 删除规则 * 删除规则
* *
* @param request HTTP请求,从中获取用户ID * @param id 规则ID
* @param id 规则ID
* @return 操作结果 * @return 操作结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "删除规则", description = "根据规则ID删除指定规则及其标签关联") @Operation(summary = "删除规则", description = "根据规则ID删除指定规则及其标签关联")
public Result<Void> delete(HttpServletRequest request, public Result<Void> delete(@Parameter(description = "规则ID") @PathVariable Long id) {
@Parameter(description = "规则ID") @PathVariable Long id) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
ruleService.delete(userId, id); ruleService.delete(userId, id);
return Result.success(); return Result.success();
} }
@@ -104,17 +99,15 @@ public class RuleController {
/** /**
* 启用或停用规则 * 启用或停用规则
* *
* @param request HTTP请求,从中获取用户ID
* @param id 规则ID * @param id 规则ID
* @param enabled 是否启用 * @param enabled 是否启用
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}/enabled") @PutMapping("/{id}/enabled")
@Operation(summary = "启用/停用规则", description = "切换指定规则的启用状态") @Operation(summary = "启用/停用规则", description = "切换指定规则的启用状态")
public Result<Void> toggleEnabled(HttpServletRequest request, public Result<Void> toggleEnabled(@Parameter(description = "规则ID") @PathVariable Long id,
@Parameter(description = "规则ID") @PathVariable Long id,
@RequestBody RuleEnabledRequest enabled) { @RequestBody RuleEnabledRequest enabled) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
ruleService.toggleEnabled(userId, id, enabled.getEnabled()); ruleService.toggleEnabled(userId, id, enabled.getEnabled());
return Result.success(); return Result.success();
} }
@@ -1,26 +1,27 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result; import java.util.List;
import cn.rosercode.finance.dto.StatisticsQuery;
import cn.rosercode.finance.dto.StatisticsRangeQuery;
import cn.rosercode.finance.enums.StatisticsDimension;
import cn.rosercode.finance.service.StatisticsService;
import cn.rosercode.finance.vo.CategoryStatisticsVO;
import cn.rosercode.finance.vo.MonthlyStatisticsVO;
import cn.rosercode.finance.vo.StatisticsGroupVO;
import cn.rosercode.finance.vo.StatisticsSummaryVO;
import cn.rosercode.finance.vo.TrendStatisticsVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.StatisticsQuery;
import cn.rosercode.finance.dto.StatisticsRangeQuery;
import cn.rosercode.finance.enums.StatisticsDimension;
import cn.rosercode.finance.service.StatisticsService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.CategoryStatisticsVO;
import cn.rosercode.finance.vo.MonthlyStatisticsVO;
import cn.rosercode.finance.vo.StatisticsGroupVO;
import cn.rosercode.finance.vo.StatisticsSummaryVO;
import cn.rosercode.finance.vo.TrendStatisticsVO;
/** /**
* 统计控制器 * 统计控制器
@@ -41,77 +42,70 @@ public class StatisticsController {
/** /**
* 获取月度统计数据 * 获取月度统计数据
* *
* @param request HTTP请求,从中获取用户ID
* @param query 统计查询参数 * @param query 统计查询参数
* @return 月度统计结果 * @return 月度统计结果
*/ */
@GetMapping("/monthly") @GetMapping("/monthly")
@Operation(summary = "获取月度统计数据", description = "获取当前用户指定月份的收支统计汇总数据") @Operation(summary = "获取月度统计数据", description = "获取当前用户指定月份的收支统计汇总数据")
public Result<MonthlyStatisticsVO> monthly(HttpServletRequest request, @Valid public Result<MonthlyStatisticsVO> monthly(@Valid
@Parameter(description = "统计查询参数") StatisticsQuery query) { @Parameter(description = "统计查询参数") StatisticsQuery query) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(statisticsService.getMonthlyStatistics(userId, query)); return Result.success(statisticsService.getMonthlyStatistics(userId, query));
} }
/** /**
* 获取分类统计数据 * 获取分类统计数据
* *
* @param request HTTP请求,从中获取用户ID
* @param query 统计查询参数 * @param query 统计查询参数
* @return 分类统计列表 * @return 分类统计列表
*/ */
@GetMapping("/category") @GetMapping("/category")
@Operation(summary = "获取分类统计数据", description = "获取当前用户指定月份按分类维度的统计数据") @Operation(summary = "获取分类统计数据", description = "获取当前用户指定月份按分类维度的统计数据")
public Result<List<CategoryStatisticsVO>> category(HttpServletRequest request, @Valid public Result<List<CategoryStatisticsVO>> category(@Valid
@Parameter(description = "统计查询参数") StatisticsQuery query) { @Parameter(description = "统计查询参数") StatisticsQuery query) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(statisticsService.getCategoryStatistics(userId, query)); return Result.success(statisticsService.getCategoryStatistics(userId, query));
} }
/** /**
* 获取趋势统计数据 * 获取趋势统计数据
* *
* @param request HTTP请求,从中获取用户ID
* @param query 统计查询参数 * @param query 统计查询参数
* @return 趋势统计列表 * @return 趋势统计列表
*/ */
@GetMapping("/trend") @GetMapping("/trend")
@Operation(summary = "获取趋势统计数据", description = "获取当前用户指定时间范围内的收支趋势数据") @Operation(summary = "获取趋势统计数据", description = "获取当前用户指定时间范围内的收支趋势数据")
public Result<List<TrendStatisticsVO>> trend(HttpServletRequest request, @Valid public Result<List<TrendStatisticsVO>> trend(@Valid
@Parameter(description = "统计查询参数") StatisticsQuery query) { @Parameter(description = "统计查询参数") StatisticsQuery query) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(statisticsService.getTrendStatistics(userId, query)); return Result.success(statisticsService.getTrendStatistics(userId, query));
} }
/** /**
* 获取指定时间范围的统计概览 * 获取指定时间范围的统计概览
* *
* @param request HTTP请求,从中获取用户ID * @param query 时间范围查询参数
* @param query 时间范围查询参数
* @return 统计概览 * @return 统计概览
*/ */
@GetMapping("/summary") @GetMapping("/summary")
@Operation(summary = "获取统计概览", description = "获取当前用户指定时间范围的收支汇总、日均、最大单笔与平均每笔") @Operation(summary = "获取统计概览", description = "获取当前用户指定时间范围的收支汇总、日均、最大单笔与平均每笔")
public Result<StatisticsSummaryVO> summary(HttpServletRequest request, public Result<StatisticsSummaryVO> summary(@Parameter(description = "时间范围查询参数") StatisticsRangeQuery query) {
@Parameter(description = "时间范围查询参数") StatisticsRangeQuery query) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(statisticsService.getSummary(userId, query)); return Result.success(statisticsService.getSummary(userId, query));
} }
/** /**
* 按维度获取分组统计数据 * 按维度获取分组统计数据
* *
* @param request HTTP请求,从中获取用户ID
* @param dimension 统计维度(day/week/month/quarter/year/paymentMethod/category/type) * @param dimension 统计维度(day/week/month/quarter/year/paymentMethod/category/type)
* @param query 时间范围查询参数 * @param query 时间范围查询参数
* @return 分组统计列表 * @return 分组统计列表
*/ */
@GetMapping("/group") @GetMapping("/group")
@Operation(summary = "获取分组统计数据", description = "按指定维度对时间范围内的交易进行分组统计") @Operation(summary = "获取分组统计数据", description = "按指定维度对时间范围内的交易进行分组统计")
public Result<List<StatisticsGroupVO>> group(HttpServletRequest request, public Result<List<StatisticsGroupVO>> group(@Parameter(description = "统计维度") @RequestParam("dimension") String dimension,
@Parameter(description = "统计维度") @RequestParam("dimension") String dimension,
@Parameter(description = "时间范围查询参数") StatisticsRangeQuery query) { @Parameter(description = "时间范围查询参数") StatisticsRangeQuery query) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(statisticsService.getGroup(userId, query, StatisticsDimension.from(dimension))); return Result.success(statisticsService.getGroup(userId, query, StatisticsDimension.from(dimension)));
} }
} }
@@ -1,13 +1,10 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result; import java.util.List;
import cn.rosercode.finance.dto.TagRequest;
import cn.rosercode.finance.service.TagService;
import cn.rosercode.finance.vo.TagVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,7 +15,11 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.TagRequest;
import cn.rosercode.finance.service.TagService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.TagVO;
/** /**
* 标签控制器 * 标签控制器
@@ -41,27 +42,25 @@ public class TagController {
/** /**
* 获取标签列表 * 获取标签列表
* *
* @param request HTTP请求,从中获取用户ID
* @return 标签列表 * @return 标签列表
*/ */
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "获取标签列表", description = "获取当前用户的全部标签") @Operation(summary = "获取标签列表", description = "获取当前用户的全部标签")
public Result<List<TagVO>> list(HttpServletRequest request) { public Result<List<TagVO>> list() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(tagService.list(userId)); return Result.success(tagService.list(userId));
} }
/** /**
* 创建标签 * 创建标签
* *
* @param request HTTP请求,从中获取用户ID
* @param tagRequest 标签请求参数 * @param tagRequest 标签请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping @PostMapping
@Operation(summary = "创建标签", description = "为当前用户创建新的标签") @Operation(summary = "创建标签", description = "为当前用户创建新的标签")
public Result<Void> create(HttpServletRequest request, @RequestBody @Valid TagRequest tagRequest) { public Result<Void> create(@RequestBody @Valid TagRequest tagRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
tagService.create(userId, tagRequest); tagService.create(userId, tagRequest);
return Result.success(); return Result.success();
} }
@@ -69,17 +68,15 @@ public class TagController {
/** /**
* 更新标签 * 更新标签
* *
* @param request HTTP请求,从中获取用户ID
* @param id 标签ID * @param id 标签ID
* @param tagRequest 标签请求参数 * @param tagRequest 标签请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新标签", description = "根据标签ID更新标签信息") @Operation(summary = "更新标签", description = "根据标签ID更新标签信息")
public Result<Void> update(HttpServletRequest request, public Result<Void> update(@Parameter(description = "标签ID") @PathVariable Long id,
@Parameter(description = "标签ID") @PathVariable Long id,
@RequestBody @Valid TagRequest tagRequest) { @RequestBody @Valid TagRequest tagRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
tagService.update(userId, id, tagRequest); tagService.update(userId, id, tagRequest);
return Result.success(); return Result.success();
} }
@@ -87,15 +84,13 @@ public class TagController {
/** /**
* 删除标签 * 删除标签
* *
* @param request HTTP请求,从中获取用户ID * @param id 标签ID
* @param id 标签ID
* @return 操作结果 * @return 操作结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "删除标签", description = "根据标签ID删除指定标签及其交易关联") @Operation(summary = "删除标签", description = "根据标签ID删除指定标签及其交易关联")
public Result<Void> delete(HttpServletRequest request, public Result<Void> delete(@Parameter(description = "标签ID") @PathVariable Long id) {
@Parameter(description = "标签ID") @PathVariable Long id) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
tagService.delete(userId, id); tagService.delete(userId, id);
return Result.success(); return Result.success();
} }
@@ -1,24 +1,20 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy; import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.BatchCategoryRequest;
import cn.rosercode.finance.dto.BatchDeleteRequest;
import cn.rosercode.finance.dto.BatchTagRequest;
import cn.rosercode.finance.dto.TransactionQuery;
import cn.rosercode.finance.dto.TransactionRequest;
import cn.rosercode.finance.exporter.TransactionExportSheetHandler;
import cn.rosercode.finance.service.TransactionService;
import cn.rosercode.finance.vo.TransactionExportVO;
import cn.rosercode.finance.vo.TransactionVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Sheet;
@@ -32,13 +28,18 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.io.IOException; import cn.rosercode.finance.common.PageResult;
import java.net.URLEncoder; import cn.rosercode.finance.common.Result;
import java.nio.charset.StandardCharsets; import cn.rosercode.finance.dto.BatchCategoryRequest;
import java.time.LocalDateTime; import cn.rosercode.finance.dto.BatchDeleteRequest;
import java.time.format.DateTimeFormatter; import cn.rosercode.finance.dto.BatchTagRequest;
import java.util.List; import cn.rosercode.finance.dto.TransactionQuery;
import java.util.Map; import cn.rosercode.finance.dto.TransactionRequest;
import cn.rosercode.finance.exporter.TransactionExportSheetHandler;
import cn.rosercode.finance.service.TransactionService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.TransactionExportVO;
import cn.rosercode.finance.vo.TransactionVO;
/** /**
* 交易记录控制器 * 交易记录控制器
@@ -70,29 +71,26 @@ public class TransactionController {
/** /**
* 分页查询交易记录 * 分页查询交易记录
* *
* @param request HTTP请求,从中获取用户ID
* @param query 分页查询参数 * @param query 分页查询参数
* @return 分页交易记录列表 * @return 分页交易记录列表
*/ */
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "分页查询交易记录", description = "根据查询条件分页获取当前用户的交易记录") @Operation(summary = "分页查询交易记录", description = "根据查询条件分页获取当前用户的交易记录")
public Result<PageResult<TransactionVO>> page(HttpServletRequest request, public Result<PageResult<TransactionVO>> page(@Parameter(description = "分页查询参数") TransactionQuery query) {
@Parameter(description = "分页查询参数") TransactionQuery query) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(transactionService.page(userId, query)); return Result.success(transactionService.page(userId, query));
} }
/** /**
* 新增交易记录 * 新增交易记录
* *
* @param request HTTP请求,从中获取用户ID
* @param transactionRequest 交易记录请求参数 * @param transactionRequest 交易记录请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PostMapping @PostMapping
@Operation(summary = "新增交易记录", description = "为当前用户新增一条交易记录") @Operation(summary = "新增交易记录", description = "为当前用户新增一条交易记录")
public Result<Void> create(HttpServletRequest request, @RequestBody @Valid TransactionRequest transactionRequest) { public Result<Void> create(@RequestBody @Valid TransactionRequest transactionRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
transactionService.create(userId, transactionRequest); transactionService.create(userId, transactionRequest);
return Result.success(); return Result.success();
} }
@@ -100,17 +98,15 @@ public class TransactionController {
/** /**
* 更新交易记录 * 更新交易记录
* *
* @param request HTTP请求,从中获取用户ID
* @param id 交易记录ID * @param id 交易记录ID
* @param transactionRequest 交易记录请求参数 * @param transactionRequest 交易记录请求参数
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新交易记录", description = "根据交易记录ID更新交易信息") @Operation(summary = "更新交易记录", description = "根据交易记录ID更新交易信息")
public Result<Void> update(HttpServletRequest request, public Result<Void> update(@Parameter(description = "交易记录ID") @PathVariable Long id,
@Parameter(description = "交易记录ID") @PathVariable Long id,
@RequestBody @Valid TransactionRequest transactionRequest) { @RequestBody @Valid TransactionRequest transactionRequest) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
transactionService.update(userId, id, transactionRequest); transactionService.update(userId, id, transactionRequest);
return Result.success(); return Result.success();
} }
@@ -118,15 +114,13 @@ public class TransactionController {
/** /**
* 删除交易记录 * 删除交易记录
* *
* @param request HTTP请求,从中获取用户ID
* @param id 交易记录ID * @param id 交易记录ID
* @return 操作结果 * @return 操作结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "删除交易记录", description = "根据交易记录ID删除指定交易记录") @Operation(summary = "删除交易记录", description = "根据交易记录ID删除指定交易记录")
public Result<Void> delete(HttpServletRequest request, public Result<Void> delete(@Parameter(description = "交易记录ID") @PathVariable Long id) {
@Parameter(description = "交易记录ID") @PathVariable Long id) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
transactionService.delete(userId, id); transactionService.delete(userId, id);
return Result.success(); return Result.success();
} }
@@ -134,32 +128,28 @@ public class TransactionController {
/** /**
* 批量删除交易记录 * 批量删除交易记录
* *
* @param request HTTP请求,从中获取用户ID * @param body 批量删除请求(记录ID列表)
* @param body 批量删除请求(记录ID列表)
* @return 实际删除的记录数 * @return 实际删除的记录数
*/ */
@DeleteMapping("/batch") @DeleteMapping("/batch")
@Operation(summary = "批量删除交易记录", description = "根据记录ID列表批量删除当前用户的交易记录") @Operation(summary = "批量删除交易记录", description = "根据记录ID列表批量删除当前用户的交易记录")
public Result<Integer> batchDelete(HttpServletRequest request, public Result<Integer> batchDelete(@RequestBody @Valid BatchDeleteRequest body) {
@RequestBody @Valid BatchDeleteRequest body) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(transactionService.batchDelete(userId, body.getIds())); return Result.success(transactionService.batchDelete(userId, body.getIds()));
} }
/** /**
* 切换交易记录的"计入统计"标记 * 切换交易记录的"计入统计"标记
* *
* @param request HTTP请求,从中获取用户ID
* @param id 交易记录ID * @param id 交易记录ID
* @param body 请求体,包含 includeInStats 字段 * @param body 请求体,包含 includeInStats 字段
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}/stats") @PutMapping("/{id}/stats")
@Operation(summary = "切换计入统计标记", description = "切换交易记录是否计入统计(用于表格中开关)") @Operation(summary = "切换计入统计标记", description = "切换交易记录是否计入统计(用于表格中开关)")
public Result<Void> toggleStats(HttpServletRequest request, public Result<Void> toggleStats(@Parameter(description = "交易记录ID") @PathVariable Long id,
@Parameter(description = "交易记录ID") @PathVariable Long id,
@RequestBody Map<String, Boolean> body) { @RequestBody Map<String, Boolean> body) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
Boolean includeInStats = body != null ? body.get("includeInStats") : null; Boolean includeInStats = body != null ? body.get("includeInStats") : null;
transactionService.toggleStats(userId, id, includeInStats); transactionService.toggleStats(userId, id, includeInStats);
return Result.success(); return Result.success();
@@ -168,17 +158,15 @@ public class TransactionController {
/** /**
* 切换交易记录的备注隐藏标记 * 切换交易记录的备注隐藏标记
* *
* @param request HTTP请求,从中获取用户ID
* @param id 交易记录ID * @param id 交易记录ID
* @param body 请求体,包含 remarkHidden 字段 * @param body 请求体,包含 remarkHidden 字段
* @return 操作结果 * @return 操作结果
*/ */
@PutMapping("/{id}/remark-hidden") @PutMapping("/{id}/remark-hidden")
@Operation(summary = "切换备注隐藏标记", description = "切换交易记录的备注是否隐藏(用于表格中单元格按钮)") @Operation(summary = "切换备注隐藏标记", description = "切换交易记录的备注是否隐藏(用于表格中单元格按钮)")
public Result<Void> toggleRemarkHidden(HttpServletRequest request, public Result<Void> toggleRemarkHidden(@Parameter(description = "交易记录ID") @PathVariable Long id,
@Parameter(description = "交易记录ID") @PathVariable Long id,
@RequestBody Map<String, Boolean> body) { @RequestBody Map<String, Boolean> body) {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
Boolean remarkHidden = body != null ? body.get("remarkHidden") : null; Boolean remarkHidden = body != null ? body.get("remarkHidden") : null;
transactionService.toggleRemarkHidden(userId, id, remarkHidden); transactionService.toggleRemarkHidden(userId, id, remarkHidden);
return Result.success(); return Result.success();
@@ -187,47 +175,40 @@ public class TransactionController {
/** /**
* 批量设置交易记录的分类 * 批量设置交易记录的分类
* *
* @param request HTTP请求,从中获取用户ID * @param body 批量设置请求参数
* @param body 批量设置请求参数
* @return 实际更新的记录数 * @return 实际更新的记录数
*/ */
@PutMapping("/batch-category") @PutMapping("/batch-category")
@Operation(summary = "批量设置分类", description = "将当前表格展示的多条交易记录按类型一键归入所选分类") @Operation(summary = "批量设置分类", description = "将当前表格展示的多条交易记录按类型一键归入所选分类")
public Result<Integer> batchUpdateCategory(HttpServletRequest request, public Result<Integer> batchUpdateCategory(@RequestBody @Valid BatchCategoryRequest body) {
@RequestBody @Valid BatchCategoryRequest body) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(transactionService.batchUpdateCategory(userId, body)); return Result.success(transactionService.batchUpdateCategory(userId, body));
} }
/** /**
* 批量给交易记录添加/移除标签 * 批量给交易记录添加/移除标签
* *
* @param request HTTP请求,从中获取用户ID * @param body 批量标签请求参数
* @param body 批量标签请求参数
* @return 涉及的记录数 * @return 涉及的记录数
*/ */
@PutMapping("/batch-tags") @PutMapping("/batch-tags")
@Operation(summary = "批量设置标签", description = "为选中的交易记录添加或移除标签") @Operation(summary = "批量设置标签", description = "为选中的交易记录添加或移除标签")
public Result<Integer> batchTags(HttpServletRequest request, public Result<Integer> batchTags(@RequestBody @Valid BatchTagRequest body) {
@RequestBody @Valid BatchTagRequest body) { Long userId = UserContext.getUserId();
Long userId = (Long) request.getAttribute("userId");
return Result.success(transactionService.batchTags(userId, body)); return Result.success(transactionService.batchTags(userId, body));
} }
/** /**
* 导出交易记录为 Excel 文件 * 导出交易记录为 Excel 文件
* *
* @param request HTTP请求,从中获取用户ID
* @param response HTTP响应,用于输出文件流
* @param query 查询条件 * @param query 查询条件
* @throws IOException 文件输出异常 * @throws IOException 文件输出异常
*/ */
@GetMapping("/export") @GetMapping("/export")
@Operation(summary = "导出交易记录", description = "根据查询条件导出交易记录为 Excel 文件") @Operation(summary = "导出交易记录", description = "根据查询条件导出交易记录为 Excel 文件")
public void export(HttpServletRequest request, public void export(@Parameter(description = "查询条件") TransactionQuery query) throws IOException {
HttpServletResponse response, Long userId = UserContext.getUserId();
@Parameter(description = "查询条件") TransactionQuery query) throws IOException { HttpServletResponse response = UserContext.getResponse();
Long userId = (Long) request.getAttribute("userId");
// 设置响应头 // 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@@ -1,17 +1,7 @@
package cn.rosercode.finance.controller; package cn.rosercode.finance.controller;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.LoginRequest;
import cn.rosercode.finance.dto.RegisterRequest;
import cn.rosercode.finance.dto.RegistrationUpdateRequest;
import cn.rosercode.finance.service.RegistrationService;
import cn.rosercode.finance.service.UserService;
import cn.rosercode.finance.vo.LoginVO;
import cn.rosercode.finance.vo.RegistrationStatusVO;
import cn.rosercode.finance.vo.UserVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -20,6 +10,17 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.LoginRequest;
import cn.rosercode.finance.dto.RegisterRequest;
import cn.rosercode.finance.dto.RegistrationUpdateRequest;
import cn.rosercode.finance.service.RegistrationService;
import cn.rosercode.finance.service.UserService;
import cn.rosercode.finance.util.UserContext;
import cn.rosercode.finance.vo.LoginVO;
import cn.rosercode.finance.vo.RegistrationStatusVO;
import cn.rosercode.finance.vo.UserVO;
/** /**
* 用户控制器 * 用户控制器
* *
@@ -90,13 +91,12 @@ public class UserController {
/** /**
* 获取当前用户信息 * 获取当前用户信息
* *
* @param request HTTP请求,从中获取用户ID
* @return 用户信息 * @return 用户信息
*/ */
@GetMapping("/info") @GetMapping("/info")
@Operation(summary = "获取当前用户信息", description = "获取已登录用户的详细信息") @Operation(summary = "获取当前用户信息", description = "获取已登录用户的详细信息")
public Result<UserVO> info(HttpServletRequest request) { public Result<UserVO> info() {
Long userId = (Long) request.getAttribute("userId"); Long userId = UserContext.getUserId();
return Result.success(userService.getUserInfo(userId)); return Result.success(userService.getUserInfo(userId));
} }
} }
@@ -1,5 +1,11 @@
package cn.rosercode.finance.converter; package cn.rosercode.finance.converter;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import cn.rosercode.finance.entity.AiUsage; import cn.rosercode.finance.entity.AiUsage;
import cn.rosercode.finance.entity.Category; import cn.rosercode.finance.entity.Category;
import cn.rosercode.finance.entity.HttpLog; import cn.rosercode.finance.entity.HttpLog;
@@ -20,11 +26,6 @@ import cn.rosercode.finance.vo.RuleVO;
import cn.rosercode.finance.vo.TagVO; import cn.rosercode.finance.vo.TagVO;
import cn.rosercode.finance.vo.TransactionVO; import cn.rosercode.finance.vo.TransactionVO;
import cn.rosercode.finance.vo.UserVO; import cn.rosercode.finance.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import java.util.List;
/** /**
* 实体与视图对象之间的转换器,由 MapStruct 在编译期生成实现。 * 实体与视图对象之间的转换器,由 MapStruct 在编译期生成实现。
@@ -1,12 +1,13 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import cn.rosercode.finance.vo.AiNewTagVO; import java.util.List;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import java.util.List; import cn.rosercode.finance.vo.AiNewTagVO;
/** /**
* AI 建议应用请求DTO,携带逐条记录的建议分类与标签,用于一键写入。 * AI 建议应用请求DTO,携带逐条记录的建议分类与标签,用于一键写入。
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import java.math.BigDecimal;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
/** /**
* 单条记录 AI 建议请求 DTO,携带表单当前内容用于生成分类与标签建议。 * 单条记录 AI 建议请求 DTO,携带表单当前内容用于生成分类与标签建议。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import java.util.List;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* AI 建议请求DTO,携带需要分析分类与标签的交易记录ID列表。 * AI 建议请求DTO,携带需要分析分类与标签的交易记录ID列表。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import java.util.List;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 批量设置分类请求 DTO,用于将当前表格展示的多条交易记录一键归入分类。 * 批量设置分类请求 DTO,用于将当前表格展示的多条交易记录一键归入分类。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import java.util.List;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 批量删除交易记录请求DTO,用于一次性删除多条收支记录。 * 批量删除交易记录请求DTO,用于一次性删除多条收支记录。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import java.util.List;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 批量设置标签请求DTO,用于为多条交易记录添加或移除标签。 * 批量设置标签请求DTO,用于为多条交易记录添加或移除标签。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/** /**
* 账单导入请求 DTO,用于「后导入」阶段接收前端回传的勾选行。 * 账单导入请求 DTO,用于「后导入」阶段接收前端回传的勾选行。
* *
@@ -1,8 +1,9 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import cn.rosercode.finance.config.datasource.DataSourceType;
import lombok.Data; import lombok.Data;
import cn.rosercode.finance.config.datasource.DataSourceType;
/** /**
* 数据库配置请求对象,用于提交 MySQL 或 SQLite 的连接信息。 * 数据库配置请求对象,用于提交 MySQL 或 SQLite 的连接信息。
* *
@@ -1,9 +1,9 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import lombok.Data;
import java.util.Map; import java.util.Map;
import lombok.Data;
/** /**
* 通知渠道保存请求DTO,用于新增或修改消息推送渠道。 * 通知渠道保存请求DTO,用于新增或修改消息推送渠道。
* *
@@ -1,11 +1,11 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/** /**
* 规则请求DTO,用于接收创建或更新自定义分类与标签规则时提交的信息。 * 规则请求DTO,用于接收创建或更新自定义分类与标签规则时提交的信息。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import lombok.Data;
/** /**
* 统计查询 DTO,用于按时间区间及可选过滤条件进行统计。 * 统计查询 DTO,用于按时间区间及可选过滤条件进行统计。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import lombok.Data;
/** /**
* 交易查询DTO,用于接收交易记录分页查询的筛选条件 * 交易查询DTO,用于接收交易记录分页查询的筛选条件
*/ */
@@ -1,14 +1,14 @@
package cn.rosercode.finance.dto; package cn.rosercode.finance.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/** /**
* 交易请求DTO,用于接收创建或更新交易记录时提交的信息 * 交易请求DTO,用于接收创建或更新交易记录时提交的信息
*/ */
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* AI 配置实体类,对应数据库表 t_ai_config,用于记录用户的 AI 智能建议配置。 * AI 配置实体类,对应数据库表 t_ai_config,用于记录用户的 AI 智能建议配置。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* AI 使用记录实体类,对应数据库表 t_ai_usage,记录每次调用 DeepSeek 的用量与结果。 * AI 使用记录实体类,对应数据库表 t_ai_usage,记录每次调用 DeepSeek 的用量与结果。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 分类实体类,对应数据库表 t_category,用于记录收支分类信息 * 分类实体类,对应数据库表 t_category,用于记录收支分类信息
*/ */
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* HTTP 访问日志实体类,对应数据库表 t_http_log,记录每次接口请求的方法、路径、状态与耗时。 * HTTP 访问日志实体类,对应数据库表 t_http_log,记录每次接口请求的方法、路径、状态与耗时。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 登录日志实体类,对应数据库表 t_login_log,记录每次登录尝试的结果。 * 登录日志实体类,对应数据库表 t_login_log,记录每次登录尝试的结果。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 通知渠道实体类,对应数据库表 t_notification_channel,用于记录用户配置的消息推送渠道。 * 通知渠道实体类,对应数据库表 t_notification_channel,用于记录用户配置的消息推送渠道。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 通知配置实体类,对应数据库表 t_notification_config,用于记录用户的账单导入提醒周期。 * 通知配置实体类,对应数据库表 t_notification_config,用于记录用户的账单导入提醒周期。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 通知日志实体类,对应数据库表 t_notification_log,用于记录每次提醒的发送结果。 * 通知日志实体类,对应数据库表 t_notification_log,用于记录每次提醒的发送结果。
* *
@@ -1,13 +1,13 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/** /**
* 自定义分类与标签规则实体类,对应数据库表 t_rule。 * 自定义分类与标签规则实体类,对应数据库表 t_rule。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 规则标签关联实体类,对应数据库表 t_rule_tag,表示规则与标签的多对多关系。 * 规则标签关联实体类,对应数据库表 t_rule_tag,表示规则与标签的多对多关系。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 标签实体类,对应数据库表 t_tag,用于记录用户自定义标签。 * 标签实体类,对应数据库表 t_tag,用于记录用户自定义标签。
* *
@@ -1,13 +1,13 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/** /**
* 交易记录实体类,对应数据库表 t_transaction,用于记录每笔收支交易 * 交易记录实体类,对应数据库表 t_transaction,用于记录每笔收支交易
*/ */
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 交易标签关联实体类,对应数据库表 t_transaction_tag,维护交易与标签的多对多关系。 * 交易标签关联实体类,对应数据库表 t_transaction_tag,维护交易与标签的多对多关系。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.entity; package cn.rosercode.finance.entity;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime;
/** /**
* 用户实体类,对应数据库表 t_user * 用户实体类,对应数据库表 t_user
*/ */
@@ -1,14 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.enums.BillType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -24,6 +15,16 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.enums.BillType;
/** /**
* 支付宝账单解析器。 * 支付宝账单解析器。
* *
@@ -1,11 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.common.BusinessException;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -13,6 +7,13 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import org.springframework.stereotype.Component;
import cn.rosercode.finance.common.BusinessException;
/** /**
* 账单压缩包解压工具。 * 账单压缩包解压工具。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.enums.BillType;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import cn.rosercode.finance.enums.BillType;
/** /**
* 账单解析器接口,定义不同账单来源的解析行为。 * 账单解析器接口,定义不同账单来源的解析行为。
* *
@@ -1,17 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.enums.BillType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -25,6 +13,19 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.enums.BillType;
/** /**
* 交通银行账单解析器。 * 交通银行账单解析器。
* *
@@ -1,14 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.enums.BillType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@@ -27,6 +18,16 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.enums.BillType;
/** /**
* 招商银行账单解析器。 * 招商银行账单解析器。
* *
@@ -1,14 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@@ -22,6 +13,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/** /**
* 招商银行「交易流水」PDF 解析器。 * 招商银行「交易流水」PDF 解析器。
* *
@@ -1,14 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.enums.BillType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -24,6 +15,16 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.enums.BillType;
/** /**
* 美团账单解析器。 * 美团账单解析器。
* *
@@ -1,12 +1,13 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.vo.TagVO;
import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import lombok.Data;
import cn.rosercode.finance.vo.TagVO;
/** /**
* 账单解析结果行,作为「先解析、后导入」流程中解析阶段的中间对象。 * 账单解析结果行,作为「先解析、后导入」流程中解析阶段的中间对象。
* *
@@ -1,17 +1,5 @@
package cn.rosercode.finance.importer; package cn.rosercode.finance.importer;
import cn.rosercode.finance.enums.BillType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -23,6 +11,19 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import cn.rosercode.finance.enums.BillType;
/** /**
* 微信支付账单解析器。 * 微信支付账单解析器。
* *
@@ -1,14 +1,14 @@
package cn.rosercode.finance.interceptor; package cn.rosercode.finance.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import cn.rosercode.finance.entity.HttpLog; import cn.rosercode.finance.entity.HttpLog;
import cn.rosercode.finance.service.LogService; import cn.rosercode.finance.service.LogService;
import cn.rosercode.finance.util.RequestUtils; import cn.rosercode.finance.util.RequestUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/** /**
* HTTP 访问日志拦截器。 * HTTP 访问日志拦截器。
@@ -19,10 +19,9 @@ import org.springframework.web.servlet.HandlerInterceptor;
* @date 2026-09-18 16:53:21 * @date 2026-09-18 16:53:21
*/ */
@Component @Component
@Slf4j
public class AccessLogInterceptor implements HandlerInterceptor { public class AccessLogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AccessLogInterceptor.class);
/** 请求属性名:请求开始时间 */ /** 请求属性名:请求开始时间 */
private static final String START_TIME_ATTR = "accessLogStartTime"; private static final String START_TIME_ATTR = "accessLogStartTime";
@@ -1,15 +1,16 @@
package cn.rosercode.finance.interceptor; package cn.rosercode.finance.interceptor;
import cn.rosercode.finance.config.datasource.DataSourceContextHolder; import java.util.HashMap;
import cn.rosercode.finance.util.JwtUtil; import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.util.HashMap; import cn.rosercode.finance.config.datasource.DataSourceContextHolder;
import java.util.Map; import cn.rosercode.finance.util.JwtUtil;
/** /**
* JWT 认证拦截器。 * JWT 认证拦截器。
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.AiConfig;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.AiConfig;
/** /**
* AI 配置Mapper接口,继承MyBatis-Plus的BaseMapper,提供AI配置表的基础CRUD操作。 * AI 配置Mapper接口,继承MyBatis-Plus的BaseMapper,提供AI配置表的基础CRUD操作。
* *
@@ -1,12 +1,13 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.AiUsage;
import cn.rosercode.finance.vo.AiUsageSummaryVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import cn.rosercode.finance.entity.AiUsage;
import cn.rosercode.finance.vo.AiUsageSummaryVO;
/** /**
* AI 使用记录Mapper接口,继承MyBatis-Plus的BaseMapper,并提供汇总统计查询。 * AI 使用记录Mapper接口,继承MyBatis-Plus的BaseMapper,并提供汇总统计查询。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.Category;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.Category;
/** /**
* 分类Mapper接口,继承MyBatis-Plus的BaseMapper,提供分类表的基础CRUD操作 * 分类Mapper接口,继承MyBatis-Plus的BaseMapper,提供分类表的基础CRUD操作
*/ */
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.HttpLog;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.HttpLog;
/** /**
* HTTP 访问日志Mapper接口,继承MyBatis-Plus的BaseMapper,提供访问日志表的基础CRUD操作。 * HTTP 访问日志Mapper接口,继承MyBatis-Plus的BaseMapper,提供访问日志表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.LoginLog;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.LoginLog;
/** /**
* 登录日志Mapper接口,继承MyBatis-Plus的BaseMapper,提供登录日志表的基础CRUD操作。 * 登录日志Mapper接口,继承MyBatis-Plus的BaseMapper,提供登录日志表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.NotificationChannel;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.NotificationChannel;
/** /**
* 通知渠道Mapper接口,继承MyBatis-Plus的BaseMapper,提供通知渠道表的基础CRUD操作。 * 通知渠道Mapper接口,继承MyBatis-Plus的BaseMapper,提供通知渠道表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.NotificationConfig;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.NotificationConfig;
/** /**
* 通知配置Mapper接口,继承MyBatis-Plus的BaseMapper,提供通知配置表的基础CRUD操作。 * 通知配置Mapper接口,继承MyBatis-Plus的BaseMapper,提供通知配置表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.NotificationLog;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.NotificationLog;
/** /**
* 通知日志Mapper接口,继承MyBatis-Plus的BaseMapper,提供通知日志表的基础CRUD操作。 * 通知日志Mapper接口,继承MyBatis-Plus的BaseMapper,提供通知日志表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.Rule;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.Rule;
/** /**
* 规则Mapper接口,继承MyBatis-Plus的BaseMapper,提供规则表的基础CRUD操作。 * 规则Mapper接口,继承MyBatis-Plus的BaseMapper,提供规则表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.RuleTag;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.RuleTag;
/** /**
* 规则标签关联Mapper接口,继承MyBatis-Plus的BaseMapper,提供规则标签关联表的基础CRUD操作。 * 规则标签关联Mapper接口,继承MyBatis-Plus的BaseMapper,提供规则标签关联表的基础CRUD操作。
* *
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.Tag;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.Tag;
/** /**
* 标签Mapper接口,继承MyBatis-Plus的BaseMapper,提供标签表的基础CRUD操作。 * 标签Mapper接口,继承MyBatis-Plus的BaseMapper,提供标签表的基础CRUD操作。
* *
@@ -1,11 +1,12 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.Transaction;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import cn.rosercode.finance.entity.Transaction;
/** /**
* 交易记录Mapper接口,继承MyBatis-Plus的BaseMapper,提供交易记录表的基础CRUD操作 * 交易记录Mapper接口,继承MyBatis-Plus的BaseMapper,提供交易记录表的基础CRUD操作
@@ -1,11 +1,12 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.TransactionTag;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import cn.rosercode.finance.entity.TransactionTag;
/** /**
* 交易标签关联Mapper接口,继承MyBatis-Plus的BaseMapper,提供关联表的基础CRUD操作。 * 交易标签关联Mapper接口,继承MyBatis-Plus的BaseMapper,提供关联表的基础CRUD操作。
@@ -1,9 +1,10 @@
package cn.rosercode.finance.mapper; package cn.rosercode.finance.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.rosercode.finance.entity.User;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import cn.rosercode.finance.entity.User;
/** /**
* 用户Mapper接口,继承MyBatis-Plus的BaseMapper,提供用户表的基础CRUD操作 * 用户Mapper接口,继承MyBatis-Plus的BaseMapper,提供用户表的基础CRUD操作
*/ */
@@ -1,9 +1,5 @@
package cn.rosercode.finance.notification; package cn.rosercode.finance.notification;
import cn.rosercode.finance.common.BusinessException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
@@ -14,6 +10,11 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.rosercode.finance.common.BusinessException;
/** /**
* HTTP 通知发送器抽象基类。 * HTTP 通知发送器抽象基类。
* *
@@ -1,7 +1,8 @@
package cn.rosercode.finance.notification; package cn.rosercode.finance.notification;
import cn.rosercode.finance.common.BusinessException; import java.util.Map;
import cn.rosercode.finance.enums.NotificationChannelType; import java.util.Properties;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.MailException; import org.springframework.mail.MailException;
@@ -9,8 +10,8 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Map; import cn.rosercode.finance.common.BusinessException;
import java.util.Properties; import cn.rosercode.finance.enums.NotificationChannelType;
/** /**
* 邮箱通知发送器,通过 SMTP 发送纯文本邮件(兼容 QQ 邮箱)。 * 邮箱通知发送器,通过 SMTP 发送纯文本邮件(兼容 QQ 邮箱)。
@@ -1,9 +1,9 @@
package cn.rosercode.finance.notification; package cn.rosercode.finance.notification;
import cn.rosercode.finance.enums.NotificationChannelType;
import java.util.Map; import java.util.Map;
import cn.rosercode.finance.enums.NotificationChannelType;
/** /**
* 通知发送器接口,定义不同渠道的消息推送能力。 * 通知发送器接口,定义不同渠道的消息推送能力。
* *
@@ -1,13 +1,14 @@
package cn.rosercode.finance.notification; package cn.rosercode.finance.notification;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.enums.NotificationChannelType;
import org.springframework.stereotype.Component;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.stereotype.Component;
import cn.rosercode.finance.common.BusinessException;
import cn.rosercode.finance.enums.NotificationChannelType;
/** /**
* 通知发送器工厂,按渠道类型分发到对应的发送器实现。 * 通知发送器工厂,按渠道类型分发到对应的发送器实现。
* *
@@ -1,14 +1,15 @@
package cn.rosercode.finance.notification; package cn.rosercode.finance.notification;
import cn.rosercode.finance.common.BusinessException; import java.util.LinkedHashMap;
import cn.rosercode.finance.enums.NotificationChannelType; import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.LinkedHashMap; import cn.rosercode.finance.common.BusinessException;
import java.util.Map; import cn.rosercode.finance.enums.NotificationChannelType;
/** /**
* Server 酱通知发送器,通过 Server 酱 Turbo 接口将消息推送到个人微信。 * Server 酱通知发送器,通过 Server 酱 Turbo 接口将消息推送到个人微信。
@@ -1,18 +1,18 @@
package cn.rosercode.finance.scheduler; package cn.rosercode.finance.scheduler;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import cn.rosercode.finance.config.app.AppConfig; import cn.rosercode.finance.config.app.AppConfig;
import cn.rosercode.finance.config.app.AppConfigStore; import cn.rosercode.finance.config.app.AppConfigStore;
import cn.rosercode.finance.entity.HttpLog; import cn.rosercode.finance.entity.HttpLog;
import cn.rosercode.finance.entity.LoginLog; import cn.rosercode.finance.entity.LoginLog;
import cn.rosercode.finance.mapper.HttpLogMapper; import cn.rosercode.finance.mapper.HttpLogMapper;
import cn.rosercode.finance.mapper.LoginLogMapper; import cn.rosercode.finance.mapper.LoginLogMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/** /**
* 日志清理调度器。 * 日志清理调度器。
@@ -23,10 +23,9 @@ import java.time.LocalDateTime;
* @date 2026-09-18 17:15:49 * @date 2026-09-18 17:15:49
*/ */
@Component @Component
@Slf4j
public class LogCleanupScheduler { public class LogCleanupScheduler {
private static final Logger log = LoggerFactory.getLogger(LogCleanupScheduler.class);
private final AppConfigStore appConfigStore; private final AppConfigStore appConfigStore;
private final HttpLogMapper httpLogMapper; private final HttpLogMapper httpLogMapper;
private final LoginLogMapper loginLogMapper; private final LoginLogMapper loginLogMapper;
@@ -1,17 +1,5 @@
package cn.rosercode.finance.scheduler; package cn.rosercode.finance.scheduler;
import cn.rosercode.finance.entity.NotificationChannel;
import cn.rosercode.finance.entity.NotificationConfig;
import cn.rosercode.finance.enums.NotificationFrequency;
import cn.rosercode.finance.mapper.NotificationConfigMapper;
import cn.rosercode.finance.service.NotificationChannelService;
import cn.rosercode.finance.service.NotificationService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
@@ -19,6 +7,18 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.List; import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import cn.rosercode.finance.entity.NotificationChannel;
import cn.rosercode.finance.entity.NotificationConfig;
import cn.rosercode.finance.enums.NotificationFrequency;
import cn.rosercode.finance.mapper.NotificationConfigMapper;
import cn.rosercode.finance.service.NotificationChannelService;
import cn.rosercode.finance.service.NotificationService;
/** /**
* 通知调度器。 * 通知调度器。
* *
@@ -28,10 +28,9 @@ import java.util.List;
* @date 2026-09-18 12:38:30 * @date 2026-09-18 12:38:30
*/ */
@Component @Component
@Slf4j
public class NotificationScheduler { public class NotificationScheduler {
private static final Logger log = LoggerFactory.getLogger(NotificationScheduler.class);
/** 时间格式 */ /** 时间格式 */
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
@@ -1,11 +1,11 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.dto.AiApplyRequest; import cn.rosercode.finance.dto.AiApplyRequest;
import cn.rosercode.finance.dto.AiSuggestOneRequest; import cn.rosercode.finance.dto.AiSuggestOneRequest;
import cn.rosercode.finance.vo.AiSuggestionVO; import cn.rosercode.finance.vo.AiSuggestionVO;
import java.util.List;
/** /**
* AI 建议服务接口,提供交易记录的分类与标签智能建议及应用能力。 * AI 建议服务接口,提供交易记录的分类与标签智能建议及应用能力。
* *
@@ -1,10 +1,5 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import cn.rosercode.finance.util.AppLogUtils;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
@@ -18,6 +13,12 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import cn.rosercode.finance.util.AppLogUtils;
/** /**
* 应用运行日志实时推送服务。 * 应用运行日志实时推送服务。
* *
@@ -1,12 +1,12 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.io.IOException;
import cn.rosercode.finance.dto.BillImportRequest; import cn.rosercode.finance.dto.BillImportRequest;
import cn.rosercode.finance.enums.BillType; import cn.rosercode.finance.enums.BillType;
import cn.rosercode.finance.vo.BillImportResultVO; import cn.rosercode.finance.vo.BillImportResultVO;
import cn.rosercode.finance.vo.BillParseResultVO; import cn.rosercode.finance.vo.BillParseResultVO;
import java.io.IOException;
/** /**
* 账单导入服务,负责「先解析、后导入」两阶段的业务逻辑。 * 账单导入服务,负责「先解析、后导入」两阶段的业务逻辑。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.dto.CategoryRequest; import cn.rosercode.finance.dto.CategoryRequest;
import cn.rosercode.finance.vo.CategoryVO; import cn.rosercode.finance.vo.CategoryVO;
import java.util.List;
/** /**
* 分类服务接口,提供收支分类的增删改查操作。 * 分类服务接口,提供收支分类的增删改查操作。
*/ */
@@ -1,11 +1,11 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.dto.NotificationChannelRequest; import cn.rosercode.finance.dto.NotificationChannelRequest;
import cn.rosercode.finance.entity.NotificationChannel; import cn.rosercode.finance.entity.NotificationChannel;
import cn.rosercode.finance.vo.NotificationChannelVO; import cn.rosercode.finance.vo.NotificationChannelVO;
import java.util.List;
/** /**
* 通知渠道服务接口,负责用户消息推送渠道的增删改查。 * 通知渠道服务接口,负责用户消息推送渠道的增删改查。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.entity.NotificationChannel; import cn.rosercode.finance.entity.NotificationChannel;
import cn.rosercode.finance.vo.NotificationLogVO; import cn.rosercode.finance.vo.NotificationLogVO;
import java.util.List;
/** /**
* 通知发送服务接口,负责消息的实际推送与发送日志记录。 * 通知发送服务接口,负责消息的实际推送与发送日志记录。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.math.BigDecimal;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
/** /**
* 规则匹配上下文,承载单条交易的匹配所需信息。 * 规则匹配上下文,承载单条交易的匹配所需信息。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import lombok.Data;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.Data;
/** /**
* 规则匹配结果,承载命中规则后建议应用的分类与标签。 * 规则匹配结果,承载命中规则后建议应用的分类与标签。
* *
@@ -1,10 +1,10 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.dto.RuleRequest; import cn.rosercode.finance.dto.RuleRequest;
import cn.rosercode.finance.vo.RuleVO; import cn.rosercode.finance.vo.RuleVO;
import java.util.List;
/** /**
* 规则服务,负责自定义分类与标签规则的增删改查及匹配。 * 规则服务,负责自定义分类与标签规则的增删改查及匹配。
* *
@@ -1,5 +1,7 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.dto.StatisticsQuery; import cn.rosercode.finance.dto.StatisticsQuery;
import cn.rosercode.finance.dto.StatisticsRangeQuery; import cn.rosercode.finance.dto.StatisticsRangeQuery;
import cn.rosercode.finance.enums.StatisticsDimension; import cn.rosercode.finance.enums.StatisticsDimension;
@@ -9,8 +11,6 @@ import cn.rosercode.finance.vo.StatisticsGroupVO;
import cn.rosercode.finance.vo.StatisticsSummaryVO; import cn.rosercode.finance.vo.StatisticsSummaryVO;
import cn.rosercode.finance.vo.TrendStatisticsVO; import cn.rosercode.finance.vo.TrendStatisticsVO;
import java.util.List;
/** /**
* 统计服务接口,提供月度收支统计、分类统计和趋势分析功能。 * 统计服务接口,提供月度收支统计、分类统计和趋势分析功能。
*/ */
@@ -1,10 +1,10 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.dto.TagRequest; import cn.rosercode.finance.dto.TagRequest;
import cn.rosercode.finance.vo.TagVO; import cn.rosercode.finance.vo.TagVO;
import java.util.List;
/** /**
* 标签服务接口,提供标签的增删改查操作。 * 标签服务接口,提供标签的增删改查操作。
* *
@@ -1,5 +1,7 @@
package cn.rosercode.finance.service; package cn.rosercode.finance.service;
import java.util.List;
import cn.rosercode.finance.common.PageResult; import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.dto.BatchCategoryRequest; import cn.rosercode.finance.dto.BatchCategoryRequest;
import cn.rosercode.finance.dto.BatchTagRequest; import cn.rosercode.finance.dto.BatchTagRequest;
@@ -8,8 +10,6 @@ import cn.rosercode.finance.dto.TransactionRequest;
import cn.rosercode.finance.vo.TransactionExportVO; import cn.rosercode.finance.vo.TransactionExportVO;
import cn.rosercode.finance.vo.TransactionVO; import cn.rosercode.finance.vo.TransactionVO;
import java.util.List;
/** /**
* 交易记录服务接口,提供收支记录的分页查询、增删改操作。 * 交易记录服务接口,提供收支记录的分页查询、增删改操作。
*/ */
@@ -1,5 +1,12 @@
package cn.rosercode.finance.service.impl; package cn.rosercode.finance.service.impl;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import cn.rosercode.finance.client.DeepSeekClient; import cn.rosercode.finance.client.DeepSeekClient;
import cn.rosercode.finance.client.DeepSeekResponse; import cn.rosercode.finance.client.DeepSeekResponse;
import cn.rosercode.finance.common.BusinessException; import cn.rosercode.finance.common.BusinessException;
@@ -9,12 +16,6 @@ import cn.rosercode.finance.service.AiCategoryService;
import cn.rosercode.finance.service.AiConfigService; import cn.rosercode.finance.service.AiConfigService;
import cn.rosercode.finance.service.AiUsageService; import cn.rosercode.finance.service.AiUsageService;
import cn.rosercode.finance.vo.AiCategoryResultVO; import cn.rosercode.finance.vo.AiCategoryResultVO;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* AI 生成分类服务实现类,根据自然语言描述生成分类名称、类型与图标。 * AI 生成分类服务实现类,根据自然语言描述生成分类名称、类型与图标。

Some files were not shown because too many files have changed in this diff Show More