chore: initial commit
Some checks failed
Synchronize to Gitee / repo-sync (push) Has been cancelled
Typos Checking / Spell Check with Typos (push) Has been cancelled

This commit is contained in:
2026-06-23 11:56:23 +08:00
commit 72e2110987
2883 changed files with 367388 additions and 0 deletions

123
backend/app/pom.xml Normal file
View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.cordys</groupId>
<artifactId>backend</artifactId>
<version>${revision}</version>
</parent>
<artifactId>app</artifactId>
<version>${revision}</version>
<name>app</name>
<dependencies>
<dependency>
<groupId>cn.cordys</groupId>
<artifactId>framework</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.cordys</groupId>
<artifactId>crm</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.json</include>
<include>**/*.tpl</include>
<include>**/*.js</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!-- Spring Boot 插件,支持 Spring Boot 应用的构建 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<loaderImplementation>CLASSIC</loaderImplementation>
</configuration>
</plugin>
<!-- Maven Clean 插件,清理资源文件夹中的静态文件 -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>src/main/resources/static</directory>
<includes>
<include>**</include>
</includes>
<followSymlinks>false</followSymlinks>
</fileset>
</filesets>
</configuration>
</plugin>
<!-- Maven Antrun 插件,用于复制前端资源到静态目录 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>main-class-placement</id>
<phase>generate-resources</phase>
<configuration>
<skip>${skipAntRunForJenkins}</skip>
<target>
<!-- 复制移动端资源到静态目录 -->
<copy todir="src/main/resources/static/mobile" failonerror="false">
<fileset dir="../../frontend/packages/mobile/dist"/>
</copy>
<!-- 复制WEB端资源到静态目录 -->
<copy todir="src/main/resources/static" failonerror="false">
<fileset dir="../../frontend/packages/web/dist"/>
</copy>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Maven Surefire 插件,配置测试执行顺序 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>${argLine} -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,28 @@
package cn.cordys;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.PropertySource;
@SpringBootApplication(exclude = {
QuartzAutoConfiguration.class,
LdapAutoConfiguration.class,
Neo4jAutoConfiguration.class
})
@PropertySource(value = {
"classpath:commons.properties",
"file:/opt/cordys/conf/cordys-crm.properties",
}, encoding = "UTF-8", ignoreResourceNotFound = true)
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,108 @@
package cn.cordys.listener;
import cn.cordys.common.service.DataInitService;
import cn.cordys.common.uid.impl.DefaultUidGenerator;
import cn.cordys.common.util.HikariCPUtils;
import cn.cordys.common.util.JSON;
import cn.cordys.common.util.rsa.RsaKey;
import cn.cordys.common.util.rsa.RsaUtils;
import cn.cordys.crm.system.service.ExportTaskStopService;
import cn.cordys.crm.system.service.ExtScheduleService;
import cn.cordys.crm.system.service.SystemService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
@Slf4j
class AppListener implements ApplicationRunner {
@Resource
private DefaultUidGenerator uidGenerator;
@Resource
private ExtScheduleService extScheduleService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private DataInitService dataInitService;
@Resource
private ExportTaskStopService exportTaskStopService;
@Resource
private SystemService systemService;
/**
* 应用启动后执行的初始化方法。
* <p>
* 此方法会依次初始化唯一 ID 生成器、MinIO 配置和 RSA 配置。
* </p>
*
* @param args 启动参数
*/
@Override
public void run(ApplicationArguments args) {
log.info("===== 开始初始化配置 =====");
// 初始化唯一ID生成器
uidGenerator.init();
// 初始化RSA配置
log.info("初始化RSA配置");
initializeRsaConfiguration();
log.info("初始化定时任务");
extScheduleService.startEnableSchedules();
HikariCPUtils.printHikariCPStatus();
log.info("初始化默认组织数据");
dataInitService.initOneTime();
log.info("停止导出任务");
exportTaskStopService.stopPreparedAll();
log.info("清理表单缓存");
systemService.clearFormCache();
log.info("===== 完成初始化配置 =====");
}
/**
* 初始化 RSA 配置。
* <p>
* 此方法首先尝试加载现有的 RSA 密钥。如果不存在,则生成新的 RSA 密钥并保存到文件系统。
* </p>
*/
private void initializeRsaConfiguration() {
String redisKey = "rsa:key";
try {
// 从 Redis 获取 RSA 密钥
String rsaStr = stringRedisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotBlank(rsaStr)) {
// 如果 RSA 密钥存在,反序列化并设置密钥
RsaKey rsaKey = JSON.parseObject(rsaStr, RsaKey.class);
RsaUtils.setRsaKey(rsaKey);
return;
}
} catch (Exception e) {
log.error("从 Redis 获取 RSA 配置失败", e);
}
try {
// 如果 Redis 中没有密钥,生成新的 RSA 密钥并保存到 Redis
RsaKey rsaKey = RsaUtils.getRsaKey();
stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(rsaKey));
RsaUtils.setRsaKey(rsaKey);
} catch (Exception e) {
log.error("初始化 RSA 配置失败", e);
}
}
}

