7. 报表与分析
财务报表
业务分析报表
监管报表
数据可视化
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Reporting -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- Other -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.14</version>
</dependency>
</dependencies>
spring:
datasource:
url: jdbc:mysql://localhost:3306/bank_reporting_db?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
reporting:
output-directory: ./reports
retention-days: 30
server:
port: 8080
@Entity
@Table(name = "report_definition")
@Data
public class ReportDefinition {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String reportCode;
@Column(nullable = false)
private String reportName;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportType reportType;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportFrequency frequency;
@Column(nullable = false, columnDefinition = "TEXT")
private String sqlQuery;
@Column(nullable = false)
private String outputFields;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportFormat defaultFormat;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportStatus status = ReportStatus.ACTIVE;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "generated_report")
@Data
public class GeneratedReport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String reportCode;
@Column(nullable = false)
private String reportName;
@Column(nullable = false)
private LocalDate reportDate;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportFormat format;
@Column(nullable = false)
private String filePath;
@Column(nullable = false)
private Long fileSize;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportStatus status = ReportStatus.PENDING;
@Column
private String generatedBy;
@CreationTimestamp
private LocalDateTime createdAt;
}
@Entity
@Table(name = "report_schedule")
@Data
public class ReportSchedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String reportCode;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ReportFrequency frequency;
@Column
private LocalTime dailyTime;
@Column
private Integer dayOfMonth;
@Column
private Integer weekDay;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ScheduleStatus status = ScheduleStatus.ACTIVE;
@Column
private String recipients;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
public enum ReportType {
FINANCIAL, // 财务报表
BUSINESS, // 业务报表
REGULATORY, // 监管报表
OPERATIONAL, // 运营报表
ANALYTICAL // 分析报表
}
public enum ReportFrequency {
DAILY, // 每日
WEEKLY, // 每周
MONTHLY, // 每月
QUARTERLY, // 每季度
YEARLY, // 每年
ON_DEMAND // 按需
}
public enum ReportFormat {
PDF, // PDF格式
EXCEL, // Excel格式
CSV, // CSV格式
HTML, // HTML格式
JSON // JSON格式
}
public enum ReportStatus {
ACTIVE, // 活跃
INACTIVE, // 不活跃
PENDING, // 待处理
GENERATING, // 生成中
COMPLETED, // 已完成
FAILED // 失败
}
public enum ScheduleStatus {
ACTIVE, // 活跃
INACTIVE, // 不活跃
PAUSED // 暂停
}
@Data
public class ReportDefinitionDTO {
private Long id;
private String reportCode;
private String reportName;
private ReportType reportType;
private ReportFrequency frequency;
private String sqlQuery;
private String outputFields;
private ReportFormat defaultFormat;
private ReportStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@Data
public class ReportRequestDTO {
@NotNull
private String reportCode;
private LocalDate reportDate;
@NotNull
private ReportFormat format;
private String parameters; // JSON格式的参数
}
@Data
public class ReportRequestDTO {
@NotNull
private String reportCode;
private LocalDate reportDate;
@NotNull
private ReportFormat format;
private String parameters; // JSON格式的参数
}
@Data
public class ReportScheduleDTO {
private Long id;
private String reportCode;
private ReportFrequency frequency;
private LocalTime dailyTime;
private Integer dayOfMonth;
private Integer weekDay;
private ScheduleStatus status;
private String recipients;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@Data
public class ReportDataDTO {
private String reportCode;
private String reportName;
private List<String> headers;
private List<Map<String, Object>> data;
private long totalRecords;
}
@Data
public class DashboardDataDTO {
private BigDecimal totalDeposits;
private BigDecimal totalLoans;
private BigDecimal totalAssets;
private BigDecimal totalLiabilities;
private BigDecimal netIncome;
private Map<LocalDate, BigDecimal> dailyTransactions;
private Map<String, Long> customerDistribution;
private List<Map<String, Object>> recentTransactions;
private List<Map<String, Object>> loanPortfolio;
}
Repository层
public interface ReportDefinitionRepository extends JpaRepository<ReportDefinition, Long> {
Optional<ReportDefinition> findByReportCode(String reportCode);
List<ReportDefinition> findByReportType(ReportType reportType);
List<ReportDefinition> findByStatus(ReportStatus status);
}
public interface GeneratedReportRepository extends JpaRepository<GeneratedReport, Long> {
List<GeneratedReport> findByReportCodeAndReportDateBetween(
String reportCode, LocalDate startDate, LocalDate endDate);
List<GeneratedReport> findByReportCodeAndFormatAndStatus(
String reportCode, ReportFormat format, ReportStatus status);
Optional<GeneratedReport> findByReportCodeAndReportDateAndFormat(
String reportCode, LocalDate reportDate, ReportFormat format);
}
public interface ReportScheduleRepository extends JpaRepository<ReportSchedule, Long> {
List<ReportSchedule> findByReportCode(String reportCode);
List<ReportSchedule> findByFrequencyAndStatus(ReportFrequency frequency, ScheduleStatus status);
}
Service层
@Service
@RequiredArgsConstructor
@Slf4j
public class ReportGenerationService {
private final ReportDefinitionRepository definitionRepository;
private final GeneratedReportRepository generatedReportRepository;
private final ReportQueryExecutor queryExecutor;
private final ReportFileGenerator fileGenerator;
@Transactional
public GeneratedReportDTO generateReport(ReportRequestDTO request) {
LocalDate reportDate = request.getReportDate() != null ?
request.getReportDate() : LocalDate.now();
// 检查是否已存在相同报告
Optional<GeneratedReport> existingReport = generatedReportRepository
.findByReportCodeAndReportDateAndFormat(
request.getReportCode(),
reportDate,
request.getFormat());
if (existingReport.isPresent()) {
return convertToDTO(existingReport.get());
}
// 获取报告定义
ReportDefinition definition = definitionRepository.findByReportCode(request.getReportCode())
.orElseThrow(() -> new AccountException(ErrorCode.REPORT_DEFINITION_NOT_FOUND));
// 创建报告记录
GeneratedReport report = new GeneratedReport();
report.setReportCode(definition.getReportCode());
report.setReportName(definition.getReportName());
report.setReportDate(reportDate);
report.setFormat(request.getFormat());
report.setStatus(ReportStatus.PENDING);
GeneratedReport saved = generatedReportRepository.save(report);
log.info("Report generation requested: {}", saved.getId());
// 异步生成报告
generateReportAsync(saved.getId(), definition, request.getParameters());
return convertToDTO(saved);
}
@Async
public void generateReportAsync(Long reportId, ReportDefinition definition, String parameters) {
try {
// 更新状态为生成中
GeneratedReport report = generatedReportRepository.findById(reportId)
.orElseThrow(() -> new AccountException(ErrorCode.REPORT_NOT_FOUND));
report.setStatus(ReportStatus.GENERATING);
generatedReportRepository.save(report);
// 执行SQL查询获取数据
Map<String, Object> params = parseParameters(parameters);
ReportDataDTO reportData = queryExecutor.executeQuery(definition.getSqlQuery(), params);
// 生成报告文件
String filePath = fileGenerator.generateReportFile(
reportData,
definition.getOutputFields(),
report.getFormat(),
definition.getReportCode(),
report.getReportDate());
// 更新报告记录
report.setFilePath(filePath);
report.setFileSize(getFileSize(filePath));
report.setStatus(ReportStatus.COMPLETED);
generatedReportRepository.save(report);
log.info("Report generated successfully: {}", reportId);
} catch (Exception e) {
log.error("Failed to generate report: " + reportId, e);
generatedReportRepository.findById(reportId).ifPresent(r -> {
r.setStatus(ReportStatus.FAILED);
generatedReportRepository.save(r);
});
}
}
@Transactional(readOnly = true)
public ReportDataDTO getReportData(String reportCode, String parameters) {
ReportDefinition definition = definitionRepository.findByReportCode(reportCode)
.orElseThrow(() -> new AccountException(ErrorCode.REPORT_DEFINITION_NOT_FOUND));
Map<String, Object> params = parseParameters(parameters);
return queryExecutor.executeQuery(definition.getSqlQuery(), params);
}
private Map<String, Object> parseParameters(String parameters) {
// 实现JSON参数解析
// 实际项目中可以使用Jackson或Gson
return Map.of(); // 简化处理
}
private long getFileSize(String filePath) {
// 实现获取文件大小逻辑
return 0; // 简化处理
}
private GeneratedReportDTO convertToDTO(GeneratedReport report) {
GeneratedReportDTO dto = new GeneratedReportDTO();
dto.setId(report.getId());
dto.setReportCode(report.getReportCode());
dto.setReportName(report.getReportName());
dto.setReportDate(report.getReportDate());
dto.setFormat(report.getFormat());
dto.setFilePath(report.getFilePath());
dto.setFileSize(report.getFileSize());
dto.setStatus(report.getStatus());
dto.setGeneratedBy(report.getGeneratedBy());
dto.setCreatedAt(report.getCreatedAt());
return dto;
}
}
/**
报表查询执行器
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ReportQueryExecutor {
private final JdbcTemplate jdbcTemplate;
public ReportDataDTO executeQuery(String sqlQuery, Map<String, Object> parameters) {
try {
ReportDataDTO result = new ReportDataDTO();
List<Map<String, Object>> data = new ArrayList<>();
// 执行查询
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sqlQuery, parameters);
// 获取列信息
List<String> headers = new ArrayList<>();
if (rowSet.next()) {
String[] columnNames = rowSet.getMetaData().getColumnNames();
for (String columnName : columnNames) {
headers.add(columnName);
}
// 处理第一行数据
data.add(extractRowData(rowSet, headers));
// 处理剩余数据
while (rowSet.next()) {
data.add(extractRowData(rowSet, headers));
}
}
result.setHeaders(headers);
result.setData(data);
result.setTotalRecords(data.size());
return result;
} catch (Exception e) {
log.error("Error executing report query: " + e.getMessage(), e);
throw new AccountException(ErrorCode.REPORT_QUERY_EXECUTION_FAILED);
}
}
private Map<String, Object> extractRowData(SqlRowSet rowSet, List<String> headers) {
Map<String, Object> rowData = new HashMap<>();
for (String column : headers) {
rowData.put(column, rowSet.getObject(column));
}
return rowData;
}
}
/**
报表文件生成器
*/
@Component
@Slf4j
public class ReportFileGenerator {
@Value("${reporting.output-directory}")
private String outputDirectory;
public String generateReportFile(
ReportDataDTO reportData,
String outputFields,
ReportFormat format,
String reportCode,
LocalDate reportDate) throws IOException {
String fileName = String.format("%s_%s.%s",
reportCode,
reportDate.format(DateTimeFormatter.BASIC_ISO_DATE),
format.name().toLowerCase());
Path outputPath = Paths.get(outputDirectory, fileName);
switch (format) {
case EXCEL:
generateExcelFile(reportData, outputPath);
break;
case PDF:
// 实现PDF生成逻辑
break;
case CSV:
// 实现CSV生成逻辑
break;
case HTML:
// 实现HTML生成逻辑
break;
case JSON:
// 实现JSON生成逻辑
break;
default:
throw new IllegalArgumentException("Unsupported report format: " + format);
}
return outputPath.toString();
}
private void generateExcelFile(ReportDataDTO reportData, Path outputPath) throws IOException {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Report");
// 创建标题行
Row headerRow = sheet.createRow(0);
List<String> headers = reportData.getHeaders();
for (int i = 0; i < headers.size(); i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers.get(i));
}
// 填充数据
List<Map<String, Object>> data = reportData.getData();
for (int i = 0; i < data.size(); i++) {
Row row = sheet.createRow(i + 1);
Map<String, Object> rowData = data.get(i);
for (int j = 0; j < headers.size(); j++) {
Cell cell = row.createCell(j);
Object value = rowData.get(headers.get(j));
if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else {
cell.setCellValue(value != null ? value.toString() : "");
}
}
}
// 自动调整列宽
for (int i = 0; i < headers.size(); i++) {
sheet.autoSizeColumn(i);
}
// 写入文件
try (FileOutputStream outputStream = new FileOutputStream(outputPath.toFile())) {
workbook.write(outputStream);
}
}
}
}
/**
定时任务服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ReportSchedulingService {
private final ReportScheduleRepository scheduleRepository;
private final ReportGenerationService generationService;
@Scheduled(cron = "0 0 6 * * ?") // 每天6点执行
public void generateDailyReports() {
log.info("Starting daily report generation");
List<ReportSchedule> schedules = scheduleRepository
.findByFrequencyAndStatus(ReportFrequency.DAILY, ScheduleStatus.ACTIVE);
schedules.forEach(schedule -> {
try {
generateScheduledReport(schedule);
} catch (Exception e) {
log.error("Failed to generate daily report: " + schedule.getReportCode(), e);
}
});
}
@Scheduled(cron = "0 0 6 * * MON") // 每周一6点执行
public void generateWeeklyReports() {
log.info("Starting weekly report generation");
List<ReportSchedule> schedules = scheduleRepository
.findByFrequencyAndStatus(ReportFrequency.WEEKLY, ScheduleStatus.ACTIVE);
schedules.forEach(schedule -> {
try {
generateScheduledReport(schedule);
} catch (Exception e) {
log.error("Failed to generate weekly report: " + schedule.getReportCode(), e);
}
});
}
@Scheduled(cron = "0 0 6 1 * ?") // 每月1号6点执行
public void generateMonthlyReports() {
log.info("Starting monthly report generation");
List<ReportSchedule> schedules = scheduleRepository
.findByFrequencyAndStatus(ReportFrequency.MONTHLY, ScheduleStatus.ACTIVE);
schedules.forEach(schedule -> {
try {
generateScheduledReport(schedule);
} catch (Exception e) {
log.error("Failed to generate monthly report: " + schedule.getReportCode(), e);
}
});
}
private void generateScheduledReport(ReportSchedule schedule) {
ReportRequestDTO request = new ReportRequestDTO();
request.setReportCode(schedule.getReportCode());
request.setFormat(ReportFormat.EXCEL); // 默认生成Excel格式
generationService.generateReport(request);
log.info("Scheduled report generated: {}", schedule.getReportCode());
}
}
/**
数据可视化服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class DashboardService {
private final JdbcTemplate jdbcTemplate;
public DashboardDataDTO getDashboardData() {
DashboardDataDTO dashboard = new DashboardDataDTO();
// 获取总存款
dashboard.setTotalDeposits(getTotalDeposits());
// 获取总贷款
dashboard.setTotalLoans(getTotalLoans());
// 获取总资产和总负债
Map<String, BigDecimal> assetsAndLiabilities = getAssetsAndLiabilities();
dashboard.setTotalAssets(assetsAndLiabilities.get("assets"));
dashboard.setTotalLiabilities(assetsAndLiabilities.get("liabilities"));
// 获取净收入
dashboard.setNetIncome(getNetIncome());
// 获取每日交易量
dashboard.setDailyTransactions(getDailyTransactions());
// 获取客户分布
dashboard.setCustomerDistribution(getCustomerDistribution());
// 获取最近交易
dashboard.setRecentTransactions(getRecentTransactions());
// 获取贷款组合
dashboard.setLoanPortfolio(getLoanPortfolio());
return dashboard;
}
private BigDecimal getTotalDeposits() {
String sql = "SELECT SUM(balance) FROM account WHERE account_type IN ('PERSONAL_SAVINGS', 'PERSONAL_CURRENT', 'CORPORATE_CURRENT')";
return jdbcTemplate.queryForObject(sql, BigDecimal.class);
}
private BigDecimal getTotalLoans() {
String sql = "SELECT SUM(outstanding_amount) FROM loan_account WHERE status = 'ACTIVE'";
return jdbcTemplate.queryForObject(sql, BigDecimal.class);
}
private Map<String, BigDecimal> getAssetsAndLiabilities() {
String sql = "SELECT 'assets' AS type, SUM(balance) AS amount FROM account " +
"UNION ALL " +
"SELECT 'liabilities' AS type, SUM(outstanding_amount) AS amount FROM loan_account";
return jdbcTemplate.query(sql, rs -> {
Map<String, BigDecimal> result = new HashMap<>();
while (rs.next()) {
result.put(rs.getString("type"), rs.getBigDecimal("amount"));
}
return result;
});
}
private BigDecimal getNetIncome() {
String sql = "SELECT (SELECT SUM(amount) FROM transaction WHERE amount > 0 AND created_at >= DATE_TRUNC('month', CURRENT_DATE)) - " +
"(SELECT SUM(amount) FROM transaction WHERE amount < 0 AND created_at >= DATE_TRUNC('month', CURRENT_DATE))";
return jdbcTemplate.queryForObject(sql, BigDecimal.class);
}
private Map<LocalDate, BigDecimal> getDailyTransactions() {
String sql = "SELECT DATE(created_at) AS day, SUM(ABS(amount)) AS volume " +
"FROM transaction " +
"WHERE created_at >= CURRENT_DATE - INTERVAL '30 days' " +
"GROUP BY day " +
"ORDER BY day";
return jdbcTemplate.query(sql, rs -> {
Map<LocalDate, BigDecimal> result = new HashMap<>();
while (rs.next()) {
result.put(rs.getDate("day").toLocalDate(), rs.getBigDecimal("volume"));
}
return result;
});
}
private Map<String, Long> getCustomerDistribution() {
String sql = "SELECT tier, COUNT(*) AS count FROM customer GROUP BY tier";
return jdbcTemplate.query(sql, rs -> {
Map<String, Long> result = new HashMap<>();
while (rs.next()) {
result.put(rs.getString("tier"), rs.getLong("count"));
}
return result;
});
}
private List<Map<String, Object>> getRecentTransactions() {
String sql = "SELECT t.id, t.amount, t.description, a.account_number, c.first_name, c.last_name " +
"FROM transaction t " +
"JOIN account a ON t.account_number = a.account_number " +
"JOIN customer c ON a.customer_id = c.id " +
"ORDER BY t.created_at DESC LIMIT 10";
return jdbcTemplate.queryForList(sql);
}
private List<Map<String, Object>> getLoanPortfolio() {
String sql = "SELECT loan_type, SUM(outstanding_amount) AS amount, COUNT(*) AS count " +
"FROM loan_account " +
"WHERE status = 'ACTIVE' " +
"GROUP BY loan_type";
return jdbcTemplate.queryForList(sql);
}
}
Controller层
@RestController
@RequestMapping("/api/reports")
@RequiredArgsConstructor
public class ReportController {
private final ReportGenerationService generationService;
private final ReportSchedulingService schedulingService;
@PostMapping("/generate")
@PreAuthorize("hasAnyRole('REPORT_USER', 'ADMIN')")
public GeneratedReportDTO generateReport(@Valid @RequestBody ReportRequestDTO request) {
return generationService.generateReport(request);
}
@GetMapping("/data/{reportCode}")
@PreAuthorize("hasAnyRole('REPORT_USER', 'ADMIN')")
public ReportDataDTO getReportData(
@PathVariable String reportCode,
@RequestParam(required = false) String parameters) {
return generationService.getReportData(reportCode, parameters);
}
}
// DashboardController.java
package com.bank.reporting.controller;
import com.bank.reporting.dto.DashboardDataDTO;
import com.bank.reporting.service.DashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/dashboard")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
@GetMapping
public DashboardDataDTO getDashboardData() {
return dashboardService.getDashboardData();
}
}
8. 系统管理
用户权限管理
角色管理
系统参数配置
操作日志审计
@Entity
@Table(name = "system_user")
@Data
public class SystemUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String fullName;
@Column(nullable = false, unique = true)
private String email;
@Column
private String phone;
@Column(nullable = false)
private String department;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<SystemRole> roles = new HashSet<>();
@Column
private LocalDateTime lastLoginTime;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
@Version
private Long version;
}
@Entity
@Table(name = "system_role")
@Data
public class SystemRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String roleCode;
@Column(nullable = false)
private String roleName;
@Column
private String description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<SystemPermission> permissions = new HashSet<>();
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "system_permission")
@Data
public class SystemPermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String permissionCode;
@Column(nullable = false)
private String permissionName;
@Column
private String description;
@Column(nullable = false)
private String resource;
@Column(nullable = false)
private String action;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "system_parameter")
@Data
public class SystemParameter {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String paramKey;
@Column(nullable = false)
private String paramName;
@Column(nullable = false)
private String paramValue;
@Column
private String description;
@Column(nullable = false)
private Boolean isEditable = true;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "audit_log")
@Data
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private String username;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AuditAction action;
@Column(nullable = false)
private String entityType;
@Column
private Long entityId;
@Column(columnDefinition = "TEXT")
private String oldValue;
@Column(columnDefinition = "TEXT")
private String newValue;
@Column
private String ipAddress;
@Column
private String userAgent;
@CreationTimestamp
private LocalDateTime createdAt;
}
public enum UserStatus {
ACTIVE, // 活跃
INACTIVE, // 不活跃
LOCKED, // 锁定
PASSWORD_EXPIRED // 密码过期
}
public enum AuditAction {
CREATE, // 创建
UPDATE, // 更新
DELETE, // 删除
LOGIN, // 登录
LOGOUT, // 登出
ACCESS // 访问
}
public enum AuditAction {
CREATE, // 创建
UPDATE, // 更新
DELETE, // 删除
LOGIN, // 登录
LOGOUT, // 登出
ACCESS // 访问
}
@Data
public class UserCreateRequest {
@NotBlank
private String username;
@NotBlank
@Size(min = 8)
private String password;
@NotBlank
private String fullName;
@Email
@NotBlank
private String email;
private String phone;
@NotBlank
private String department;
}
@Data
public class UserUpdateRequest {
@NotBlank
private String fullName;
@Email
@NotBlank
private String email;
private String phone;
@NotBlank
private String department;
}
@Data
public class RoleDTO {
private Long id;
private String roleCode;
private String roleName;
private String description;
private Set<String> permissions;
private LocalDateTime createdAt;
}
@Data
public class RoleDTO {
private Long id;
private String roleCode;
private String roleName;
private String description;
private Set<String> permissions;
private LocalDateTime createdAt;
}
@Data
public class ParameterDTO {
private Long id;
private String paramKey;
private String paramName;
private String paramValue;
private String description;
private Boolean isEditable;
private LocalDateTime createdAt;
}
@Data
public class AuditLogDTO {
private Long id;
private Long userId;
private String username;
private AuditAction action;
private String entityType;
private Long entityId;
private String oldValue;
private String newValue;
private String ipAddress;
private String userAgent;
private LocalDateTime createdAt;
}
Repository层
public interface UserRepository extends JpaRepository<SystemUser, Long> {
Optional<SystemUser> findByUsername(String username);
Optional<SystemUser> findByEmail(String email);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
@Query("SELECT u FROM SystemUser u JOIN u.roles r WHERE r.roleCode = :roleCode")
List<SystemUser> findByRoleCode(@Param("roleCode") String roleCode);
}
public interface RoleRepository extends JpaRepository<SystemRole, Long> {
Optional<SystemRole> findByRoleCode(String roleCode);
boolean existsByRoleCode(String roleCode);
}
public interface RoleRepository extends JpaRepository<SystemRole, Long> {
Optional<SystemRole> findByRoleCode(String roleCode);
boolean existsByRoleCode(String roleCode);
}
public interface ParameterRepository extends JpaRepository<SystemParameter, Long> {
Optional<SystemParameter> findByParamKey(String paramKey);
boolean existsByParamKey(String paramKey);
}
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
List<AuditLog> findByUserId(Long userId);
List<AuditLog> findByAction(AuditAction action);
List<AuditLog> findByEntityTypeAndEntityId(String entityType, Long entityId);
List<AuditLog> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
Service层
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final AuditLogService auditLogService;
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public UserDTO createUser(UserCreateRequest request) {
if (userRepository.existsByUsername(request.getUsername())) {
throw new AccountException(ErrorCode.USERNAME_ALREADY_EXISTS);
}
if (userRepository.existsByEmail(request.getEmail())) {
throw new AccountException(ErrorCode.EMAIL_ALREADY_EXISTS);
}
SystemUser user = new SystemUser();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setDepartment(request.getDepartment());
user.setStatus(UserStatus.ACTIVE);
SystemUser saved = userRepository.save(user);
log.info("User created: {}", saved.getUsername());
auditLogService.logUserAction(saved.getId(), "CREATE", "User created");
return convertToDTO(saved);
}
@Transactional
@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public UserDTO updateUser(String username, UserUpdateRequest request) {
SystemUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new AccountException(ErrorCode.USER_NOT_FOUND));
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setDepartment(request.getDepartment());
SystemUser saved = userRepository.save(user);
log.info("User updated: {}", username);
auditLogService.logUserAction(saved.getId(), "UPDATE", "User updated");
return convertToDTO(saved);
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public UserDTO updateUserStatus(String username, UserStatus status) {
SystemUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new AccountException(ErrorCode.USER_NOT_FOUND));
user.setStatus(status);
SystemUser saved = userRepository.save(user);
log.info("User status updated to {}: {}", status, username);
auditLogService.logUserAction(saved.getId(), "UPDATE", "Status updated to " + status);
return convertToDTO(saved);
}
@Transactional
@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public void changePassword(String username, String newPassword) {
SystemUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new AccountException(ErrorCode.USER_NOT_FOUND));
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
log.info("Password changed for user: {}", username);
auditLogService.logUserAction(user.getId(), "UPDATE", "Password changed");
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public UserDTO assignRoles(String username, Set<String> roleCodes) {
SystemUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new AccountException(ErrorCode.USER_NOT_FOUND));
Set<SystemRole> roles = roleCodes.stream()
.map(roleCode -> roleRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new AccountException(ErrorCode.ROLE_NOT_FOUND)))
.collect(Collectors.toSet());
user.setRoles(roles);
SystemUser saved = userRepository.save(user);
log.info("Roles assigned to user {}: {}", username, roleCodes);
auditLogService.logUserAction(saved.getId(), "UPDATE", "Roles assigned: " + roleCodes);
return convertToDTO(saved);
}
@Transactional(readOnly = true)
@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public UserDTO getUser(String username) {
SystemUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new AccountException(ErrorCode.USER_NOT_FOUND));
return convertToDTO(user);
}
@Transactional(readOnly = true)
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> getAllUsers() {
return userRepository.findAll()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> getUsersByRole(String roleCode) {
return userRepository.findByRoleCode(roleCode)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
private UserDTO convertToDTO(SystemUser user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setFullName(user.getFullName());
dto.setEmail(user.getEmail());
dto.setPhone(user.getPhone());
dto.setDepartment(user.getDepartment());
dto.setStatus(user.getStatus());
dto.setRoles(user.getRoles().stream()
.map(SystemRole::getRoleCode)
.collect(Collectors.toSet()));
dto.setLastLoginTime(user.getLastLoginTime());
dto.setCreatedAt(user.getCreatedAt());
return dto;
}
}
/**
角色与权限服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RoleService {
private final RoleRepository roleRepository;
private final PermissionRepository permissionRepository;
private final AuditLogService auditLogService;
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public RoleDTO createRole(String roleCode, String roleName, String description) {
if (roleRepository.existsByRoleCode(roleCode)) {
throw new AccountException(ErrorCode.ROLE_ALREADY_EXISTS);
}
SystemRole role = new SystemRole();
role.setRoleCode(roleCode);
role.setRoleName(roleName);
role.setDescription(description);
SystemRole saved = roleRepository.save(role);
log.info("Role created: {}", roleCode);
auditLogService.logRoleAction(saved.getId(), "CREATE", "Role created");
return convertToDTO(saved);
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public RoleDTO updateRole(String roleCode, String roleName, String description) {
SystemRole role = roleRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new AccountException(ErrorCode.ROLE_NOT_FOUND));
role.setRoleName(roleName);
role.setDescription(description);
SystemRole saved = roleRepository.save(role);
log.info("Role updated: {}", roleCode);
auditLogService.logRoleAction(saved.getId(), "UPDATE", "Role updated");
return convertToDTO(saved);
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public RoleDTO assignPermissions(String roleCode, Set<String> permissionCodes) {
SystemRole role = roleRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new AccountException(ErrorCode.ROLE_NOT_FOUND));
Set<SystemPermission> permissions = permissionCodes.stream()
.map(code -> permissionRepository.findByPermissionCode(code)
.orElseThrow(() -> new AccountException(ErrorCode.PERMISSION_NOT_FOUND)))
.collect(Collectors.toSet());
role.setPermissions(permissions);
SystemRole saved = roleRepository.save(role);
log.info("Permissions assigned to role {}: {}", roleCode, permissionCodes);
auditLogService.logRoleAction(saved.getId(), "UPDATE", "Permissions assigned: " + permissionCodes);
return convertToDTO(saved);
}
@Transactional(readOnly = true)
public List<RoleDTO> getAllRoles() {
return roleRepository.findAll()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public RoleDTO getRole(String roleCode) {
SystemRole role = roleRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new AccountException(ErrorCode.ROLE_NOT_FOUND));
return convertToDTO(role);
}
private RoleDTO convertToDTO(SystemRole role) {
RoleDTO dto = new RoleDTO();
dto.setId(role.getId());
dto.setRoleCode(role.getRoleCode());
dto.setRoleName(role.getRoleName());
dto.setDescription(role.getDescription());
dto.setPermissions(role.getPermissions().stream()
.map(SystemPermission::getPermissionCode)
.collect(Collectors.toSet()));
dto.setCreatedAt(role.getCreatedAt());
return dto;
}
}
/**
系统参数服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ParameterService {
private final ParameterRepository parameterRepository;
private final AuditLogService auditLogService;
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public ParameterDTO createParameter(String key, String name, String value, String description, Boolean isEditable) {
if (parameterRepository.existsByParamKey(key)) {
throw new AccountException(ErrorCode.PARAMETER_ALREADY_EXISTS);
}
SystemParameter parameter = new SystemParameter();
parameter.setParamKey(key);
parameter.setParamName(name);
parameter.setParamValue(value);
parameter.setDescription(description);
parameter.setIsEditable(isEditable);
SystemParameter saved = parameterRepository.save(parameter);
log.info("System parameter created: {}", key);
auditLogService.logParameterAction(saved.getId(), "CREATE", "Parameter created");
return convertToDTO(saved);
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public ParameterDTO updateParameter(String key, String value, String description) {
SystemParameter parameter = parameterRepository.findByParamKey(key)
.orElseThrow(() -> new AccountException(ErrorCode.PARAMETER_NOT_FOUND));
if (!parameter.getIsEditable()) {
throw new AccountException(ErrorCode.PARAMETER_NOT_EDITABLE);
}
parameter.setParamValue(value);
parameter.setDescription(description);
SystemParameter saved = parameterRepository.save(parameter);
log.info("System parameter updated: {}", key);
auditLogService.logParameterAction(saved.getId(), "UPDATE", "Parameter updated");
return convertToDTO(saved);
}
@Transactional(readOnly = true)
public ParameterDTO getParameter(String key) {
SystemParameter parameter = parameterRepository.findByParamKey(key)
.orElseThrow(() -> new AccountException(ErrorCode.PARAMETER_NOT_FOUND));
return convertToDTO(parameter);
}
@Transactional(readOnly = true)
public List<ParameterDTO> getAllParameters() {
return parameterRepository.findAll()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
private ParameterDTO convertToDTO(SystemParameter parameter) {
ParameterDTO dto = new ParameterDTO();
dto.setId(parameter.getId());
dto.setParamKey(parameter.getParamKey());
dto.setParamName(parameter.getParamName());
dto.setParamValue(parameter.getParamValue());
dto.setDescription(parameter.getDescription());
dto.setIsEditable(parameter.getIsEditable());
dto.setCreatedAt(parameter.getCreatedAt());
return dto;
}
}
/**
审计日志服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuditLogService {
private final AuditLogRepository auditLogRepository;
@Transactional
public void logUserAction(Long userId, String action, String description) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
AuditLog logEntry = new AuditLog();
logEntry.setUserId(userId);
logEntry.setUsername(username);
logEntry.setAction(AuditAction.valueOf(action));
logEntry.setEntityType("USER");
logEntry.setEntityId(userId);
logEntry.setIpAddress(request.getRemoteAddr());
logEntry.setUserAgent(request.getHeader("User-Agent"));
auditLogRepository.save(logEntry);
}
@Transactional
public void logRoleAction(Long roleId, String action, String description) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
AuditLog logEntry = new AuditLog();
logEntry.setUserId(getCurrentUserId());
logEntry.setUsername(username);
logEntry.setAction(AuditAction.valueOf(action));
logEntry.setEntityType("ROLE");
logEntry.setEntityId(roleId);
logEntry.setIpAddress(request.getRemoteAddr());
logEntry.setUserAgent(request.getHeader("User-Agent"));
auditLogRepository.save(logEntry);
}
@Transactional
public void logParameterAction(Long parameterId, String action, String description) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
AuditLog logEntry = new AuditLog();
logEntry.setUserId(getCurrentUserId());
logEntry.setUsername(username);
logEntry.setAction(AuditAction.valueOf(action));
logEntry.setEntityType("PARAMETER");
logEntry.setEntityId(parameterId);
logEntry.setIpAddress(request.getRemoteAddr());
logEntry.setUserAgent(request.getHeader("User-Agent"));
auditLogRepository.save(logEntry);
}
@Transactional(readOnly = true)
public List<AuditLogDTO> getLogsByUser(Long userId) {
return auditLogRepository.findByUserId(userId)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<AuditLogDTO> getLogsByEntity(String entityType, Long entityId) {
return auditLogRepository.findByEntityTypeAndEntityId(entityType, entityId)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<AuditLogDTO> getLogsBetweenDates(LocalDateTime start, LocalDateTime end) {
return auditLogRepository.findByCreatedAtBetween(start, end)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
private Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 实际项目中需要从认证信息中获取用户ID
return 1L; // 简化处理
}
private AuditLogDTO convertToDTO(AuditLog log) {
AuditLogDTO dto = new AuditLogDTO();
dto.setId(log.getId());
dto.setUserId(log.getUserId());
dto.setUsername(log.getUsername());
dto.setAction(log.getAction());
dto.setEntityType(log.getEntityType());
dto.setEntityId(log.getEntityId());
dto.setOldValue(log.getOldValue());
dto.setNewValue(log.getNewValue());
dto.setIpAddress(log.getIpAddress());
dto.setUserAgent(log.getUserAgent());
dto.setCreatedAt(log.getCreatedAt());
return dto;
}
}
Controller层
@RestController
@RequestMapping("/api/admin/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public UserDTO createUser(@Valid @RequestBody UserCreateRequest request) {
return userService.createUser(request);
}
@PutMapping("/{username}")
public UserDTO updateUser(
@PathVariable String username,
@Valid @RequestBody UserUpdateRequest request) {
return userService.updateUser(username, request);
}
@PutMapping("/{username}/status/{status}")
@PreAuthorize("hasRole('ADMIN')")
public UserDTO updateUserStatus(
@PathVariable String username,
@PathVariable String status) {
return userService.updateUserStatus(username, UserStatus.valueOf(status));
}
@PutMapping("/{username}/password")
public void changePassword(
@PathVariable String username,
@RequestParam String newPassword) {
userService.changePassword(username, newPassword);
}
@PutMapping("/{username}/roles")
@PreAuthorize("hasRole('ADMIN')")
public UserDTO assignRoles(
@PathVariable String username,
@RequestBody Set<String> roleCodes) {
return userService.assignRoles(username, roleCodes);
}
@GetMapping("/{username}")
public UserDTO getUser(@PathVariable String username) {
return userService.getUser(username);
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/by-role/{roleCode}")
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> getUsersByRole(@PathVariable String roleCode) {
return userService.getUsersByRole(roleCode);
}
}
@RestController
@RequestMapping("/api/admin/roles")
@RequiredArgsConstructor
public class RoleController {
private final RoleService roleService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public RoleDTO createRole(
@RequestParam String roleCode,
@RequestParam String roleName,
@RequestParam(required = false) String description) {
return roleService.createRole(roleCode, roleName, description);
}
@PutMapping("/{roleCode}")
@PreAuthorize("hasRole('ADMIN')")
public RoleDTO updateRole(
@PathVariable String roleCode,
@RequestParam String roleName,
@RequestParam(required = false) String description) {
return roleService.updateRole(roleCode, roleName, description);
}
@PutMapping("/{roleCode}/permissions")
@PreAuthorize("hasRole('ADMIN')")
public RoleDTO assignPermissions(
@PathVariable String roleCode,
@RequestBody Set<String> permissionCodes) {
return roleService.assignPermissions(roleCode, permissionCodes);
}
@GetMapping("/{roleCode}")
public RoleDTO getRole(@PathVariable String roleCode) {
return roleService.getRole(roleCode);
}
@GetMapping
public List<RoleDTO> getAllRoles() {
return roleService.getAllRoles();
}
}
@RestController
@RequestMapping("/api/admin/parameters")
@RequiredArgsConstructor
public class ParameterController {
private final ParameterService parameterService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ParameterDTO createParameter(
@RequestParam String key,
@RequestParam String name,
@RequestParam String value,
@RequestParam(required = false) String description,
@RequestParam(defaultValue = "true") Boolean isEditable) {
return parameterService.createParameter(key, name, value, description, isEditable);
}
@PutMapping("/{key}")
@PreAuthorize("hasRole('ADMIN')")
public ParameterDTO updateParameter(
@PathVariable String key,
@RequestParam String value,
@RequestParam(required = false) String description) {
return parameterService.updateParameter(key, value, description);
}
@GetMapping("/{key}")
public ParameterDTO getParameter(@PathVariable String key) {
return parameterService.getParameter(key);
}
@GetMapping
public List<ParameterDTO> getAllParameters() {
return parameterService.getAllParameters();
}
}