View File

@@ -0,0 +1,25 @@
package cn.cordys.listener;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 自定义错误控制器类,用于处理应用中的错误页面请求。
* <p>
* 该控制器会将所有错误页面的请求重定向到网站的根页面("/")。
* </p>
*/
@Controller
public class CustomError implements ErrorController {
/**
* 错误处理方法,当发生错误时,会将请求重定向到根页面。
*
* @return 重定向到根页面
*/
@GetMapping("/error")
public String redirectRoot() {
return "redirect:/";
}
}

View File

@@ -0,0 +1,44 @@
package cn.cordys.listener;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 主页控制器类,处理访问根页面("/")和登录页面("/login")的请求。
* <p>
* 该控制器负责将请求转发到 `index.html` 页面。
* </p>
*/
@Controller
public class Index {
/**
* 处理根路径("/")的请求,并返回首页 `index.html` 页面。
*
* @return 返回首页的视图名称
*/
@GetMapping("/web")
public String index() {
return "index.html";
}
/**
* 处理移动端根路径("/")的请求,并返回首页 `/mobile/index.html` 页面。
*
* @return 返回首页的视图名称
*/
@GetMapping("/mobile")
public String mobileIndex() {
return "mobile/index.html";
}
/**
* 处理登录页面("/login")的请求,并返回 `index.html` 页面。
*
* @return 返回登录页面的视图名称
*/
@GetMapping(value = "/login")
public String login() {
return "/index.html";
}
}

View File

@@ -0,0 +1,7 @@
██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ ██████╗██████╗ ███╗ ███╗
██╔════╝██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝██╔════╝ ██╔════╝██╔══██╗████╗ ████║
██║ ██║ ██║██████╔╝██║ ██║ ╚████╔╝ ███████╗ ██║ ██████╔╝██╔████╔██║
██║ ██║ ██║██╔══██╗██║ ██║ ╚██╔╝ ╚════██║ ██║ ██╔══██╗██║╚██╔╝██║
╚██████╗╚██████╔╝██║ ██║██████╔╝ ██║ ███████║ ╚██████╗██║ ██║██║ ╚═╝ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝

View File

@@ -0,0 +1,93 @@
# Application Settings
spring.application.name=cordys-crm
server.port=8081
# Compression Settings (gzip)
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css,text/javascript,image/jpeg
server.compression.min-response-size=2048
# Logging Settings
logging.file.path=/opt/cordys/logs/cordys-crm
# DataSource Configuration (HikariCP)
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=100
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
# Quartz Scheduler DataSource Settings
quartz.enabled=true
quartz.scheduler-name=cordys-crm-quartz
quartz.thread-count=10
quartz.properties.org.quartz.jobStore.acquireTriggersWithinLock=true
spring.datasource.quartz.url=${spring.datasource.url}
spring.datasource.quartz.username=${spring.datasource.username}
spring.datasource.quartz.password=${spring.datasource.password}
spring.datasource.quartz.hikari.maximum-pool-size=50
spring.datasource.quartz.hikari.minimum-idle=10
spring.datasource.quartz.hikari.idle-timeout=300000
spring.datasource.quartz.hikari.auto-commit=true
spring.datasource.quartz.hikari.pool-name=DatebookHikariCP
spring.datasource.quartz.hikari.max-lifetime=1800000
spring.datasource.quartz.hikari.connection-timeout=30000
spring.datasource.quartz.hikari.connection-test-query=SELECT 1
# MyBatis Configuration
mybatis.configuration.cache-enabled=false
mybatis.configuration.lazy-loading-enabled=false
mybatis.configuration.aggressive-lazy-loading=true
mybatis.configuration.use-column-label=true
mybatis.configuration.auto-mapping-behavior=full
mybatis.configuration.default-statement-timeout=25000
mybatis.configuration.map-underscore-to-camel-case=true
# Virtual Thread Settings (for Thread Management)
spring.threads.virtual.enabled=true
spring.mvc.log-request-details=false
# Flyway Database Migration Configuration
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:migration
spring.flyway.table=cordys_crm_version
spring.flyway.baseline-version=0
spring.flyway.encoding=UTF-8
spring.flyway.validate-on-migrate=false
# File Upload Configuration
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
# Redisson (Session Management with Redis)
spring.session.timeout=43200s
spring.session.redis.repository-type=indexed
spring.cache.type=redis
#spring.redis.redisson.file=file:/opt/cordys/conf/redisson.yml
# Template Engines (Freemarker, Groovy)
spring.freemarker.check-template-location=false
spring.groovy.template.check-template-location=false
# Swagger Configuration (API Documentation)
springdoc.swagger-ui.enabled=true
springdoc.api-docs.enabled=true
springdoc.api-docs.groups.enabled=true
# i18n
spring.messages.basename=i18n/cordys-crm
# Enable whitelist functionality, if not enabled, access will not be restricted.
allowed.ip.ranges.enabled=false
# Enable whitelist functionality, if not enabled, access will not be restricted.
allowed.ip.ranges=
# List of URLs that require XSS filtering, supports Ant-style path matching, e.g., /api/**. If no URLs need to be filtered, it can be left empty.
# xss.protection.url.list=/account/follow/**,/announcement/add

View File

@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<property resource="commons.properties"/>
<property file="/opt/cordys/conf/cordys-crm.properties" ignoreResourceNotFound="true"/>
<!-- Console 输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<!-- ===================== 文件 Appender ===================== -->
<appender name="traceAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/trace.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/trace.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="debugAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/debug.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/debug.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="infoAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/info.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/info.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="warnAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/warn.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/warn.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<File>${logging.file.path}/error.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${logging.file.path}/history/error.%d{yyyyMMdd}-%i.log</FileNamePattern>
<maxHistory>${logger.max.history:-30}</maxHistory>
<maxFileSize>50MB</maxFileSize>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<Pattern>%d [%thread] %-5level %logger{36} %line - %msg%n</Pattern>
</encoder>
</appender>
<!-- ===================== Async Appender ===================== -->
<appender name="traceAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<appender-ref ref="traceAppender"/>
</appender>
<appender name="debugAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<appender-ref ref="debugAppender"/>
</appender>
<appender name="infoAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<appender-ref ref="infoAppender"/>
</appender>
<appender name="warnAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<includeCallerData>true</includeCallerData>
<appender-ref ref="warnAppender"/>
</appender>
<appender name="errorAsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<queueSize>10000</queueSize>
<includeCallerData>true</includeCallerData>
<appender-ref ref="errorAppender"/>
</appender>
<!-- ===================== Logger ===================== -->
<!-- cn.cordys 模块日志 -->
<logger name="cn.cordys" additivity="false" level="${logback.level:INFO}">
<appender-ref ref="traceAsyncAppender"/>
<appender-ref ref="debugAsyncAppender"/>
<appender-ref ref="infoAsyncAppender"/>
<appender-ref ref="warnAsyncAppender"/>
<appender-ref ref="errorAsyncAppender"/>
<appender-ref ref="console"/>
</logger>
<!-- cn.cordys.Application 单独 INFO 输出 -->
<logger name="cn.cordys.Application" additivity="false" level="${logback.level:INFO}">
<appender-ref ref="infoAsyncAppender"/>
</logger>
<!-- 容器日志 -->
<logger name="org.eclipse.jetty.ee10.servlet.ServletChannel" level="ERROR"/>
<!-- ===================== Root ===================== -->
<root level="INFO">
<appender-ref ref="console"/>
</root>
</configuration>