chore: initial commit
This commit is contained in:
38
backend/.gitignore
vendored
Normal file
38
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
.DS_Store
|
||||
node_modules
|
||||
node/
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.iml
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
src/main/resources/static
|
||||
src/main/resources/public
|
||||
|
||||
|
||||
target
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
.factorypath
|
||||
/crm/src/main/resources/packages/
|
||||
/app/src/main/resources/static/
|
||||
/crm/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
|
||||
123
backend/app/pom.xml
Normal file
123
backend/app/pom.xml
Normal 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>
|
||||
28
backend/app/src/main/java/cn/cordys/Application.java
Normal file
28
backend/app/src/main/java/cn/cordys/Application.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
backend/app/src/main/java/cn/cordys/listener/AppListener.java
Normal file
108
backend/app/src/main/java/cn/cordys/listener/AppListener.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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:/";
|
||||
}
|
||||
}
|
||||
44
backend/app/src/main/java/cn/cordys/listener/Index.java
Normal file
44
backend/app/src/main/java/cn/cordys/listener/Index.java
Normal 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";
|
||||
}
|
||||
}
|
||||
7
backend/app/src/main/resources/banner.txt
Normal file
7
backend/app/src/main/resources/banner.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ ██████╗██████╗ ███╗ ███╗
|
||||
██╔════╝██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝██╔════╝ ██╔════╝██╔══██╗████╗ ████║
|
||||
██║ ██║ ██║██████╔╝██║ ██║ ╚████╔╝ ███████╗ ██║ ██████╔╝██╔████╔██║
|
||||
██║ ██║ ██║██╔══██╗██║ ██║ ╚██╔╝ ╚════██║ ██║ ██╔══██╗██║╚██╔╝██║
|
||||
╚██████╗╚██████╔╝██║ ██║██████╔╝ ██║ ███████║ ╚██████╗██║ ██║██║ ╚═╝ ██║
|
||||
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
||||
93
backend/app/src/main/resources/commons.properties
Normal file
93
backend/app/src/main/resources/commons.properties
Normal 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
|
||||
185
backend/app/src/main/resources/logback-spring.xml
Normal file
185
backend/app/src/main/resources/logback-spring.xml
Normal 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>
|
||||
34
backend/crm/pom.xml
Normal file
34
backend/crm/pom.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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>crm</artifactId>
|
||||
<version>${revision}</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.cordys</groupId>
|
||||
<artifactId>framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,401 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import cn.cordys.crm.system.dto.field.base.BaseField;
|
||||
import cn.cordys.crm.system.dto.field.base.SubField;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 业务模块字段(定义在主表中,有特定业务含义)(标准字段)
|
||||
*
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-02-18 17:27
|
||||
*/
|
||||
@Getter
|
||||
public enum BusinessModuleField {
|
||||
|
||||
/*------ start: CUSTOMER ------*/
|
||||
/**
|
||||
* 客户名称
|
||||
*/
|
||||
CUSTOMER_NAME("customerName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CUSTOMER.getKey()),
|
||||
/**
|
||||
* 负责人
|
||||
*/
|
||||
CUSTOMER_OWNER("customerOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CUSTOMER.getKey()),
|
||||
/*------ end: CUSTOMER ------*/
|
||||
|
||||
/*------ start: CLUE ------*/
|
||||
/**
|
||||
* 线索名称
|
||||
*/
|
||||
CLUE_NAME("clueName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CLUE.getKey()),
|
||||
/**
|
||||
* 负责人
|
||||
*/
|
||||
CLUE_OWNER("clueOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CLUE.getKey()),
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
CLUE_CONTACT("clueContactName", "contact", Set.of(), FormKey.CLUE.getKey()),
|
||||
/**
|
||||
* 联系人电话
|
||||
*/
|
||||
CLUE_CONTACT_PHONE("clueContactPhone", "phone", Set.of(), FormKey.CLUE.getKey()),
|
||||
/**
|
||||
* 意向产品
|
||||
*/
|
||||
CLUE_PRODUCTS("clueProduct", "products", Set.of(), FormKey.CLUE.getKey()),
|
||||
/*------ end: CUSTOMER ------*/
|
||||
|
||||
/*------ start: CUSTOMER_MANAGEMENT_CONTACT ------*/
|
||||
/**
|
||||
* 联系人客户id
|
||||
*/
|
||||
CUSTOMER_CONTACT_CUSTOMER("contactCustomer", "customerId", Set.of(), FormKey.CONTACT.getKey()),
|
||||
/**
|
||||
* 联系人责任人
|
||||
*/
|
||||
CUSTOMER_CONTACT_OWNER("contactOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTACT.getKey()),
|
||||
/**
|
||||
* 联系人名称
|
||||
*/
|
||||
CUSTOMER_CONTACT_NAME("contactName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTACT.getKey()),
|
||||
/**
|
||||
* 联系人电话
|
||||
*/
|
||||
CUSTOMER_CONTACT_PHONE("contactPhone", "phone", Set.of(), FormKey.CONTACT.getKey()),
|
||||
/*------ end: CUSTOMER_MANAGEMENT_CONTACT ------*/
|
||||
|
||||
|
||||
/*------ start: OPPORTUNITY ------*/
|
||||
/**
|
||||
* 商机名称
|
||||
*/
|
||||
OPPORTUNITY_NAME("opportunityName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.OPPORTUNITY.getKey()),
|
||||
/**
|
||||
* 客户名称
|
||||
*/
|
||||
OPPORTUNITY_CUSTOMER_NAME("opportunityCustomer", "customerId", Set.of(), FormKey.OPPORTUNITY.getKey()),
|
||||
/**
|
||||
* 商机金额
|
||||
*/
|
||||
OPPORTUNITY_AMOUNT("opportunityPrice", "amount", Set.of(), FormKey.OPPORTUNITY.getKey()),
|
||||
/**
|
||||
* 可能性
|
||||
*/
|
||||
OPPORTUNITY_POSSIBLE("opportunityWinRate", "possible", Set.of(), FormKey.OPPORTUNITY.getKey()),
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
OPPORTUNITY_END_TIME("opportunityEndTime", "expectedEndTime", Set.of(), FormKey.OPPORTUNITY.getKey()),
|
||||
/**
|
||||
* 意向产品
|
||||
*/
|
||||
OPPORTUNITY_PRODUCTS("opportunityProduct", "products", Set.of(), FormKey.OPPORTUNITY.getKey()),
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
OPPORTUNITY_CONTACT("opportunityContact", "contactId", Set.of(), FormKey.OPPORTUNITY.getKey()),
|
||||
|
||||
/**
|
||||
* 负责人
|
||||
*/
|
||||
OPPORTUNITY_OWNER("opportunityOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.OPPORTUNITY.getKey()),
|
||||
/*------ end: OPPORTUNITY ------*/
|
||||
|
||||
|
||||
|
||||
/*------ start: FOLLOW_UP_RECORD ------*/
|
||||
/**
|
||||
* 跟进类型
|
||||
*/
|
||||
FOLLOW_RECORD_TYPE("recordType", "type", Set.of("options", "rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 客户id
|
||||
*/
|
||||
FOLLOW_RECORD_CUSTOMER("recordCustomer", "customerId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 商机id
|
||||
*/
|
||||
FOLLOW_RECORD_OPPORTUNITY("recordOpportunity", "opportunityId", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 线索id
|
||||
*/
|
||||
FOLLOW_RECORD_CLUE("recordClue", "clueId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 责任人id
|
||||
*/
|
||||
FOLLOW_RECORD_OWNER("recordOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 联系人id
|
||||
*/
|
||||
FOLLOW_RECORD_CONTACT("recordContact", "contactId", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 跟进内容
|
||||
*/
|
||||
FOLLOW_RECORD_CONTENT("recordDescription", "content", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 跟进时间
|
||||
*/
|
||||
FOLLOW_RECORD_TIME("recordTime", "followTime", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/**
|
||||
* 跟进方式
|
||||
*/
|
||||
FOLLOW_METHOD("recordMethod", "followMethod", Set.of(), FormKey.FOLLOW_RECORD.getKey()),
|
||||
/*------ end: FOLLOW_UP_RECORD ------*/
|
||||
|
||||
|
||||
/*------ start: FOLLOW_UP_PLAN ------*/
|
||||
/**
|
||||
* 跟进类型
|
||||
*/
|
||||
FOLLOW_PLAN_TYPE("planType", "type", Set.of("options", "rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 客户id
|
||||
*/
|
||||
FOLLOW_PLAN_CUSTOMER("planCustomer", "customerId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 商机id
|
||||
*/
|
||||
FOLLOW_PLAN_OPPORTUNITY("planOpportunity", "opportunityId", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 线索id
|
||||
*/
|
||||
FOLLOW_PLAN_CLUE("planClue", "clueId", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 责任人id
|
||||
*/
|
||||
FOLLOW_PLAN_OWNER("planOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 联系人id
|
||||
*/
|
||||
FOLLOW_PLAN_CONTACT("planContact", "contactId", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 预计开始时间
|
||||
*/
|
||||
FOLLOW_PLAN_ESTIMATED_TIME("planStartTime", "estimatedTime", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 预计沟通内容
|
||||
*/
|
||||
FOLLOW_PLAN_CONTENT("planContent", "content", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
|
||||
/**
|
||||
* 跟进方式
|
||||
*/
|
||||
FOLLOW_PLAN_METHOD("planMethod", "method", Set.of(), FormKey.FOLLOW_PLAN.getKey()),
|
||||
|
||||
|
||||
/*------ end: FOLLOW_UP_PLAN ------*/
|
||||
|
||||
/*------ start: PRODUCT ------*/
|
||||
PRODUCT_NAME("productName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.PRODUCT.getKey()),
|
||||
PRODUCT_PRICE("productPrice", "price", Set.of(), FormKey.PRODUCT.getKey()),
|
||||
PRODUCT_STATUS("productStatus", "status", Set.of("rules.required", "mobile", "readable"), FormKey.PRODUCT.getKey()),
|
||||
/*------ end: PRODUCT ------*/
|
||||
|
||||
/**
|
||||
* 价格表单 (修改价格子表格为自定义时, 注意处理对应详情解析逻辑)
|
||||
*/
|
||||
PRICE_NAME("priceName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.PRICE.getKey()),
|
||||
PRICE_STATUS("priceStatus", "status", Set.of("rules.required", "mobile", "readable"), FormKey.PRICE.getKey()),
|
||||
PRICE_PRODUCT_TABLE("priceProducts", "products", Set.of("mobile", "readable"), FormKey.PRICE.getKey()),
|
||||
PRICE_PRODUCT("priceProduct", "product", Set.of("rules.required", "mobile", "dataSourceType", "readable"), FormKey.PRICE.getKey()),
|
||||
PRICE_PRODUCT_AMOUNT("priceProductAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.PRICE.getKey()),
|
||||
|
||||
/**
|
||||
* 报价单表单
|
||||
*/
|
||||
QUOTATION_NAME("quotationName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.QUOTATION.getKey()),
|
||||
QUOTATION_OPPORTUNITY("quotationOpportunity", "opportunityId", Set.of("rules.required", "mobile", "dataSourceType", "readable"), FormKey.QUOTATION.getKey()),
|
||||
QUOTATION_UNTIL_TIME("quotationUntilTime", "untilTime", Set.of("rules.required", "mobile", "readable"), FormKey.QUOTATION.getKey()),
|
||||
QUOTATION_TOTAL_AMOUNT("quotationTotalAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.QUOTATION.getKey()),
|
||||
|
||||
|
||||
/*------ start: CONTRACT_PAYMENT_PLAN ------*/
|
||||
/**
|
||||
* 负责人
|
||||
*/
|
||||
CONTRACT_PAYMENT_PLAN_OWNER("contractPaymentPlanOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
|
||||
/**
|
||||
* 合同
|
||||
*/
|
||||
CONTRACT_PAYMENT_PLAN_CONTRACT("contractPaymentPlanContract", "contractId", Set.of("rules.required", "dataSourceType", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
|
||||
/**
|
||||
* 计划回款金额
|
||||
*/
|
||||
CONTRACT_PAYMENT_PLAN_PLAN_AMOUNT("contractPaymentPlanPlanAmount", "planAmount", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
|
||||
/**
|
||||
* 计划回款时间
|
||||
*/
|
||||
CONTRACT_PAYMENT_PLAN_PLAN_END_TIME("contractPaymentPlanPlanEndTime", "planEndTime", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
|
||||
/**
|
||||
* 回款计划名称
|
||||
*/
|
||||
CONTRACT_PAYMENT_PLAN_NAME("contractPaymentPlanName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_PLAN.getKey()),
|
||||
/*------ end: CONTRACT_PAYMENT_PLAN ------*/
|
||||
|
||||
|
||||
/*------ start: CONTRACT ------*/
|
||||
/**
|
||||
* 合同名稱
|
||||
*/
|
||||
CONTRACT_NAME("contractName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT.getKey()),
|
||||
|
||||
CONTRACT_CUSTOMER_NAME("contractCustomer", "customerId", Set.of("rules.required", "mobile", "readable", "dataSourceType"), FormKey.CONTRACT.getKey()),
|
||||
CONTRACT_OWNER("contractOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT.getKey()),
|
||||
CONTRACT_NO("contractNo", "number", Set.of("rules.required"), FormKey.CONTRACT.getKey()),
|
||||
CONTRACT_START_TIME("contractStartTime", "startTime", Set.of("mobile", "readable"), FormKey.CONTRACT.getKey()),
|
||||
CONTRACT_END_TIME("contractEndTime", "endTime", Set.of("mobile", "readable"), FormKey.CONTRACT.getKey()),
|
||||
CONTRACT_TOTAL_AMOUNT("contractTotalAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT.getKey()),
|
||||
|
||||
/*------ end: CONTRACT ------*/
|
||||
|
||||
/**
|
||||
* 发票
|
||||
*/
|
||||
/*------ start: CONTRACT_INVOICE ------*/
|
||||
INVOICE_NAME("invoiceName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.INVOICE.getKey()),
|
||||
INVOICE_OWNER("invoiceOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.INVOICE.getKey()),
|
||||
INVOICE_AMOUNT("invoiceAmount", "amount", Set.of("rules.required", "readable", "mobile"), FormKey.INVOICE.getKey()),
|
||||
INVOICE_CONTRACT_ID("invoiceContract", "contractId", Set.of("rules.required", "mobile", "readable", "dataSourceType"), FormKey.INVOICE.getKey()),
|
||||
INVOICE_INVOICE_TYPE("invoiceType", "invoiceType", Set.of("rules.required", "readable", "mobile"), FormKey.INVOICE.getKey()),
|
||||
INVOICE_TAX_RATE("invoiceTaxRate", "taxRate", Set.of("rules.required", "readable", "mobile"), FormKey.INVOICE.getKey()),
|
||||
INVOICE_BUSINESS_TITLE_ID("invoiceBusinessTitle", "businessTitleId", Set.of("rules.required", "mobile", "readable", "dataSourceType"), FormKey.INVOICE.getKey()),
|
||||
/*------ end: CONTRACT_INVOICE ------*/
|
||||
|
||||
|
||||
/*------ start: CONTRACT_PAYMENT_RECORD 合同回款记录 ------*/
|
||||
CONTRACT_PAYMENT_RECORD_NO("contractPaymentRecordNo", "no", Set.of("rules.required"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
CONTRACT_PAYMENT_RECORD_NAME("contractPaymentRecordName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
CONTRACT_PAYMENT_RECORD_OWNER("contractPaymentRecordOwner", "owner", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
CONTRACT_PAYMENT_RECORD_CONTRACT("contractPaymentRecordContract", "contractId", Set.of("rules.required", "dataSourceType", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
CONTRACT_PAYMENT_RECORD_PLAN("contractPaymentRecordPlan", "paymentPlanId", Set.of("dataSourceType", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
CONTRACT_PAYMENT_RECORD_AMOUNT("contractPaymentRecordAmount", "recordAmount", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
CONTRACT_PAYMENT_RECORD_END_TIME("contractPaymentRecordEndTime", "recordEndTime", Set.of("rules.required", "mobile", "readable"), FormKey.CONTRACT_PAYMENT_RECORD.getKey()),
|
||||
/*------ end: CONTRACT_PAYMENT_RECORD 合同回款记录 ------*/
|
||||
|
||||
/*------ start: ORDER ------*/
|
||||
ORDER_NAME("orderName", "name", Set.of("rules.required", "mobile", "readable"), FormKey.ORDER.getKey()),
|
||||
ORDER_CUSTOMER("orderCustomer", "customerId", Set.of("dataSourceType"), FormKey.ORDER.getKey()),
|
||||
ORDER_CONTRACT("orderContract", "contractId", Set.of("dataSourceType"), FormKey.ORDER.getKey()),
|
||||
ORDER_OWNER("orderOwner", "owner", Set.of(), FormKey.ORDER.getKey()),
|
||||
ORDER_NO("orderNo", "number", Set.of("rules.required"), FormKey.ORDER.getKey()),
|
||||
ORDER_TOTAL_AMOUNT("orderAmount", "amount", Set.of("rules.required", "mobile", "readable"), FormKey.ORDER.getKey()),
|
||||
/*------ end: ORDER ------*/
|
||||
|
||||
|
||||
/*------ start: ORDER ------*/
|
||||
CUSTOM_FORM_DATA_NAME("customFormDataName", "name", Set.of("rules.required", "mobile", "readable"), null),
|
||||
CUSTOM_FORM_DATA_OWNER("customFormDataNOwner", "owner", Set.of("rules.required", "mobile", "readable"), null),
|
||||
/*------ end: ORDER ------*/
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* 业务字段缓存
|
||||
*/
|
||||
private static final Map<String, BusinessModuleField> INTERNAL_CACHE = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (BusinessModuleField field : values()) {
|
||||
// 防止ofKey方法频繁调用
|
||||
INTERNAL_CACHE.put(field.key, field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段 key,field.json 中的 internalKey
|
||||
*/
|
||||
private final String key;
|
||||
/**
|
||||
* 业务字段 key
|
||||
*/
|
||||
private final String businessKey;
|
||||
/**
|
||||
* 禁止修改的参数列表
|
||||
*/
|
||||
private final Set<String> disabledProps;
|
||||
/**
|
||||
* 表单 key
|
||||
*/
|
||||
private final String formKey;
|
||||
|
||||
BusinessModuleField(String key, String businessKey, Set<String> disabledProps, String formKey) {
|
||||
this.key = key;
|
||||
this.businessKey = businessKey;
|
||||
this.disabledProps = disabledProps;
|
||||
this.formKey = formKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断业务字段是否被删除
|
||||
*
|
||||
* @param formKey 表单 key
|
||||
* @param fields 字段集合
|
||||
* @return 是否被删除
|
||||
*/
|
||||
public static boolean isBusinessDeleted(String formKey, List<BaseField> fields) {
|
||||
List<BusinessModuleField> formBusinessFields;
|
||||
if (FormKey.ofKey(formKey) == null) {
|
||||
// 如果不是内置表单,则校验自定义表单字段必须要名字和负责人
|
||||
formBusinessFields = List.of(BusinessModuleField.CUSTOM_FORM_DATA_NAME, BusinessModuleField.CUSTOM_FORM_DATA_OWNER);
|
||||
} else {
|
||||
formBusinessFields = Arrays.stream(BusinessModuleField.values()).filter(field -> Strings.CS.equals(formKey, field.getFormKey())).toList();
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(formBusinessFields)) {
|
||||
return false;
|
||||
}
|
||||
return formBusinessFields.stream()
|
||||
.anyMatch(businessField ->
|
||||
fields.stream().noneMatch(field -> Strings.CS.equals(businessField.getKey(), field.getInternalKey()))
|
||||
&& businessField.noneMatchOfSubFields(fields)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断子表字段中是否存在业务字段
|
||||
*
|
||||
* @param fields 字段集合
|
||||
* @return 是否存在业务字段
|
||||
*/
|
||||
private boolean noneMatchOfSubFields(List<BaseField> fields) {
|
||||
boolean noneMatch = true;
|
||||
for (BaseField field : fields) {
|
||||
if (field instanceof SubField subField && CollectionUtils.isNotEmpty(subField.getSubFields())) {
|
||||
noneMatch = subField.getSubFields().stream().noneMatch(sub -> Strings.CS.equals(this.getKey(), sub.getInternalKey()));
|
||||
if (!noneMatch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return noneMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有重复的字段
|
||||
*
|
||||
* @param fields 字段集合
|
||||
* @return 是否有重复的字段
|
||||
*/
|
||||
public static boolean hasRepeatName(List<BaseField> fields) {
|
||||
return fields.stream()
|
||||
.collect(Collectors.groupingBy(BaseField::getName, Collectors.counting()))
|
||||
.values().stream()
|
||||
.anyMatch(count -> count > 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过Key查询业务字段
|
||||
*
|
||||
* @param internalKey 业务key
|
||||
* @return 业务字段
|
||||
*/
|
||||
public static BusinessModuleField ofKey(String internalKey) {
|
||||
return INTERNAL_CACHE.get(internalKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* 客户,线索,商机等业务数据的搜索类型
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
public enum BusinessSearchType {
|
||||
/**
|
||||
* 全部数据
|
||||
*/
|
||||
ALL,
|
||||
/**
|
||||
* 负责人是我的数据
|
||||
*/
|
||||
SELF,
|
||||
/**
|
||||
* 有数据权限的部门的数据
|
||||
*/
|
||||
DEPARTMENT
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-14 10:11
|
||||
*/
|
||||
public enum ChartAggregateMethod {
|
||||
SUM,
|
||||
AVG,
|
||||
COUNT,
|
||||
MAX,
|
||||
MIN
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import cn.cordys.common.exception.IResultCode;
|
||||
|
||||
/**
|
||||
* 通用功能状态码
|
||||
* 通用功能返回的状态码
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
public enum CommonResultCode implements IResultCode {
|
||||
|
||||
FIELD_VALIDATE_ERROR(100001, "field_validate_error"),
|
||||
FIELD_OPTION_VALUE_ERROR(100002, "field_option_value_error"),
|
||||
APPROVAL_NOT_ENABLED_ERROR(100003, "approval.not.enabled");
|
||||
|
||||
|
||||
private final int code;
|
||||
private final String message;
|
||||
|
||||
CommonResultCode(int code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
|
||||
@Getter
|
||||
public enum FormKey {
|
||||
|
||||
/**
|
||||
* 线索
|
||||
*/
|
||||
CLUE("clue"),
|
||||
/**
|
||||
* 客户
|
||||
*/
|
||||
CUSTOMER("customer"),
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
CONTACT("contact"),
|
||||
/**
|
||||
* 跟进记录
|
||||
*/
|
||||
FOLLOW_RECORD("record"),
|
||||
/**
|
||||
* 跟进计划
|
||||
*/
|
||||
FOLLOW_PLAN("plan"),
|
||||
/**
|
||||
* 商机
|
||||
*/
|
||||
OPPORTUNITY("opportunity"),
|
||||
/**
|
||||
* 产品
|
||||
*/
|
||||
PRODUCT("product"),
|
||||
/**
|
||||
* 价格
|
||||
*/
|
||||
PRICE("price"),
|
||||
/**
|
||||
* 报价单
|
||||
*/
|
||||
QUOTATION("quotation"),
|
||||
/**
|
||||
* 合同
|
||||
*/
|
||||
CONTRACT("contract"),
|
||||
/**
|
||||
* 发票
|
||||
*/
|
||||
INVOICE("invoice"),
|
||||
/**
|
||||
* 合同回款计划
|
||||
*/
|
||||
CONTRACT_PAYMENT_PLAN("contractPaymentPlan"),
|
||||
/**
|
||||
* 回款记录
|
||||
*/
|
||||
CONTRACT_PAYMENT_RECORD("contractPaymentRecord"),
|
||||
/**
|
||||
* 订单
|
||||
*/
|
||||
ORDER("order");
|
||||
|
||||
private final String key;
|
||||
|
||||
FormKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public static List<String> allKeys() {
|
||||
return Arrays.stream(FormKey.values()).map(FormKey::getKey).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static FormKey ofKey(String key) {
|
||||
for (FormKey formKey : FormKey.values()) {
|
||||
if (Strings.CI.equals(formKey.getKey(), key)) {
|
||||
return formKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasSnapshot() {
|
||||
return Strings.CI.equalsAny(this.key, CONTRACT.getKey(), INVOICE.getKey(), QUOTATION.getKey(), ORDER.getKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
|
||||
@Getter
|
||||
public class FormKeyConstants {
|
||||
|
||||
public static final String ORDER = "order";
|
||||
public static final String CLUE = "clue";
|
||||
public static final String CUSTOMER = "customer";
|
||||
public static final String CONTRACT = "contract";
|
||||
public static final String CONTRACT_INVOICE = "contractInvoice";
|
||||
public static final String CONTRACT_PAYMENT_PLAN = "contractPaymentPlan";
|
||||
public static final String CONTRACT_PAYMENT_RECORD = "contractPaymentRecord";
|
||||
public static final String OPPORTUNITY = "opportunity";
|
||||
public static final String QUOTATION = "quotation";
|
||||
public static final String FOLLOW_PLAN = "plan";
|
||||
public static final String FOLLOW_RECORD = "record";
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
public enum HttpMethodConstants {
|
||||
GET,
|
||||
HEAD,
|
||||
POST,
|
||||
PUT,
|
||||
PATCH,
|
||||
DELETE,
|
||||
OPTIONS,
|
||||
TRACE,
|
||||
CONNECT
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 系统内置角色ID
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
@Getter
|
||||
public enum InternalRole {
|
||||
ORG_ADMIN("org_admin"),
|
||||
SALES_MANAGER("sales_manager"),
|
||||
SALES_STAFF("sales_staff");
|
||||
|
||||
private final String value;
|
||||
|
||||
InternalRole(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-07-25 14:05
|
||||
*/
|
||||
public enum InternalUserView {
|
||||
/**
|
||||
* 全部视图
|
||||
*/
|
||||
ALL,
|
||||
/**
|
||||
* 个人视图
|
||||
*/
|
||||
SELF,
|
||||
/**
|
||||
* 部门视图
|
||||
*/
|
||||
DEPARTMENT,
|
||||
/**
|
||||
* 协作客户视图
|
||||
*/
|
||||
CUSTOMER_COLLABORATION,
|
||||
/**
|
||||
* 赢单视图
|
||||
*/
|
||||
OPPORTUNITY_SUCCESS;
|
||||
|
||||
public static final String CURRENT_USER = "CURRENT_USER";
|
||||
|
||||
|
||||
public static List<String> getCurrentUserArrayValue() {
|
||||
List<String> values = new ArrayList<>(0);
|
||||
values.add(CURRENT_USER);
|
||||
return values;
|
||||
}
|
||||
|
||||
public static boolean isInternalUserView(String viewId) {
|
||||
if (StringUtils.isBlank(viewId)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.stream(InternalUserView.values())
|
||||
.map(InternalUserView::name)
|
||||
.collect(Collectors.toSet()).contains(viewId);
|
||||
}
|
||||
|
||||
public static boolean isAll(String searchType) {
|
||||
return Strings.CS.equals(ALL.name(), searchType);
|
||||
}
|
||||
|
||||
public static boolean isSelf(String searchType) {
|
||||
return Strings.CS.equals(SELF.name(), searchType);
|
||||
}
|
||||
|
||||
public static boolean isDepartment(String searchType) {
|
||||
return Strings.CS.equals(DEPARTMENT.name(), searchType);
|
||||
}
|
||||
|
||||
public static boolean isVisible(String searchType) {
|
||||
return Strings.CS.equals(CUSTOMER_COLLABORATION.name(), searchType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* 联动场景
|
||||
*
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
public enum LinkScenarioKey {
|
||||
|
||||
/**
|
||||
* 线索转客户
|
||||
*/
|
||||
CLUE_TO_CUSTOMER,
|
||||
/**
|
||||
* 线索转联系人
|
||||
*/
|
||||
CLUE_TO_CONTACT,
|
||||
/**
|
||||
* 线索转商机
|
||||
*/
|
||||
CLUE_TO_OPPORTUNITY,
|
||||
/**
|
||||
* 客户转商机
|
||||
*/
|
||||
CUSTOMER_TO_OPPORTUNITY,
|
||||
/**
|
||||
* 线索转记录
|
||||
*/
|
||||
CLUE_TO_RECORD,
|
||||
/**
|
||||
* 客户转记录
|
||||
*/
|
||||
CUSTOMER_TO_RECORD,
|
||||
/**
|
||||
* 商机转记录
|
||||
*/
|
||||
OPPORTUNITY_TO_RECORD,
|
||||
/**
|
||||
* 计划转记录
|
||||
*/
|
||||
PLAN_TO_RECORD,
|
||||
/**
|
||||
* 合同转发票
|
||||
*/
|
||||
CONTRACT_TO_INVOICE,
|
||||
/**
|
||||
* 合同转订单
|
||||
*/
|
||||
CONTRACT_TO_ORDER,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum ModuleKey {
|
||||
|
||||
/**
|
||||
* 首页
|
||||
*/
|
||||
HOME("home"),
|
||||
/**
|
||||
* 线索管理
|
||||
*/
|
||||
CLUE("clue"),
|
||||
/**
|
||||
* 客户管理
|
||||
*/
|
||||
CUSTOMER("customer"),
|
||||
/**
|
||||
* 商机管理
|
||||
*/
|
||||
BUSINESS("business"),
|
||||
/**
|
||||
* 产品管理
|
||||
*/
|
||||
PRODUCT("product"),
|
||||
/**
|
||||
* 系统设置
|
||||
*/
|
||||
SETTING("setting");
|
||||
|
||||
|
||||
/**
|
||||
* *******************************************
|
||||
* 注意:
|
||||
* 新增菜单不要在moduleKey中添加了
|
||||
* *******************************************
|
||||
*/
|
||||
|
||||
private final String key;
|
||||
|
||||
ModuleKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
* @date 2025-01-03 11:31:40
|
||||
*/
|
||||
public class PermissionConstants {
|
||||
|
||||
/*------ start: SYSTEM_ROLE ------*/
|
||||
public static final String SYSTEM_ROLE_READ = "SYSTEM_ROLE:READ";
|
||||
public static final String SYSTEM_ROLE_ADD = "SYSTEM_ROLE:ADD";
|
||||
public static final String SYSTEM_ROLE_UPDATE = "SYSTEM_ROLE:UPDATE";
|
||||
public static final String SYSTEM_ROLE_DELETE = "SYSTEM_ROLE:DELETE";
|
||||
public static final String SYSTEM_ROLE_ADD_USER = "SYSTEM_ROLE:ADD_USER";
|
||||
public static final String SYSTEM_ROLE_REMOVE_USER = "SYSTEM_ROLE:REMOVE_USER";
|
||||
/*------ end: SYSTEM_ROLE------*/
|
||||
|
||||
|
||||
/*------ start: OPERATION_LOG ------*/
|
||||
public static final String OPERATION_LOG_READ = "OPERATION_LOG:READ";
|
||||
/*------ end: OPERATION_LOG ------*/
|
||||
|
||||
/*------ start: SYSTEM_NOTICE ------*/
|
||||
public static final String SYSTEM_NOTICE_READ = "SYSTEM_NOTICE:READ";
|
||||
public static final String SYSTEM_NOTICE_ADD = "SYSTEM_NOTICE:ADD";
|
||||
public static final String SYSTEM_NOTICE_UPDATE = "SYSTEM_NOTICE:UPDATE";
|
||||
public static final String SYSTEM_NOTICE_DELETE = "SYSTEM_NOTICE:DELETE";
|
||||
/*------ end: SYSTEM_NOTICE ------*/
|
||||
|
||||
/*------ start: SYS_DEPARTMENT ------*/
|
||||
public static final String SYS_ORGANIZATION_READ = "SYS_ORGANIZATION:READ";
|
||||
public static final String SYS_ORGANIZATION_ADD = "SYS_ORGANIZATION:ADD";
|
||||
public static final String SYS_ORGANIZATION_UPDATE = "SYS_ORGANIZATION:UPDATE";
|
||||
public static final String SYS_ORGANIZATION_DELETE = "SYS_ORGANIZATION:DELETE";
|
||||
public static final String SYS_ORGANIZATION_IMPORT = "SYS_ORGANIZATION:IMPORT";
|
||||
public static final String SYS_ORGANIZATION_SYNC = "SYS_ORGANIZATION:SYNC";
|
||||
public static final String SYS_ORGANIZATION_USER_RESET_PASSWORD = "SYS_ORGANIZATION_USER:RESET_PASSWORD";
|
||||
|
||||
/*------ end: SYS_DEPARTMENT ------*/
|
||||
|
||||
/*------ start: SYSTEM_SETTING ------*/
|
||||
public static final String SYSTEM_SETTING_READ = "SYSTEM_SETTING:READ";
|
||||
public static final String SYSTEM_SETTING_UPDATE = "SYSTEM_SETTING:UPDATE";
|
||||
public static final String SYSTEM_SETTING_ADD = "SYSTEM_SETTING:ADD";
|
||||
public static final String SYSTEM_SETTING_DELETE = "SYSTEM_SETTING:DELETE";
|
||||
/*------ end: SYSTEM_SETTING ------*/
|
||||
|
||||
/**
|
||||
* module setting permission
|
||||
*/
|
||||
public static final String MODULE_SETTING_READ = "MODULE_SETTING:READ";
|
||||
public static final String MODULE_SETTING_UPDATE = "MODULE_SETTING:UPDATE";
|
||||
|
||||
|
||||
/*------ start: CUSTOMER_MANAGEMENT------*/
|
||||
public static final String CUSTOMER_MANAGEMENT_READ = "CUSTOMER_MANAGEMENT:READ";
|
||||
public static final String CUSTOMER_MANAGEMENT_ADD = "CUSTOMER_MANAGEMENT:ADD";
|
||||
public static final String CUSTOMER_MANAGEMENT_UPDATE = "CUSTOMER_MANAGEMENT:UPDATE";
|
||||
public static final String CUSTOMER_MANAGEMENT_TRANSFER = "CUSTOMER_MANAGEMENT:TRANSFER";
|
||||
public static final String CUSTOMER_MANAGEMENT_RECYCLE = "CUSTOMER_MANAGEMENT:RECYCLE";
|
||||
public static final String CUSTOMER_MANAGEMENT_DELETE = "CUSTOMER_MANAGEMENT:DELETE";
|
||||
public static final String CUSTOMER_MANAGEMENT_EXPORT = "CUSTOMER_MANAGEMENT:EXPORT";
|
||||
public static final String CUSTOMER_MANAGEMENT_IMPORT = "CUSTOMER_MANAGEMENT:IMPORT";
|
||||
public static final String CUSTOMER_MANAGEMENT_MERGE = "CUSTOMER_MANAGEMENT:MERGE";
|
||||
/*------ end: CUSTOMER_MANAGEMENT ------*/
|
||||
|
||||
|
||||
/*------ start: CUSTOMER_MANAGEMENT_POOL ------*/
|
||||
public static final String CUSTOMER_MANAGEMENT_POOL_READ = "CUSTOMER_MANAGEMENT_POOL:READ";
|
||||
public static final String CUSTOMER_MANAGEMENT_POOL_UPDATE = "CUSTOMER_MANAGEMENT_POOL:UPDATE";
|
||||
public static final String CUSTOMER_MANAGEMENT_POOL_DELETE = "CUSTOMER_MANAGEMENT_POOL:DELETE";
|
||||
public static final String CUSTOMER_MANAGEMENT_POOL_PICK = "CUSTOMER_MANAGEMENT_POOL:PICK";
|
||||
public static final String CUSTOMER_MANAGEMENT_POOL_ASSIGN = "CUSTOMER_MANAGEMENT_POOL:ASSIGN";
|
||||
public static final String CUSTOMER_MANAGEMENT_POOL_EXPORT = "CUSTOMER_MANAGEMENT_POOL:EXPORT";
|
||||
/*------ end: CUSTOMER_MANAGEMENT_POOL ------*/
|
||||
|
||||
|
||||
/*------ start: CUSTOMER_MANAGEMENT_CONTACT ------*/
|
||||
public static final String CUSTOMER_MANAGEMENT_CONTACT_READ = "CUSTOMER_MANAGEMENT_CONTACT:READ";
|
||||
public static final String CUSTOMER_MANAGEMENT_CONTACT_ADD = "CUSTOMER_MANAGEMENT_CONTACT:ADD";
|
||||
public static final String CUSTOMER_MANAGEMENT_CONTACT_UPDATE = "CUSTOMER_MANAGEMENT_CONTACT:UPDATE";
|
||||
public static final String CUSTOMER_MANAGEMENT_CONTACT_DELETE = "CUSTOMER_MANAGEMENT_CONTACT:DELETE";
|
||||
public static final String CUSTOMER_MANAGEMENT_CONTACT_EXPORT = "CUSTOMER_MANAGEMENT_CONTACT:EXPORT";
|
||||
public static final String CUSTOMER_MANAGEMENT_CONTACT_IMPORT = "CUSTOMER_MANAGEMENT_CONTACT:IMPORT";
|
||||
/*------ end: CUSTOMER_MANAGEMENT_CONTACT ------*/
|
||||
|
||||
/*------ start: PRODUCT_MANAGEMENT ------*/
|
||||
public static final String PRODUCT_MANAGEMENT_READ = "PRODUCT_MANAGEMENT:READ";
|
||||
public static final String PRODUCT_MANAGEMENT_ADD = "PRODUCT_MANAGEMENT:ADD";
|
||||
public static final String PRODUCT_MANAGEMENT_UPDATE = "PRODUCT_MANAGEMENT:UPDATE";
|
||||
public static final String PRODUCT_MANAGEMENT_DELETE = "PRODUCT_MANAGEMENT:DELETE";
|
||||
public static final String PRODUCT_MANAGEMENT_IMPORT = "PRODUCT_MANAGEMENT:IMPORT";
|
||||
/*------ end: PRODUCT_MANAGEMENT ------*/
|
||||
|
||||
/*------ start: OPPORTUNITY_MANAGEMENT ------*/
|
||||
public static final String OPPORTUNITY_MANAGEMENT_READ = "OPPORTUNITY_MANAGEMENT:READ";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_ADD = "OPPORTUNITY_MANAGEMENT:ADD";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_UPDATE = "OPPORTUNITY_MANAGEMENT:UPDATE";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_TRANSFER = "OPPORTUNITY_MANAGEMENT:TRANSFER";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_DELETE = "OPPORTUNITY_MANAGEMENT:DELETE";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_EXPORT = "OPPORTUNITY_MANAGEMENT:EXPORT";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_RESIGN = "OPPORTUNITY_MANAGEMENT:RESIGN";
|
||||
public static final String OPPORTUNITY_MANAGEMENT_IMPORT = "OPPORTUNITY_MANAGEMENT:IMPORT";
|
||||
/*------ end: OPPORTUNITY_MANAGEMENT ------*/
|
||||
|
||||
|
||||
/**
|
||||
* clue permission
|
||||
*/
|
||||
/*------ start: CLUE_MANAGEMENT ------*/
|
||||
public static final String CLUE_MANAGEMENT_READ = "CLUE_MANAGEMENT:READ";
|
||||
public static final String CLUE_MANAGEMENT_ADD = "CLUE_MANAGEMENT:ADD";
|
||||
public static final String CLUE_MANAGEMENT_UPDATE = "CLUE_MANAGEMENT:UPDATE";
|
||||
public static final String CLUE_MANAGEMENT_TRANSFER = "CLUE_MANAGEMENT:TRANSFER";
|
||||
public static final String CLUE_MANAGEMENT_RECYCLE = "CLUE_MANAGEMENT:RECYCLE";
|
||||
public static final String CLUE_MANAGEMENT_DELETE = "CLUE_MANAGEMENT:DELETE";
|
||||
public static final String CLUE_MANAGEMENT_EXPORT = "CLUE_MANAGEMENT:EXPORT";
|
||||
public static final String CLUE_MANAGEMENT_IMPORT = "CLUE_MANAGEMENT:IMPORT";
|
||||
/*------ end: CLUE_MANAGEMENT ------*/
|
||||
|
||||
|
||||
/*------ start: CLUE_MANAGEMENT_POOL ------*/
|
||||
public static final String CLUE_MANAGEMENT_POOL_READ = "CLUE_MANAGEMENT_POOL:READ";
|
||||
public static final String CLUE_MANAGEMENT_POOL_DELETE = "CLUE_MANAGEMENT_POOL:DELETE";
|
||||
public static final String CLUE_MANAGEMENT_POOL_PICK = "CLUE_MANAGEMENT_POOL:PICK";
|
||||
public static final String CLUE_MANAGEMENT_POOL_ASSIGN = "CLUE_MANAGEMENT_POOL:ASSIGN";
|
||||
public static final String CLUE_MANAGEMENT_POOL_UPDATE = "CLUE_MANAGEMENT_POOL:UPDATE";
|
||||
public static final String CLUE_MANAGEMENT_POOL_EXPORT = "CLUE_MANAGEMENT_POOL:EXPORT";
|
||||
/*------ end: CLUE_MANAGEMENT_POOL ------*/
|
||||
|
||||
/**
|
||||
* dashboard permission
|
||||
*/
|
||||
public static final String DASHBOARD_READ = "DASHBOARD:READ";
|
||||
public static final String DASHBOARD_ADD = "DASHBOARD:ADD";
|
||||
public static final String DASHBOARD_EDIT = "DASHBOARD:UPDATE";
|
||||
public static final String DASHBOARD_DELETE = "DASHBOARD:DELETE";
|
||||
|
||||
/*------ start: LICENSE ------*/
|
||||
public static final String LICENSE_READ = "LICENSE:READ";
|
||||
public static final String LICENSE_EDIT = "LICENSE:EDIT";
|
||||
/*------ end: LICENSE ------*/
|
||||
|
||||
|
||||
/*------ start: PERSON INFO ------*/
|
||||
public static final String PERSONAL_API_KEY_READ = "PERSONAL_API_KEY:READ";
|
||||
public static final String PERSONAL_API_KEY_ADD = "PERSONAL_API_KEY:ADD";
|
||||
public static final String PERSONAL_API_KEY_UPDATE = "PERSONAL_API_KEY:UPDATE";
|
||||
public static final String PERSONAL_API_KEY_DELETE = "PERSONAL_API_KEY:DELETE";
|
||||
/*------ end: PERSON INFO ------*/
|
||||
|
||||
|
||||
/*------ start: AGENT ------*/
|
||||
public static final String AGENT_READ = "AGENT:READ";
|
||||
public static final String AGENT_ADD = "AGENT:ADD";
|
||||
public static final String AGENT_UPDATE = "AGENT:UPDATE";
|
||||
public static final String AGENT_DELETE = "AGENT:DELETE";
|
||||
/*------ end: AGENT ------*/
|
||||
|
||||
/**
|
||||
* product price permission
|
||||
*/
|
||||
public static final String PRICE_READ = "PRICE:READ";
|
||||
public static final String PRICE_ADD = "PRICE:ADD";
|
||||
public static final String PRICE_UPDATE = "PRICE:UPDATE";
|
||||
public static final String PRICE_DELETE = "PRICE:DELETE";
|
||||
public static final String PRICE_IMPORT = "PRICE:IMPORT";
|
||||
public static final String PRICE_EXPORT = "PRICE:EXPORT";
|
||||
|
||||
|
||||
/*------ start: OPPORTUNITY_QUOTATION ------*/
|
||||
|
||||
public static final String OPPORTUNITY_QUOTATION_READ = "OPPORTUNITY_QUOTATION:READ";
|
||||
public static final String OPPORTUNITY_QUOTATION_ADD = "OPPORTUNITY_QUOTATION:ADD";
|
||||
public static final String OPPORTUNITY_QUOTATION_UPDATE = "OPPORTUNITY_QUOTATION:UPDATE";
|
||||
public static final String OPPORTUNITY_QUOTATION_DELETE = "OPPORTUNITY_QUOTATION:DELETE";
|
||||
public static final String OPPORTUNITY_QUOTATION_DOWNLOAD = "OPPORTUNITY_QUOTATION:DOWNLOAD";
|
||||
public static final String OPPORTUNITY_QUOTATION_VOIDED = "OPPORTUNITY_QUOTATION:VOIDED";
|
||||
public static final String OPPORTUNITY_QUOTATION_APPROVAL = "OPPORTUNITY_QUOTATION:APPROVAL";
|
||||
|
||||
/*------ end: OPPORTUNITY_QUOTATION ------*/
|
||||
|
||||
/*------ start: CONTRACT ------*/
|
||||
public static final String CONTRACT_READ = "CONTRACT:READ";
|
||||
public static final String CONTRACT_ADD = "CONTRACT:ADD";
|
||||
public static final String CONTRACT_UPDATE = "CONTRACT:UPDATE";
|
||||
public static final String CONTRACT_DELETE = "CONTRACT:DELETE";
|
||||
public static final String CONTRACT_EXPORT = "CONTRACT:EXPORT";
|
||||
public static final String CONTRACT_APPROVAL = "CONTRACT:APPROVAL";
|
||||
public static final String CONTRACT_STAGE = "CONTRACT:STAGE";
|
||||
public static final String CONTRACT_PAYMENT = "CONTRACT:PAYMENT";
|
||||
|
||||
/*------ end: CONTRACT ------*/
|
||||
|
||||
/*------ start: CONTRACT_CONTRACT_PAYMENT_PLAN_ROLE ------*/
|
||||
public static final String CONTRACT_PAYMENT_PLAN_READ = "CONTRACT_PAYMENT_PLAN:READ";
|
||||
public static final String CONTRACT_PAYMENT_PLAN_ADD = "CONTRACT_PAYMENT_PLAN:ADD";
|
||||
public static final String CONTRACT_PAYMENT_PLAN_UPDATE = "CONTRACT_PAYMENT_PLAN:UPDATE";
|
||||
public static final String CONTRACT_PAYMENT_PLAN_DELETE = "CONTRACT_PAYMENT_PLAN:DELETE";
|
||||
/*------ end: CONTRACT_CONTRACT_PAYMENT_PLAN_ROLE ------*/
|
||||
|
||||
|
||||
/*------ start: TENDER ------*/
|
||||
public static final String TENDER_READ = "TENDER:READ";
|
||||
/*------ end: TENDER ------*/
|
||||
|
||||
|
||||
/*------ start: CONTRACT_INVOICE_ROLE ------*/
|
||||
public static final String CONTRACT_INVOICE_READ = "CONTRACT_INVOICE:READ";
|
||||
public static final String CONTRACT_INVOICE_ADD = "CONTRACT_INVOICE:ADD";
|
||||
public static final String CONTRACT_INVOICE_UPDATE = "CONTRACT_INVOICE:UPDATE";
|
||||
public static final String CONTRACT_INVOICE_EXPORT = "CONTRACT_INVOICE:EXPORT";
|
||||
public static final String CONTRACT_INVOICE_APPROVAL = "CONTRACT_INVOICE:APPROVAL";
|
||||
public static final String CONTRACT_INVOICE_DELETE = "CONTRACT_INVOICE:DELETE";
|
||||
/*------ end: CONTRACT_INVOICE_ROLE ------*/
|
||||
|
||||
/*------ start: BUSINESS_TITLE ------*/
|
||||
public static final String CONTRACT_BUSINESS_TITLE_READ = "CONTRACT_BUSINESS_TITLE:READ";
|
||||
public static final String CONTRACT_BUSINESS_TITLE_ADD = "CONTRACT_BUSINESS_TITLE:ADD";
|
||||
public static final String CONTRACT_BUSINESS_TITLE_UPDATE = "CONTRACT_BUSINESS_TITLE:UPDATE";
|
||||
public static final String CONTRACT_BUSINESS_TITLE_DELETE = "CONTRACT_BUSINESS_TITLE:DELETE";
|
||||
public static final String CONTRACT_BUSINESS_TITLE_EXPORT = "CONTRACT_BUSINESS_TITLE:EXPORT";
|
||||
public static final String CONTRACT_BUSINESS_TITLE_APPROVAL = "CONTRACT_BUSINESS_TITLE:APPROVAL";
|
||||
public static final String CONTRACT_BUSINESS_TITLE_IMPORT = "CONTRACT_BUSINESS_TITLE:IMPORT";
|
||||
|
||||
/*------ end: BUSINESS_TITLE ------*/
|
||||
|
||||
/**
|
||||
* Contract payment record permission
|
||||
*/
|
||||
public static final String CONTRACT_PAYMENT_RECORD_READ = "CONTRACT_PAYMENT_RECORD:READ";
|
||||
public static final String CONTRACT_PAYMENT_RECORD_ADD = "CONTRACT_PAYMENT_RECORD:ADD";
|
||||
public static final String CONTRACT_PAYMENT_RECORD_UPDATE = "CONTRACT_PAYMENT_RECORD:UPDATE";
|
||||
public static final String CONTRACT_PAYMENT_RECORD_DELETE = "CONTRACT_PAYMENT_RECORD:DELETE";
|
||||
public static final String CONTRACT_PAYMENT_RECORD_IMPORT = "CONTRACT_PAYMENT_RECORD:IMPORT";
|
||||
public static final String CONTRACT_PAYMENT_RECORD_EXPORT = "CONTRACT_PAYMENT_RECORD:EXPORT";
|
||||
|
||||
/*------ start: ORDER_ROLE ------*/
|
||||
public static final String ORDER_READ = "ORDER:READ";
|
||||
public static final String ORDER_ADD = "ORDER:ADD";
|
||||
public static final String ORDER_UPDATE = "ORDER:UPDATE";
|
||||
public static final String ORDER_DELETE = "ORDER:DELETE";
|
||||
public static final String ORDER_DOWNLOAD = "ORDER:DOWNLOAD";
|
||||
/*------ end: ORDER_ROLE ------*/
|
||||
|
||||
/*------ start: PROCESS_SETTING ------*/
|
||||
public static final String PROCESS_SETTING_READ = "PROCESS_SETTING:READ";
|
||||
public static final String PROCESS_SETTING_ADD = "PROCESS_SETTING:ADD";
|
||||
public static final String PROCESS_SETTING_UPDATE = "PROCESS_SETTING:UPDATE";
|
||||
public static final String PROCESS_SETTING_DELETE = "PROCESS_SETTING:DELETE";
|
||||
/*------ end: PROCESS_SETTING ------*/
|
||||
|
||||
/*------ start: CUSTOM_FORM ------*/
|
||||
public static final String CUSTOM_FORM_READ = "CUSTOM_FORM:READ";
|
||||
public static final String CUSTOM_FORM_ADD = "CUSTOM_FORM:ADD";
|
||||
/*------ end: CUSTOM_FORM ------*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* 角色的数据权限范围
|
||||
*
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-01-07 16:42
|
||||
*/
|
||||
public enum RoleDataScope {
|
||||
/**
|
||||
* 全部数据权限
|
||||
*/
|
||||
ALL,
|
||||
/**
|
||||
* 指定部门数据权限
|
||||
*/
|
||||
DEPT_CUSTOM,
|
||||
/**
|
||||
* 部门及以下数据权限
|
||||
*/
|
||||
DEPT_AND_CHILD,
|
||||
/**
|
||||
* 仅本人数据权限
|
||||
*/
|
||||
SELF
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
public class RuleValidatorConstants {
|
||||
|
||||
/**
|
||||
* 必填
|
||||
*/
|
||||
public static final String REQUIRED = "required";
|
||||
/**
|
||||
* 唯一
|
||||
*/
|
||||
public static final String UNIQUE = "unique";
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* 部门来源类型
|
||||
*/
|
||||
public enum ThirdConfigTypeConstants {
|
||||
|
||||
/**
|
||||
* 本地
|
||||
*/
|
||||
INTERNAL,
|
||||
/**
|
||||
* 企业微信
|
||||
*/
|
||||
WECOM,
|
||||
/**
|
||||
* 钉钉
|
||||
*/
|
||||
DINGTALK,
|
||||
/**
|
||||
* 飞书
|
||||
*/
|
||||
LARK,
|
||||
/**
|
||||
* DE
|
||||
*/
|
||||
DE,
|
||||
/**
|
||||
* SQLBOT
|
||||
*/
|
||||
SQLBOT,
|
||||
/**
|
||||
* maxKB
|
||||
*/
|
||||
MAXKB,
|
||||
/**
|
||||
* tender
|
||||
*/
|
||||
TENDER,
|
||||
/**
|
||||
* 企查查
|
||||
*/
|
||||
QCC;
|
||||
|
||||
public static ThirdConfigTypeConstants fromString(String type) {
|
||||
try {
|
||||
return ThirdConfigTypeConstants.valueOf(type.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
public enum ThirdDetailType {
|
||||
WECOM_SYNC, DINGTALK_SYNC, LARK_SYNC, DE_BOARD, SQLBOT_CHAT, SQLBOT_BOARD, MAXKB, TENDER, QCC
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TopicConstants {
|
||||
/**
|
||||
* 下载任务的 Redis 主题名称
|
||||
*/
|
||||
public static final String DOWNLOAD_TOPIC = "download-topic";
|
||||
|
||||
/**
|
||||
* sse 消息通知的Redis 主题名称
|
||||
*/
|
||||
public static final String SSE_TOPIC = "sse-topic";
|
||||
|
||||
/**
|
||||
* 所有 Redis 主题的集合
|
||||
* 用于统一管理订阅和发布的主题
|
||||
*/
|
||||
public static final List<String> ALL_TOPICS = List.of(DOWNLOAD_TOPIC, SSE_TOPIC);
|
||||
|
||||
private TopicConstants() {
|
||||
// 私有构造函数,防止实例化
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.cordys.common.constants;
|
||||
|
||||
/**
|
||||
* 用户来源类型枚举类,用于标识用户的来源。
|
||||
* <p>
|
||||
* 此枚举类定义了不同的用户来源类型,包括本地、LDAP、CAS、OIDC、OAuth2 和二维码。
|
||||
* </p>
|
||||
*/
|
||||
public enum UserSource {
|
||||
|
||||
/**
|
||||
* 本地用户来源,表示用户通过本地系统注册和登录。
|
||||
*/
|
||||
LOCAL,
|
||||
|
||||
/**
|
||||
* LDAP 用户来源,表示用户通过 LDAP(轻量目录访问协议)系统认证。
|
||||
*/
|
||||
LDAP,
|
||||
|
||||
/**
|
||||
* CAS 用户来源,表示用户通过 CAS(中央认证服务)认证。
|
||||
*/
|
||||
CAS,
|
||||
|
||||
/**
|
||||
* OIDC 用户来源,表示用户通过 OIDC(开放ID连接)认证。
|
||||
*/
|
||||
OIDC,
|
||||
|
||||
/**
|
||||
* OAUTH2 用户来源,表示用户通过 企业微信OAUTH2 授权框架认证。
|
||||
*/
|
||||
WECOM_OAUTH2,
|
||||
|
||||
/**
|
||||
* OAUTH2 用户来源,表示用户通过 GitHub OAUTH2 授权框架认证。
|
||||
*/
|
||||
GITHUB_OAUTH2,
|
||||
|
||||
/**
|
||||
* 二维码用户来源,表示用户通过扫描二维码登录。
|
||||
*/
|
||||
QR_CODE,
|
||||
|
||||
/**
|
||||
* OAUTH2 用户来源,表示用户通过 钉钉OAUTH2 授权框架认证。
|
||||
*/
|
||||
DINGTALK_OAUTH2,
|
||||
|
||||
/**
|
||||
* OAUTH2 用户来源,表示用户通过 飞书OAUTH2 授权框架认证。
|
||||
*/
|
||||
LARK_OAUTH2
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cn.cordys.common.context;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CustomFunction<T, R> {
|
||||
R apply(T t) throws InterruptedException;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cn.cordys.common.context;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ExportTaskFunction {
|
||||
void apply() throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.cordys.common.context;
|
||||
|
||||
import cn.cordys.context.OrganizationContext;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 组织信息及请求来源的 Web 过滤器
|
||||
* <p>
|
||||
* 根据请求头自动设置组织上下文与请求来源,并在请求结束时清理资源。
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
public class OrganizationContextWebFilter extends OncePerRequestFilter {
|
||||
|
||||
public static final String ORGANIZATION_ID_HEADER = "Organization-Id";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
// 提取所有头信息
|
||||
String organizationId = request.getHeader(ORGANIZATION_ID_HEADER);
|
||||
|
||||
// 设置组织 ID
|
||||
if (StringUtils.isNotBlank(organizationId)) {
|
||||
OrganizationContext.setOrganizationId(organizationId);
|
||||
}
|
||||
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
// 保证上下文清理,避免内存泄漏
|
||||
OrganizationContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.cordys.common.context;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据源详情解析上下文
|
||||
*
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
public class SourceDetailResolveContext {
|
||||
|
||||
private static final ThreadLocal<Map<String, Map<String, Object>>> CONTEXT =
|
||||
ThreadLocal.withInitial(HashMap::new);
|
||||
private static final ThreadLocal<Integer> DEPTH =
|
||||
ThreadLocal.withInitial(() -> 0);
|
||||
|
||||
public static Map<String, Map<String, Object>> getSourceMap() {
|
||||
return CONTEXT.get();
|
||||
}
|
||||
|
||||
public static boolean contains(String sourceId) {
|
||||
return CONTEXT.get().containsKey(sourceId);
|
||||
}
|
||||
|
||||
public static void putPlaceholder(String sourceId) {
|
||||
CONTEXT.get().putIfAbsent(sourceId, new HashMap<>(8));
|
||||
}
|
||||
|
||||
public static void put(String sourceId, Map<String, Object> detail) {
|
||||
CONTEXT.get().put(sourceId, detail);
|
||||
}
|
||||
|
||||
public static void start() {
|
||||
DEPTH.set(DEPTH.get() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前深度
|
||||
*
|
||||
* @return 当前深度
|
||||
*/
|
||||
public static int getDepth() {
|
||||
return DEPTH.get();
|
||||
}
|
||||
|
||||
public static void end() {
|
||||
int depth = DEPTH.get() - 1;
|
||||
if (depth <= 0) {
|
||||
clear();
|
||||
DEPTH.remove();
|
||||
} else {
|
||||
DEPTH.set(depth);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CONTEXT.remove();
|
||||
}
|
||||
|
||||
public static void remove(String sourceId) {
|
||||
CONTEXT.get().remove(sourceId);
|
||||
}
|
||||
|
||||
private SourceDetailResolveContext() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.cordys.common.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class BaseModel implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String id;
|
||||
@Schema(description = "创建人")
|
||||
private String createUser;
|
||||
@Schema(description = "修改人")
|
||||
private String updateUser;
|
||||
@Schema(description = "创建时间")
|
||||
private Long createTime;
|
||||
@Schema(description = "更新时间")
|
||||
private Long updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.cordys.common.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BaseModuleFieldValue implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "自定义属性id")
|
||||
private String fieldId;
|
||||
|
||||
/**
|
||||
* 可能是数组
|
||||
*/
|
||||
@Schema(description = "自定义属性值")
|
||||
private Object fieldValue;
|
||||
|
||||
public boolean valid() {
|
||||
return switch (fieldValue) {
|
||||
case null -> false;
|
||||
case String fieldValueStr when StringUtils.isBlank(fieldValueStr) -> false;
|
||||
case List fieldValueList when CollectionUtils.isEmpty(fieldValueList) -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.cordys.common.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BaseResourceField extends BaseModuleFieldValue {
|
||||
@Schema(description = "ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "资源ID")
|
||||
private String resourceId;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.cordys.common.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BaseResourceSubField extends BaseResourceField {
|
||||
|
||||
@Schema(description = "关联子表格ID")
|
||||
private String refSubId;
|
||||
|
||||
@Schema(description = "行ID")
|
||||
private String rowId;
|
||||
|
||||
@Schema(description = "行唯一标识")
|
||||
private String bizId;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.dto.condition.BaseCondition;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* <p>表示分页请求的 DTO 类,继承自 {@link BaseCondition} 类,包含了分页参数和排序字段。</p>
|
||||
* <p>用于分页查询时传递当前页码、每页条数和排序信息。</p>
|
||||
*/
|
||||
@Data
|
||||
public class BasePageRequest extends BaseCondition {
|
||||
|
||||
/**
|
||||
* 当前页码,最小值为 1
|
||||
*/
|
||||
@Min(value = 1, message = "当前页码必须大于0")
|
||||
@Schema(description = "当前页码")
|
||||
private int current;
|
||||
|
||||
/**
|
||||
* 每页显示条数,范围为 1 到 500
|
||||
*/
|
||||
@Min(value = 1, message = "每页显示条数必须不小于1")
|
||||
@Max(value = 500, message = "每页显示条数不能大于500")
|
||||
@Schema(description = "每页显示条数")
|
||||
private int pageSize;
|
||||
|
||||
@Valid
|
||||
@Schema(description = "排序字段")
|
||||
private SortRequest sort;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.util.JSON;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-09-25 11:37
|
||||
*/
|
||||
@Data
|
||||
public class BatchUpdateDbParam {
|
||||
private List<String> ids;
|
||||
private String fieldName;
|
||||
private Object fieldValue;
|
||||
private String updateUser;
|
||||
private Long updateTime;
|
||||
|
||||
public Object getFieldValue() {
|
||||
if (fieldValue != null && fieldValue instanceof List) {
|
||||
return JSON.toJSONString(fieldValue);
|
||||
}
|
||||
return fieldValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BusinessDataPermission extends DeptDataPermissionDTO {
|
||||
|
||||
@Schema(description = "数据来源表")
|
||||
private String sourceTable;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.dto.chart.ChartCategoryAxisDbParam;
|
||||
import cn.cordys.common.dto.chart.ChartValueAxisDbParam;
|
||||
import cn.cordys.common.dto.condition.CombineSearch;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:34
|
||||
*/
|
||||
@Data
|
||||
public class ChartAnalysisDbRequest extends ChartAnalysisRequest {
|
||||
|
||||
@Schema(description = "过滤条件")
|
||||
private CombineSearch viewFilterCondition;
|
||||
|
||||
/**
|
||||
* x轴查询参数
|
||||
*/
|
||||
private ChartCategoryAxisDbParam categoryAxisParam;
|
||||
|
||||
/**
|
||||
* x轴子类别查询参数
|
||||
*/
|
||||
private ChartCategoryAxisDbParam subCategoryAxisParam;
|
||||
|
||||
/**
|
||||
* y轴查询参数
|
||||
*/
|
||||
private ChartValueAxisDbParam valueAxisParam;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.dto.chart.ChartConfig;
|
||||
import cn.cordys.common.dto.condition.CombineSearch;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:34
|
||||
*/
|
||||
@Data
|
||||
public class ChartAnalysisRequest {
|
||||
|
||||
@Schema(description = "视图ID")
|
||||
private String viewId;
|
||||
|
||||
@Schema(description = "过滤条件")
|
||||
@Valid
|
||||
private CombineSearch filterCondition;
|
||||
|
||||
@Schema(description = "搜索条件,支持组合搜索")
|
||||
@NotNull
|
||||
@Valid
|
||||
private ChartConfig chartConfig;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.constants.InternalUserView;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门的数据权限
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
public class DeptDataPermissionDTO {
|
||||
|
||||
/**
|
||||
* 搜索类型(ALL/SELF/DEPARTMENT/VISIBLE)
|
||||
* {@link InternalUserView}
|
||||
*/
|
||||
private String viewId;
|
||||
/**
|
||||
* 是否可查看全部数据
|
||||
*/
|
||||
private Boolean all = false;
|
||||
/**
|
||||
* 是否可查看自己的数据
|
||||
*/
|
||||
private Boolean self = false;
|
||||
/**
|
||||
* 是否被设置为可见
|
||||
*/
|
||||
private Boolean visible = false;
|
||||
/**
|
||||
* 可查看的部门Id
|
||||
*/
|
||||
private Set<String> deptIds = new HashSet<>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.domain.BaseModuleFieldValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class EnableFieldValue extends BaseModuleFieldValue {
|
||||
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Boolean enable;
|
||||
|
||||
public boolean valid() {
|
||||
return super.valid() && BooleanUtils.isTrue(enable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ExportDTO {
|
||||
private String userId;
|
||||
private String orgId;
|
||||
/**
|
||||
* {@link cn.cordys.crm.system.constants.ExportConstants.ExportType}
|
||||
*/
|
||||
private String exportType;
|
||||
private String logModule;
|
||||
private Locale locale;
|
||||
private String fileName;
|
||||
private List<ExportHeadDTO> headList;
|
||||
private DeptDataPermissionDTO deptDataPermission;
|
||||
private BasePageRequest pageRequest;
|
||||
private List<String> selectIds;
|
||||
private ExportSelectRequest selectRequest;
|
||||
private String formKey;
|
||||
/**
|
||||
* 导出字段参数 (通用参数无需设置)
|
||||
*/
|
||||
private ExportFieldParam exportFieldParam;
|
||||
private List<String> mergeHeads;
|
||||
private List<FieldExportMeta> exportMetas;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.crm.system.dto.field.base.BaseField;
|
||||
import cn.cordys.crm.system.dto.response.ModuleFormConfigDTO;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class ExportFieldParam {
|
||||
|
||||
/**
|
||||
* 子表格ID集合
|
||||
*/
|
||||
private Set<String> subIds;
|
||||
|
||||
/**
|
||||
* 字段配置
|
||||
*/
|
||||
private Map<String, BaseField> fieldConfigMap;
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
private ModuleFormConfigDTO formConfig;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExportHeadDTO {
|
||||
|
||||
@Schema(description = "key")
|
||||
private String key;
|
||||
@Schema(description = "表头名称")
|
||||
private String title;
|
||||
@Schema(description = "字段类型")
|
||||
private String columnType;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ExportSelectRequest {
|
||||
|
||||
@Schema(description = "文件名")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "表头信息")
|
||||
@NotEmpty(message = "{export_head_list_is_empty}")
|
||||
private List<ExportHeadDTO> headList;
|
||||
|
||||
@Schema(description = "勾选的数据id集合")
|
||||
@NotEmpty(message = "{export_select_ids_is_empty}")
|
||||
private List<String> ids;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.resolver.field.AbstractModuleFieldResolver;
|
||||
import cn.cordys.crm.system.dto.field.base.BaseField;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 导出字段元数据 (预处理)
|
||||
* @author song-cc-rock
|
||||
*/
|
||||
@Data
|
||||
public class FieldExportMeta {
|
||||
|
||||
private String head;
|
||||
|
||||
private BaseField field;
|
||||
|
||||
private AbstractModuleFieldResolver<?> resolver;
|
||||
|
||||
private boolean noResource;
|
||||
|
||||
private String fieldId;
|
||||
|
||||
private String businessKey;
|
||||
|
||||
private String prefixId;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RedisMessage {
|
||||
/**
|
||||
* redis 发布订阅消息主体
|
||||
*/
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 客户、商机、线索是否显示所有数据和部门数据 tab
|
||||
*
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-05-15 14:54
|
||||
*/
|
||||
@Data
|
||||
public class ResourceTabEnableDTO {
|
||||
@Schema(description = "是否显示所有数据tab")
|
||||
private Boolean all = false;
|
||||
@Schema(description = "是否显示部门数据tab")
|
||||
private Boolean dept = false;
|
||||
|
||||
/**
|
||||
* 合并权限
|
||||
*
|
||||
* @param other 其余数据权限配置
|
||||
*
|
||||
* @return 合并后的数据权限配置
|
||||
*/
|
||||
public ResourceTabEnableDTO or(ResourceTabEnableDTO other) {
|
||||
if (other != null) {
|
||||
all |= other.all;
|
||||
dept |= other.dept;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import cn.cordys.common.utils.SqlInjectionChecker;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SortRequest {
|
||||
|
||||
@Pattern(regexp = "^[A-Za-z0-9]+$")
|
||||
@Schema(description = "排序字段")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序类型(asc/desc)")
|
||||
private String type;
|
||||
|
||||
public static String camelToUnderline(String camelCase) {
|
||||
if (camelCase == null || camelCase.isEmpty()) {
|
||||
return camelCase;
|
||||
}
|
||||
|
||||
// 使用正则表达式将驼峰转换为下划线
|
||||
String underline = camelCase.replaceAll("([A-Z])", "_$1").toLowerCase();
|
||||
|
||||
// 如果开头有下划线,去掉
|
||||
if (underline.startsWith("_")) {
|
||||
underline = underline.substring(1);
|
||||
}
|
||||
|
||||
return underline;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (SqlInjectionChecker.containsSqlInjectionRisk(name)) {
|
||||
return "1";
|
||||
}
|
||||
return camelToUnderline(name);
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
if (Strings.CI.equals(type, "asc")) {
|
||||
return "asc";
|
||||
} else {
|
||||
return "desc";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mapper 中调用
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean valid() {
|
||||
return StringUtils.isNotBlank(name) && !SqlInjectionChecker.containsSqlInjectionRisk(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.cordys.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDeptDTO {
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "部门ID")
|
||||
private String deptId;
|
||||
|
||||
@Schema(description = "部门名称")
|
||||
private String deptName;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.cordys.common.dto.chart;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:54
|
||||
*/
|
||||
@Data
|
||||
public class ChartCategoryAxisConfig {
|
||||
@NotBlank
|
||||
@Schema(description = "字段ID")
|
||||
private String fieldId;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.cordys.common.dto.chart;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:54
|
||||
*/
|
||||
@Data
|
||||
public class ChartCategoryAxisDbParam extends ChartCategoryAxisConfig {
|
||||
/**
|
||||
* 是否要查blob表
|
||||
*/
|
||||
private Boolean blob = false;
|
||||
/**
|
||||
* 是否是业务字段
|
||||
*/
|
||||
private Boolean businessField = false;
|
||||
/**
|
||||
* 业务字段名称
|
||||
*/
|
||||
private String businessFieldName;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cn.cordys.common.dto.chart;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:34
|
||||
*/
|
||||
@Data
|
||||
public class ChartConfig {
|
||||
|
||||
@Schema(description = "图表类型")
|
||||
private String chatType;
|
||||
|
||||
@Schema(description = "类别轴配置")
|
||||
@NotNull
|
||||
@Valid
|
||||
private ChartCategoryAxisConfig categoryAxis;
|
||||
|
||||
@Schema(description = "子类别轴配置")
|
||||
@Valid
|
||||
private ChartCategoryAxisConfig subCategoryAxis;
|
||||
|
||||
@Schema(description = "值轴配置")
|
||||
@NotNull
|
||||
@Valid
|
||||
private ChartValueAxisConfig valueAxis;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.cordys.common.dto.chart;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-15 11:40
|
||||
*/
|
||||
@Data
|
||||
public class ChartResult {
|
||||
private String categoryAxis;
|
||||
private String categoryAxisName;
|
||||
private String subCategoryAxis;
|
||||
private String subCategoryAxisName;
|
||||
private Object valueAxis;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.cordys.common.dto.chart;
|
||||
|
||||
import cn.cordys.common.constants.ChartAggregateMethod;
|
||||
import cn.cordys.common.constants.EnumValue;
|
||||
import cn.cordys.common.uid.utils.EnumUtils;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:54
|
||||
*/
|
||||
@Data
|
||||
public class ChartValueAxisConfig {
|
||||
@Schema(description = "字段ID")
|
||||
private String fieldId;
|
||||
|
||||
@EnumValue(enumClass = ChartAggregateMethod.class)
|
||||
@Schema(description = "聚合方式")
|
||||
private String aggregateMethod;
|
||||
|
||||
public String getAggregateMethod() {
|
||||
if (this.aggregateMethod == null) {
|
||||
return ChartAggregateMethod.COUNT.name();
|
||||
}
|
||||
// 避免mapper中sql注入
|
||||
return EnumUtils.valueOf(ChartAggregateMethod.class, this.aggregateMethod).name();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package cn.cordys.common.dto.chart;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-10-13 14:54
|
||||
*/
|
||||
@Data
|
||||
public class ChartValueAxisDbParam extends ChartValueAxisConfig {
|
||||
/**
|
||||
* 是否要查blob表
|
||||
*/
|
||||
private Boolean blob = false;
|
||||
/**
|
||||
* 是否是业务字段
|
||||
*/
|
||||
private Boolean businessField = false;
|
||||
|
||||
/**
|
||||
* 业务字段名称
|
||||
*/
|
||||
private String businessFieldName;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package cn.cordys.common.dto.condition;
|
||||
|
||||
import cn.cordys.common.utils.ConditionFilterUtils;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 表示 CRM 系统中的基础条件类,用于支持过滤和搜索操作。
|
||||
*/
|
||||
@Data
|
||||
public class BaseCondition {
|
||||
|
||||
@Schema(description = "视图ID")
|
||||
private String viewId;
|
||||
|
||||
@Schema(description = "关键字,用于搜索匹配")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "筛选条件列表,用于定义多个搜索条件")
|
||||
@Valid
|
||||
private List<FilterCondition> filters;
|
||||
|
||||
@Schema(description = "高级搜索条件,支持组合搜索")
|
||||
@Valid
|
||||
private CombineSearch combineSearch;
|
||||
|
||||
private CombineSearch viewCombineSearch;
|
||||
|
||||
/**
|
||||
* 转义关键字中的特殊字符。
|
||||
*
|
||||
* @param keyword 输入的关键字
|
||||
*
|
||||
* @return 转义后的关键字
|
||||
*/
|
||||
public static String transferKeyword(String keyword) {
|
||||
if (Strings.CS.contains(keyword, "\\") && !Strings.CS.contains(keyword, "\\\\")) {
|
||||
keyword = Strings.CS.replace(keyword, "\\", "\\\\");
|
||||
}
|
||||
// 判断是否已经转义过,未转义才进行转义。
|
||||
if (Strings.CS.contains(keyword, "%") && !Strings.CS.contains(keyword, "\\%")) {
|
||||
keyword = Strings.CS.replace(keyword, "%", "\\%");
|
||||
}
|
||||
if (Strings.CS.contains(keyword, "_") && !Strings.CS.contains(keyword, "\\_")) {
|
||||
keyword = Strings.CS.replace(keyword, "_", "\\_");
|
||||
}
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public CombineSearch getCombineSearch() {
|
||||
return combineSearch == null ? new CombineSearch() : combineSearch;
|
||||
}
|
||||
|
||||
public List<FilterCondition> getFilters() {
|
||||
return ConditionFilterUtils.getValidConditions(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化关键字,直接设置字段值。
|
||||
*
|
||||
* @param keyword 初始化的关键字
|
||||
*/
|
||||
public void initKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.cordys.common.dto.condition;
|
||||
|
||||
import cn.cordys.common.constants.EnumValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 表示组合搜索条件,用于支持复杂的搜索逻辑。
|
||||
* 包含匹配模式(所有/任一)和筛选条件列表。
|
||||
*/
|
||||
@Data
|
||||
public class CombineSearch {
|
||||
|
||||
@Schema(description = "匹配模式,支持“所有”或“任一”", allowableValues = {"AND", "OR"})
|
||||
@EnumValue(enumClass = SearchMode.class)
|
||||
private String searchMode = SearchMode.AND.name();
|
||||
|
||||
@Schema(description = "筛选条件列表,用于定义多个搜索条件")
|
||||
@Valid
|
||||
private List<FilterCondition> conditions;
|
||||
|
||||
public List<FilterCondition> getConditions() {
|
||||
if (CollectionUtils.isEmpty(conditions)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return conditions.stream()
|
||||
.filter(FilterCondition::valid)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的匹配模式。如果未设置,则默认返回 "AND"。
|
||||
*
|
||||
* @return 当前的匹配模式
|
||||
*/
|
||||
public String getSearchMode() {
|
||||
return StringUtils.isBlank(searchMode) ? SearchMode.AND.name() : searchMode;
|
||||
}
|
||||
|
||||
public CombineSearch convert() {
|
||||
if (CollectionUtils.isEmpty(conditions)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
Iterator<FilterCondition> iterator = conditions.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
FilterCondition condition = iterator.next();
|
||||
if (!condition.valid()) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = condition.getCombineValue();
|
||||
boolean isBetween = Strings.CS.equals(condition.getCombineOperator(), FilterCondition.CombineConditionOperator.BETWEEN.name());
|
||||
|
||||
if (value instanceof List<?> valueList) {
|
||||
if (CollectionUtils.isEmpty(valueList)) {
|
||||
/*
|
||||
* 兜底处理, 防止前端[EMPTY, NOT_EMPTY]条件产生脏数据导致报错
|
||||
*/
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
// 多值处理
|
||||
if (!condition.expectMulti()) {
|
||||
condition.setValue(valueList.getFirst());
|
||||
}
|
||||
if (isBetween) {
|
||||
Object first = valueList.getFirst();
|
||||
condition.setValue(List.of(first, first));
|
||||
}
|
||||
} else {
|
||||
// 单值处理
|
||||
if (condition.expectMulti()) {
|
||||
if (isBetween) {
|
||||
condition.setValue(List.of(value, value));
|
||||
} else {
|
||||
condition.setValue(List.of(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举:搜索模式,定义了“所有”与“任一”两种匹配模式。
|
||||
*/
|
||||
public enum SearchMode {
|
||||
/**
|
||||
* 所有条件都匹配(“与”操作)
|
||||
*/
|
||||
AND,
|
||||
|
||||
/**
|
||||
* 任一条件匹配(“或”操作)
|
||||
*/
|
||||
OR
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
package cn.cordys.common.dto.condition;
|
||||
|
||||
import cn.cordys.common.constants.EnumValue;
|
||||
import cn.cordys.common.exception.GenericException;
|
||||
import cn.cordys.common.utils.SqlInjectionChecker;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 表示组合条件,用于支持复杂的过滤和查询逻辑。
|
||||
* 包含字段名、操作符和期望值等信息。
|
||||
*/
|
||||
@Data
|
||||
public class FilterCondition {
|
||||
|
||||
/**
|
||||
* 系统字段为字段名
|
||||
* 模块字段为字段ID
|
||||
*/
|
||||
@Schema(description = "条件的参数名称")
|
||||
@NotNull
|
||||
private String name;
|
||||
|
||||
@Schema(description = "期望值,若操作符为 BETWEEN, IN, NOT_IN 时为数组,其他操作符为单个值")
|
||||
private Object value;
|
||||
|
||||
@Schema(description = "是否是多选值")
|
||||
@NotNull
|
||||
private Boolean multipleValue = false;
|
||||
|
||||
@Schema(description = "操作符",
|
||||
allowableValues = {"IN", "NOT_IN", "BETWEEN", "GT", "LT", "GE", "LE", "COUNT_GT", "COUNT_LT", "EQUALS", "NOT_EQUALS", "CONTAINS", "NOT_CONTAINS", "EMPTY", "NOT_EMPTY"})
|
||||
@EnumValue(enumClass = CombineConditionOperator.class)
|
||||
private String operator;
|
||||
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "包含新增子部门集合")
|
||||
private List<String> containChildIds;
|
||||
|
||||
public String getName() {
|
||||
if (SqlInjectionChecker.containsSqlInjectionRisk(name)) {
|
||||
throw new GenericException("condition name illegal");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验条件是否合法,检查字段名称、操作符和值的有效性。
|
||||
*
|
||||
* @return 如果条件合法则返回 true,否则返回 false
|
||||
*/
|
||||
public boolean valid() {
|
||||
if (StringUtils.isBlank(name) || StringUtils.isBlank(operator) || SqlInjectionChecker.containsSqlInjectionRisk(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 针对空值判断操作符
|
||||
if (Strings.CS.equalsAny(operator, CombineConditionOperator.EMPTY.name(), CombineConditionOperator.NOT_EMPTY.name(), CombineConditionOperator.NOT_EQUAL_ORIGINAL.name())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 针对值为集合类型的校验
|
||||
if (value instanceof List<?> valueList && CollectionUtils.isEmpty(valueList)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 针对值为字符串的校验
|
||||
return !(value instanceof String valueStr) || !StringUtils.isBlank(valueStr);
|
||||
}
|
||||
|
||||
public boolean expectMulti() {
|
||||
return Strings.CS.equalsAny(operator, CombineConditionOperator.IN.name(), CombineConditionOperator.NOT_IN.name(), CombineConditionOperator.BETWEEN.name(), CombineConditionOperator.DYNAMICS.name());
|
||||
}
|
||||
|
||||
|
||||
public Object getCombineValue() {
|
||||
if (Strings.CI.equals(operator, CombineConditionOperator.DYNAMICS.name())) {
|
||||
// value 转为string 类型
|
||||
String strValue = (String) value;
|
||||
String[] split = strValue.split(",");
|
||||
if (split.length == 1) {
|
||||
String dateValue = split[0];
|
||||
switch (dateValue) {
|
||||
case "TODAY" -> {
|
||||
List<Long> todayList = new ArrayList<>();
|
||||
// 获取今天的日期
|
||||
LocalDate today = LocalDate.now();
|
||||
long timestamp = getTimestamp(today.atStartOfDay());
|
||||
todayList.add(timestamp);
|
||||
long timestampEnd = getTimestamp(today.atTime(23, 59, 59, 999_000_000));
|
||||
todayList.add(timestampEnd);
|
||||
return todayList;
|
||||
}
|
||||
case "YESTERDAY" -> {
|
||||
List<Long> yesterdayList = new ArrayList<>();
|
||||
LocalDate yesterday = LocalDate.now().minusDays(1);
|
||||
long timestamp = getTimestamp(yesterday.atStartOfDay());
|
||||
yesterdayList.add(timestamp);
|
||||
long timestampEnd = getTimestamp(yesterday.atTime(23, 59, 59, 999_000_000));
|
||||
yesterdayList.add(timestampEnd);
|
||||
return yesterdayList;
|
||||
}
|
||||
case "TOMORROW" -> {
|
||||
List<Long> tomorrowList = new ArrayList<>();
|
||||
LocalDate tomorrow = LocalDate.now().plusDays(1);
|
||||
long timestamp = getTimestamp(tomorrow.atStartOfDay());
|
||||
tomorrowList.add(timestamp);
|
||||
long timestampEnd = getTimestamp(tomorrow.atTime(23, 59, 59, 999_000_000));
|
||||
tomorrowList.add(timestampEnd);
|
||||
return tomorrowList;
|
||||
}
|
||||
case "WEEK" -> {
|
||||
List<Long> weeks = new ArrayList<>();
|
||||
LocalDate startOfWeek = LocalDate.now().with(java.time.DayOfWeek.MONDAY);
|
||||
long timestamp = getTimestamp(startOfWeek.atStartOfDay());
|
||||
weeks.add(timestamp);
|
||||
LocalDate now = LocalDate.now().with(DayOfWeek.SUNDAY);
|
||||
long timestampEnd = getTimestamp(now.atTime(23, 59, 59, 999_000_000));
|
||||
weeks.add(timestampEnd);
|
||||
return weeks;
|
||||
}
|
||||
case "LAST_WEEK" -> {
|
||||
List<Long> lastWeeks = new ArrayList<>();
|
||||
LocalDate startOfLastWeek = LocalDate.now().minusWeeks(1).with(java.time.DayOfWeek.MONDAY);
|
||||
long timestamp = getTimestamp(startOfLastWeek.atStartOfDay());
|
||||
lastWeeks.add(timestamp);
|
||||
LocalDate startOfLastWeekEnd = LocalDate.now().minusWeeks(1).with(DayOfWeek.SUNDAY);
|
||||
long timestampEnd = getTimestamp(startOfLastWeekEnd.atTime(23, 59, 59, 999_000_000));
|
||||
lastWeeks.add(timestampEnd);
|
||||
return lastWeeks;
|
||||
}
|
||||
case "NEXT_WEEK" -> {
|
||||
List<Long> nextWeeks = new ArrayList<>();
|
||||
LocalDate startOfNextWeek = LocalDate.now().plusWeeks(1).with(java.time.DayOfWeek.MONDAY);
|
||||
long timestamp = getTimestamp(startOfNextWeek.atStartOfDay());
|
||||
nextWeeks.add(timestamp);
|
||||
LocalDate startOfNextWeekEnd = LocalDate.now().plusWeeks(1).with(DayOfWeek.SUNDAY);
|
||||
long timestampEnd = getTimestamp(startOfNextWeekEnd.atTime(23, 59, 59, 999_000_000));
|
||||
nextWeeks.add(timestampEnd);
|
||||
return nextWeeks;
|
||||
}
|
||||
case "MONTH" -> {
|
||||
List<Long> months = new ArrayList<>();
|
||||
LocalDate startOfMonth = LocalDate.now().withDayOfMonth(1);
|
||||
long timestamp = getTimestamp(startOfMonth.atStartOfDay());
|
||||
months.add(timestamp);
|
||||
LocalDate now = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
|
||||
long timestampEnd = getTimestamp(now.atTime(23, 59, 59, 999_000_000));
|
||||
months.add(timestampEnd);
|
||||
return months;
|
||||
}
|
||||
case "LAST_MONTH" -> {
|
||||
List<Long> lastMonths = new ArrayList<>();
|
||||
LocalDate startOfLastMonth = LocalDate.now().minusMonths(1).withDayOfMonth(1);
|
||||
long timestamp = getTimestamp(startOfLastMonth.atStartOfDay());
|
||||
lastMonths.add(timestamp);
|
||||
LocalDate startOfLastMonthEnd = LocalDate.now().minusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
|
||||
long timestampEnd = getTimestamp(startOfLastMonthEnd.atTime(23, 59, 59, 999_000_000));
|
||||
lastMonths.add(timestampEnd);
|
||||
return lastMonths;
|
||||
}
|
||||
case "NEXT_MONTH" -> {
|
||||
List<Long> nextMonths = new ArrayList<>();
|
||||
LocalDate startOfNextMonth = LocalDate.now().plusMonths(1).withDayOfMonth(1);
|
||||
long timestamp = getTimestamp(startOfNextMonth.atStartOfDay());
|
||||
nextMonths.add(timestamp);
|
||||
LocalDate startOfNextMonthEnd = LocalDate.now().plusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
|
||||
long timestampEnd = getTimestamp(startOfNextMonthEnd.atTime(23, 59, 59, 999_000_000));
|
||||
nextMonths.add(timestampEnd);
|
||||
return nextMonths;
|
||||
}
|
||||
case "LAST_SEVEN" -> {
|
||||
List<Long> lastSevens = new ArrayList<>();
|
||||
LocalDate startOfLastSevenDays = LocalDate.now().minusDays(7);
|
||||
long timestamp = getTimestamp(startOfLastSevenDays.atStartOfDay());
|
||||
lastSevens.add(timestamp);
|
||||
long timestampEnd = getTimestamp(LocalDate.now().atStartOfDay());
|
||||
lastSevens.add(timestampEnd);
|
||||
return lastSevens;
|
||||
}
|
||||
case "SEVEN" -> {
|
||||
List<Long> sevens = new ArrayList<>();
|
||||
LocalDate startOfNextSevenDays = LocalDate.now().plusDays(6);
|
||||
long timestamp = getTimestamp(LocalDate.now().atStartOfDay());
|
||||
sevens.add(timestamp);
|
||||
long timestampEnd = getTimestamp(startOfNextSevenDays.atTime(23, 59, 59, 999_000_000));
|
||||
sevens.add(timestampEnd);
|
||||
return sevens;
|
||||
}
|
||||
case "THIRTY" -> {
|
||||
List<Long> thirty = new ArrayList<>();
|
||||
LocalDate startOfNextThirtyDays = LocalDate.now().plusDays(29);
|
||||
long timestamp = getTimestamp(LocalDate.now().atStartOfDay());
|
||||
thirty.add(timestamp);
|
||||
long timestampEnd = getTimestamp(startOfNextThirtyDays.atTime(23, 59, 59, 999_000_000));
|
||||
thirty.add(timestampEnd);
|
||||
return thirty;
|
||||
}
|
||||
case "LAST_THIRTY" -> {
|
||||
List<Long> lastThirty = new ArrayList<>();
|
||||
LocalDate startOfLastThirtyDays = LocalDate.now().minusDays(30);
|
||||
long timestamp = getTimestamp(startOfLastThirtyDays.atStartOfDay());
|
||||
lastThirty.add(timestamp);
|
||||
long timestampEnd = getTimestamp(LocalDate.now().atStartOfDay());
|
||||
lastThirty.add(timestampEnd);
|
||||
return lastThirty;
|
||||
}
|
||||
case "SIXTY" -> {
|
||||
List<Long> sixty = new ArrayList<>();
|
||||
LocalDate startOfNextSixtyDays = LocalDate.now().plusDays(59);
|
||||
long timestamp = getTimestamp(LocalDate.now().atStartOfDay());
|
||||
sixty.add(timestamp);
|
||||
long timestampEnd = getTimestamp(startOfNextSixtyDays.atTime(23, 59, 59, 999_000_000));
|
||||
sixty.add(timestampEnd);
|
||||
return sixty;
|
||||
}
|
||||
case "LAST_SIXTY" -> {
|
||||
List<Long> lastSixty = new ArrayList<>();
|
||||
LocalDate startOfLastSixtyDays = LocalDate.now().minusDays(60);
|
||||
long timestamp = getTimestamp(startOfLastSixtyDays.atStartOfDay());
|
||||
lastSixty.add(timestamp);
|
||||
long timestampEnd = getTimestamp(LocalDate.now().atStartOfDay());
|
||||
lastSixty.add(timestampEnd);
|
||||
return lastSixty;
|
||||
}
|
||||
//本季度
|
||||
case "QUARTER" -> {
|
||||
List<Long> quarters = new ArrayList<>();
|
||||
LocalDate now = LocalDate.now();
|
||||
int currentMonth = now.getMonthValue();
|
||||
int startMonth = (currentMonth - 1) / 3 * 3 + 1;
|
||||
LocalDate startOfQuarter = LocalDate.of(now.getYear(), startMonth, 1);
|
||||
long timestamp = getTimestamp(startOfQuarter.atStartOfDay());
|
||||
quarters.add(timestamp);
|
||||
LocalDate endOfQuarter = startOfQuarter.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
|
||||
long timestampEnd = getTimestamp(endOfQuarter.atTime(23, 59, 59, 999_000_000));
|
||||
quarters.add(timestampEnd);
|
||||
return quarters;
|
||||
}
|
||||
//上季度
|
||||
case "LAST_QUARTER" -> {
|
||||
List<Long> lastQuarters = new ArrayList<>();
|
||||
LocalDate now = LocalDate.now();
|
||||
int currentMonth = now.getMonthValue();
|
||||
int startMonth = (currentMonth - 1) / 3 * 3 + 1;
|
||||
LocalDate startOfLastQuarter = LocalDate.of(now.getYear(), startMonth, 1).minusMonths(3);
|
||||
long timestamp = getTimestamp(startOfLastQuarter.atStartOfDay());
|
||||
lastQuarters.add(timestamp);
|
||||
LocalDate endOfLastQuarter = startOfLastQuarter.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
|
||||
long timestampEnd = getTimestamp(endOfLastQuarter.atTime(23, 59, 59, 999_000_000));
|
||||
lastQuarters.add(timestampEnd);
|
||||
return lastQuarters;
|
||||
}
|
||||
//下季度
|
||||
case "NEXT_QUARTER" -> {
|
||||
List<Long> nextQuarters = new ArrayList<>();
|
||||
LocalDate now = LocalDate.now();
|
||||
int currentMonth = now.getMonthValue();
|
||||
int startMonth = (currentMonth - 1) / 3 * 3 + 1;
|
||||
LocalDate startOfNextQuarter = LocalDate.of(now.getYear(), startMonth, 1).plusMonths(3);
|
||||
long timestamp = getTimestamp(startOfNextQuarter.atStartOfDay());
|
||||
nextQuarters.add(timestamp);
|
||||
LocalDate endOfNextQuarter = startOfNextQuarter.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
|
||||
long timestampEnd = getTimestamp(endOfNextQuarter.atTime(23, 59, 59, 999_000_000));
|
||||
nextQuarters.add(timestampEnd);
|
||||
return nextQuarters;
|
||||
}
|
||||
//本年度
|
||||
case "YEAR" -> {
|
||||
List<Long> years = new ArrayList<>();
|
||||
LocalDate startOfYear = LocalDate.now().withDayOfYear(1);
|
||||
long timestamp = getTimestamp(startOfYear.atStartOfDay());
|
||||
years.add(timestamp);
|
||||
LocalDate now = LocalDate.now().with(TemporalAdjusters.lastDayOfYear());
|
||||
long timestampEnd = getTimestamp(now.atTime(23, 59, 59, 999_000_000));
|
||||
years.add(timestampEnd);
|
||||
return years;
|
||||
}
|
||||
//上年度
|
||||
case "LAST_YEAR" -> {
|
||||
List<Long> lastYears = new ArrayList<>();
|
||||
LocalDate startOfLastYear = LocalDate.now().minusYears(1).withDayOfYear(1);
|
||||
long timestamp = getTimestamp(startOfLastYear.atStartOfDay());
|
||||
lastYears.add(timestamp);
|
||||
LocalDate startOfLastYearEnd = LocalDate.now().minusYears(1).with(TemporalAdjusters.lastDayOfYear());
|
||||
long timestampEnd = getTimestamp(startOfLastYearEnd.atTime(23, 59, 59, 999_000_000));
|
||||
lastYears.add(timestampEnd);
|
||||
return lastYears;
|
||||
}
|
||||
//下年度
|
||||
case "NEXT_YEAR" -> {
|
||||
List<Long> nextYears = new ArrayList<>();
|
||||
LocalDate startOfNextYear = LocalDate.now().plusYears(1).withDayOfYear(1);
|
||||
long timestamp = getTimestamp(startOfNextYear.atStartOfDay());
|
||||
nextYears.add(timestamp);
|
||||
LocalDate startOfNextYearEnd = LocalDate.now().plusYears(1).with(TemporalAdjusters.lastDayOfYear());
|
||||
long timestampEnd = getTimestamp(startOfNextYearEnd.atTime(23, 59, 59, 999_000_000));
|
||||
nextYears.add(timestampEnd);
|
||||
return nextYears;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
String dateValue = split[1];
|
||||
String dateUnit = split[2];
|
||||
int dateNumber = Integer.parseInt(dateValue);
|
||||
switch (dateUnit) {
|
||||
case "BEFORE_DAY" -> {
|
||||
LocalDateTime startOfLastDays = LocalDateTime.now().minusDays(dateNumber);
|
||||
return getTimestamp(startOfLastDays);
|
||||
}
|
||||
case "AFTER_DAY" -> {
|
||||
LocalDateTime startOfNextDays = LocalDateTime.now().plusDays(dateNumber);
|
||||
return getTimestamp(startOfNextDays);
|
||||
}
|
||||
case "BEFORE_WEEK" -> {
|
||||
LocalDateTime startOfLastWeeks = LocalDateTime.now().minusDays(dateNumber * 7L);
|
||||
return getTimestamp(startOfLastWeeks);
|
||||
}
|
||||
case "AFTER_WEEK" -> {
|
||||
LocalDateTime startOfNextWeeks = LocalDateTime.now().plusDays(dateNumber * 7L);
|
||||
return getTimestamp(startOfNextWeeks);
|
||||
}
|
||||
case "BEFORE_MONTH" -> {
|
||||
LocalDateTime startOfLastMonths = LocalDateTime.now().minusMonths(dateNumber);
|
||||
return getTimestamp(startOfLastMonths);
|
||||
}
|
||||
case "AFTER_MONTH" -> {
|
||||
LocalDateTime startOfNextMonths = LocalDateTime.now().plusMonths(dateNumber);
|
||||
return getTimestamp(startOfNextMonths);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getCombineOperator() {
|
||||
if (Strings.CI.equals(operator, CombineConditionOperator.DYNAMICS.name())) {
|
||||
String strValue = (String) value;
|
||||
String[] split = strValue.split(",");
|
||||
if (split.length == 1) {
|
||||
return CombineConditionOperator.BETWEEN.name();
|
||||
} else {
|
||||
String dateUnit = split[2];
|
||||
switch (dateUnit) {
|
||||
case "BEFORE_DAY", "BEFORE_WEEK", "BEFORE_MONTH" -> {
|
||||
return CombineConditionOperator.LT.name();
|
||||
}
|
||||
case "AFTER_DAY", "AFTER_WEEK", "AFTER_MONTH" -> {
|
||||
return CombineConditionOperator.GT.name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return operator;
|
||||
}
|
||||
|
||||
|
||||
private long getTimestamp(LocalDateTime today) {
|
||||
// 使用系统默认时区
|
||||
ZonedDateTime zonedEndOfDay = today.atZone(ZoneId.systemDefault());
|
||||
// 转为时间戳(毫秒)
|
||||
return zonedEndOfDay.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举:组合条件操作符,定义了各种可能的查询操作符。
|
||||
*/
|
||||
public enum CombineConditionOperator {
|
||||
/**
|
||||
* 动态
|
||||
*/
|
||||
DYNAMICS,
|
||||
/**
|
||||
* 属于某个集合
|
||||
*/
|
||||
IN,
|
||||
|
||||
/**
|
||||
* 不属于某个集合
|
||||
*/
|
||||
NOT_IN,
|
||||
|
||||
/**
|
||||
* 区间操作
|
||||
*/
|
||||
BETWEEN,
|
||||
|
||||
/**
|
||||
* 大于
|
||||
*/
|
||||
GT,
|
||||
|
||||
/**
|
||||
* 小于
|
||||
*/
|
||||
LT,
|
||||
|
||||
/**
|
||||
* 大于等于
|
||||
*/
|
||||
GE,
|
||||
|
||||
/**
|
||||
* 小于等于
|
||||
*/
|
||||
LE,
|
||||
|
||||
/**
|
||||
* 数量大于
|
||||
*/
|
||||
COUNT_GT,
|
||||
|
||||
/**
|
||||
* 数量小于
|
||||
*/
|
||||
COUNT_LT,
|
||||
|
||||
/**
|
||||
* 等于
|
||||
*/
|
||||
EQUALS,
|
||||
|
||||
/**
|
||||
* 不等于
|
||||
*/
|
||||
NOT_EQUALS,
|
||||
|
||||
/**
|
||||
* 包含
|
||||
*/
|
||||
CONTAINS,
|
||||
|
||||
/**
|
||||
* 不包含
|
||||
*/
|
||||
NOT_CONTAINS,
|
||||
|
||||
/**
|
||||
* 为空
|
||||
*/
|
||||
EMPTY,
|
||||
|
||||
/**
|
||||
* 不为空
|
||||
*/
|
||||
NOT_EMPTY,
|
||||
/**
|
||||
* 不等于原值(用户审批时的条件判断)
|
||||
*/
|
||||
NOT_EQUAL_ORIGINAL
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.cordys.common.dto.condition;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 表示组合条件,用于支持复杂的过滤和查询逻辑。
|
||||
* 包含字段名、操作符和期望值等信息。
|
||||
*/
|
||||
@Data
|
||||
public class FilterDBCondition extends FilterCondition {
|
||||
|
||||
@Schema(description = "是否是自定义字段")
|
||||
private Boolean customField = false;
|
||||
|
||||
@Schema(description = "是否是大字段")
|
||||
private Boolean blob = false;
|
||||
|
||||
@Schema(description = "是否是显示字段")
|
||||
private Boolean refFiled = false;
|
||||
|
||||
@Schema(description = "显示字段的主字段是否是自定义字段")
|
||||
private Boolean refMainCustomField = false;
|
||||
|
||||
@Schema(description = "显示字段的主字段名称或ID")
|
||||
private String refMainFieldName;
|
||||
|
||||
@Schema(description = "显示字段的主表名")
|
||||
private String refMainTableName;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import cn.cordys.common.domain.BaseModuleFieldValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CirculationFieldValue extends BaseModuleFieldValue {
|
||||
|
||||
@Schema(description = "是否必填")
|
||||
private Boolean required;
|
||||
|
||||
@Schema(description = "默认值类型")
|
||||
private String valueType;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class CirculationSetting {
|
||||
|
||||
@Schema(description = "源id")
|
||||
private String originId;
|
||||
|
||||
@Schema(description = "源id对应的行目标ids")
|
||||
private List<Target> targets;
|
||||
|
||||
@Schema(description = "模块类型(order-订单/contract-合同)")
|
||||
private String moduleType;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StageAddRequest {
|
||||
|
||||
@Schema(description = "值")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "添加的位置(取值:-1,,1。 -1:源节点之前,1:源节点之后)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int dropPosition;
|
||||
|
||||
@Schema(description = "源节点")
|
||||
private String targetId;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class StageAdvancedConfigRequest {
|
||||
|
||||
@Schema(description = "流转配置类型")
|
||||
private String circulationType;
|
||||
|
||||
@Schema(description = "高级流转设置")
|
||||
private List<CirculationSetting> circulationSettings;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StageConfigResponse {
|
||||
|
||||
@Schema(description = "ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "进行中回退设置")
|
||||
private Boolean afootRollBack;
|
||||
|
||||
@Schema(description = "完结回退设置")
|
||||
private Boolean endRollBack;
|
||||
|
||||
@Schema(description = "顺序")
|
||||
private Long pos;
|
||||
|
||||
@Schema(description = "当前阶段是否存在数据")
|
||||
private Boolean stageHasData = false;
|
||||
|
||||
@Schema(description = "流转类型")
|
||||
private String circulationType;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class StageConfigsResponse {
|
||||
|
||||
@Schema(description = "订单状态流配置列表")
|
||||
List<StageConfigResponse> stageConfigList;
|
||||
|
||||
@Schema(description = "进行中回退设置")
|
||||
private Boolean afootRollBack = true;
|
||||
|
||||
@Schema(description = "完结回退设置")
|
||||
private Boolean endRollBack = false;
|
||||
|
||||
@Schema(description = "流转配置类型")
|
||||
private String circulationType;
|
||||
|
||||
@Schema(description = "高级流转设置")
|
||||
private List<CirculationSetting> advancedConfigs;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StageRollBackRequest {
|
||||
|
||||
@Schema(description = "进行中回退设置")
|
||||
private Boolean afootRollBack;
|
||||
|
||||
@Schema(description = "完结回退设置")
|
||||
private Boolean endRollBack;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import cn.cordys.common.domain.BaseModuleFieldValue;
|
||||
import cn.cordys.crm.system.dto.request.NodeMoveRequest;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class StageSortRequest extends NodeMoveRequest {
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "阶段", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String stage;
|
||||
|
||||
@Schema(description = "更新字段")
|
||||
private List<BaseModuleFieldValue> fields;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StageUpdateRequest {
|
||||
|
||||
@Schema(description = "id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "状态名称")
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.cordys.common.dto.stage;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class Target {
|
||||
|
||||
@Schema(description = "目标id")
|
||||
private String targetId;
|
||||
|
||||
@Schema(description = "是否允许流转")
|
||||
private Boolean enable;
|
||||
|
||||
@Schema(description = "字段配置")
|
||||
private List<CirculationFieldValue> circulationFieldValues;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.cordys.common.handler;
|
||||
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
import org.apache.ibatis.type.TypeReference;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
|
||||
if (parameter == null) {
|
||||
ps.setNull(i, jdbcType.TYPE_CODE);
|
||||
} else {
|
||||
setNonNullParameter(ps, i, parameter, jdbcType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return getNullableResult(rs, columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return getNullableResult(rs, columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return getNullableResult(cs, columnIndex);
|
||||
}
|
||||
|
||||
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
|
||||
|
||||
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
|
||||
|
||||
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
|
||||
|
||||
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package cn.cordys.common.handler;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.StreamReadConstraints;
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ListTypeHandler extends BaseTypeHandler<List<String>> {
|
||||
public static final int DEFAULT_MAX_STRING_LEN = Integer.MAX_VALUE;
|
||||
private static final ObjectMapper objectMapper = JsonMapper.builder()
|
||||
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
|
||||
.build();
|
||||
private static final TypeFactory typeFactory = objectMapper.getTypeFactory();
|
||||
|
||||
static {
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
// 支持json字符中带注释符
|
||||
objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
|
||||
// 自动检测所有类的全部属性
|
||||
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
// 如果一个对象中没有任何的属性,那么在序列化的时候就会报错
|
||||
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
// 设置JSON处理字符长度限制
|
||||
objectMapper.getFactory()
|
||||
.setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(DEFAULT_MAX_STRING_LEN).build());
|
||||
// 处理时间格式
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
|
||||
}
|
||||
|
||||
public static String toJSONString(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<T> parseArray(String content, Class<T> valueType) {
|
||||
CollectionType javaType = typeFactory.constructCollectionType(List.class, valueType);
|
||||
try {
|
||||
return objectMapper.readValue(content, javaType);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
|
||||
String d = toJSONString(parameter);
|
||||
ps.setString(i, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String values = rs.getString(columnName);
|
||||
return getResults(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String values = rs.getString(columnIndex);
|
||||
return getResults(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String values = cs.getString(columnIndex);
|
||||
return getResults(values);
|
||||
}
|
||||
|
||||
private List<String> getResults(String values) {
|
||||
if (StringUtils.isNotBlank(values)) {
|
||||
return parseArray(values, String.class);
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.cordys.common.interceptor;
|
||||
|
||||
import cn.cordys.common.util.CompressUtils;
|
||||
import cn.cordys.config.MybatisInterceptorConfig;
|
||||
import cn.cordys.crm.contract.domain.ContractInvoiceSnapshot;
|
||||
import cn.cordys.crm.contract.domain.ContractSnapshot;
|
||||
import cn.cordys.crm.opportunity.domain.OpportunityQuotationSnapshot;
|
||||
import cn.cordys.crm.system.domain.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统拦截器配置类
|
||||
* <p>
|
||||
* 该类用于配置 MyBatis 的拦截器,特别是字段压缩等功能的配置。
|
||||
* </p>
|
||||
*/
|
||||
@Configuration
|
||||
public class SystemInterceptor {
|
||||
|
||||
/**
|
||||
* 配置系统拦截器列表
|
||||
*
|
||||
* @return 返回 MyBatis 拦截器配置列表,目前支持字段压缩等功能。
|
||||
*/
|
||||
@Bean
|
||||
public List<MybatisInterceptorConfig> systemCompressConfigs() {
|
||||
List<MybatisInterceptorConfig> configList = new ArrayList<>();
|
||||
|
||||
configList.add(new MybatisInterceptorConfig(MessageTask.class, "template", CompressUtils.class, "zip", "unzip"));
|
||||
configList.add(new MybatisInterceptorConfig(Announcement.class, "content", CompressUtils.class, "zip", "unzip"));
|
||||
configList.add(new MybatisInterceptorConfig(Announcement.class, "receiver", CompressUtils.class, "zip", "unzip"));
|
||||
configList.add(new MybatisInterceptorConfig(Announcement.class, "receiveType", CompressUtils.class, "zip", "unzip"));
|
||||
configList.add(new MybatisInterceptorConfig(OrganizationConfigDetail.class, "content", CompressUtils.class, "zip", "unzip"));
|
||||
configList.add(new MybatisInterceptorConfig(Notification.class, "content", CompressUtils.class, "zip", "unzip"));
|
||||
|
||||
configList.add(new MybatisInterceptorConfig(OperationLogBlob.class, "originalValue", CompressUtils.class, "zip", "unzip"));
|
||||
|
||||
configList.add(new MybatisInterceptorConfig(OpportunityQuotationSnapshot.class, "quotationProp", CompressUtils.class, "zipString", "unzipString"));
|
||||
configList.add(new MybatisInterceptorConfig(ContractSnapshot.class, "contractProp", CompressUtils.class, "zipString", "unzipString"));
|
||||
configList.add(new MybatisInterceptorConfig(ContractInvoiceSnapshot.class, "invoiceProp", CompressUtils.class, "zipString", "unzipString"));
|
||||
|
||||
|
||||
// 添加自定义拦截器配置,例如压缩和解压缩功能
|
||||
// configList.add(new MybatisInterceptorConfig(TestResourcePoolBlob.class, "configuration", CompressUtils.class, "zip", "unzip"));
|
||||
return configList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.cordys.common.interceptor;
|
||||
|
||||
import cn.cordys.crm.system.domain.User;
|
||||
import org.apache.ibatis.cache.CacheKey;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 用户密码字段脱敏的拦截器。
|
||||
* 该拦截器会在查询结果中去除 User 对象的密码字段,以避免敏感信息泄露。
|
||||
*/
|
||||
@Intercepts({
|
||||
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
|
||||
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
|
||||
})
|
||||
public class UserDesensitizationInterceptor implements Interceptor {
|
||||
|
||||
/**
|
||||
* 拦截 query 方法,脱敏 User 对象中的密码字段。
|
||||
*
|
||||
* @param invocation 方法调用对象
|
||||
*
|
||||
* @return 脱敏后的结果
|
||||
*
|
||||
* @throws Throwable 如果执行方法时发生错误
|
||||
*/
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
// 执行原始方法
|
||||
Object returnValue = invocation.proceed();
|
||||
|
||||
// 如果返回值是 List 类型,处理其中的每个元素
|
||||
if (returnValue instanceof List<?>) {
|
||||
List<Object> list = new ArrayList<>();
|
||||
boolean isDecrypted = false;
|
||||
|
||||
for (Object val : (List<?>) returnValue) {
|
||||
if (val instanceof User) {
|
||||
isDecrypted = true;
|
||||
// 将密码字段置为 null,进行脱敏处理
|
||||
((User) val).setPassword(null);
|
||||
}
|
||||
list.add(val);
|
||||
}
|
||||
|
||||
// 如果有任何脱敏操作,则返回修改后的列表,否则返回原始结果
|
||||
return isDecrypted ? list : returnValue;
|
||||
}
|
||||
|
||||
// 如果返回值是单个 User 对象,脱敏其密码字段
|
||||
if (returnValue instanceof User) {
|
||||
((User) returnValue).setPassword(null);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将目标对象包装成拦截器对象。
|
||||
*
|
||||
* @param target 需要包装的目标对象
|
||||
*
|
||||
* @return 包装后的目标对象
|
||||
*/
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return Plugin.wrap(target, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拦截器的属性。
|
||||
*
|
||||
* @param properties 拦截器的属性
|
||||
*/
|
||||
@Override
|
||||
public void setProperties(Properties properties) {
|
||||
// 本拦截器不需要设置属性
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cn.cordys.common.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CommonMapper {
|
||||
boolean checkAddExist(@Param("tableName") String tableName,
|
||||
@Param("fieldName") String fieldName,
|
||||
@Param("fieldValue") String fieldValue,
|
||||
@Param("orgId") String orgId);
|
||||
|
||||
boolean checkUpdateExist(@Param("tableName") String tableName,
|
||||
@Param("fieldName") String fieldName,
|
||||
@Param("fieldValue") String fieldValue,
|
||||
@Param("orgId") String orgId,
|
||||
@Param("excludeIds") List<String> excludeIds);
|
||||
|
||||
/**
|
||||
* 获取表属性值集合
|
||||
*
|
||||
* @param tableName 表
|
||||
* @param fieldName 值
|
||||
* @param orgId 组织ID
|
||||
*
|
||||
* @return 值集合
|
||||
*/
|
||||
List<String> getCheckValList(@Param("tableName") String tableName,
|
||||
@Param("fieldName") String fieldName, @Param("orgId") String orgId);
|
||||
|
||||
/**
|
||||
* 校验字段值是否重复
|
||||
*
|
||||
* @param dataTable 数据表
|
||||
* @param fieldTable 字段表
|
||||
* @param fieldId 字段ID
|
||||
* @param fieldValue 字段值
|
||||
* @param orgId 组织ID
|
||||
*
|
||||
* @return 是否重复
|
||||
*/
|
||||
String checkFieldRepeatName(@Param("dataTable") String dataTable, @Param("fieldTable") String fieldTable,
|
||||
@Param("fieldId") String fieldId, @Param("fieldValue") String fieldValue, @Param("orgId") String orgId);
|
||||
|
||||
List<String> getCheckFieldValList(@Param("dataTable") String dataTable, @Param("fieldTable") String fieldTable,
|
||||
@Param("fieldId") String fieldId, @Param("orgId") String orgId);
|
||||
|
||||
/**
|
||||
* 校验业务字段是否重复
|
||||
*
|
||||
* @param dataTable 数据表
|
||||
* @param businessName 业务字段名
|
||||
* @param value 值
|
||||
* @param orgId 组织ID
|
||||
*
|
||||
* @return 是否重复
|
||||
*/
|
||||
String checkInternalRepeatName(@Param("dataTable") String dataTable, @Param("businessName") String businessName,
|
||||
@Param("value") String value, @Param("orgId") String orgId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.cordys.common.mapper.CommonMapper">
|
||||
|
||||
<sql id="moduleFieldCondition">
|
||||
<choose>
|
||||
<when test="condition.multipleValue != null and condition.multipleValue">
|
||||
<include refid="cn.cordys.common.mapper.CommonMapper.arrayValueCondition">
|
||||
<property name="condition" value="condition"/>
|
||||
<property name="column" value="${column}"/>
|
||||
</include>
|
||||
</when>
|
||||
<otherwise>
|
||||
<include refid="cn.cordys.common.mapper.CommonMapper.condition">
|
||||
<property name="condition" value="condition"/>
|
||||
<property name="column" value="${column}"/>
|
||||
</include>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<sql id="arrayValueCondition">
|
||||
<trim prefix="(" suffix=")">
|
||||
<choose>
|
||||
<!-- JSON_OVERLAPS 处理数组匹配 -->
|
||||
<when test="${condition}.operator == 'CONTAINS'">
|
||||
<if test="${condition}.type == 'LINK'">
|
||||
${column} like CONCAT('%', #{condition.value},'%')
|
||||
</if>
|
||||
<if test="${condition}.type != 'LINK'">
|
||||
JSON_OVERLAPS(${column}, JSON_ARRAY(
|
||||
<foreach collection="${condition}.value" item="valueItem" separator=",">
|
||||
#{valueItem}
|
||||
</foreach>))
|
||||
</if>
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'NOT_CONTAINS'">
|
||||
<if test="${condition}.type == 'LINK'">
|
||||
${column} not like CONCAT('%', #{condition.value},'%')
|
||||
or ${column} is null
|
||||
</if>
|
||||
<if test="${condition}.type != 'LINK'">
|
||||
${column} is null or ${column} = '[]' or
|
||||
NOT JSON_OVERLAPS(${column}, JSON_ARRAY(
|
||||
<foreach collection="${condition}.value" item="valueItem" separator=",">
|
||||
#{valueItem}
|
||||
</foreach>))
|
||||
</if>
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'IN'">
|
||||
JSON_OVERLAPS(${column}, JSON_ARRAY(
|
||||
<foreach collection="${condition}.value" item="tag" separator=",">
|
||||
#{tag}
|
||||
</foreach>))
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'NOT_IN'">
|
||||
${column} is null or ${column} = '[]' or
|
||||
NOT JSON_OVERLAPS(${column}, JSON_ARRAY(
|
||||
<foreach collection="${condition}.value" item="tag" separator=",">
|
||||
#{tag}
|
||||
</foreach>))
|
||||
</when>
|
||||
|
||||
<!-- COUNT_GT 和 COUNT_LT -->
|
||||
<when test="${condition}.operator == 'COUNT_GT'">
|
||||
JSON_LENGTH(${column}) > #{condition.value}
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'COUNT_LT'">
|
||||
<choose>
|
||||
<when test="${condition}.value == 0">
|
||||
false
|
||||
</when>
|
||||
<otherwise>
|
||||
(JSON_LENGTH(${column}) < #{condition.value} OR ${column} is null OR ${column} = '[]')
|
||||
</otherwise>
|
||||
</choose>
|
||||
</when>
|
||||
|
||||
<!-- EMPTY / NOT_EMPTY -->
|
||||
<when test="${condition}.operator == 'EMPTY'">
|
||||
(${column} IS NULL OR ${column} = '[]')
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'NOT_EMPTY'">
|
||||
(${column} IS NOT NULL AND ${column} != '[]')
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'EQUALS'">
|
||||
${column} = #{condition.value}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'NOT_EQUALS'">
|
||||
${column} != #{condition.value}
|
||||
or ${column} is null
|
||||
</when>
|
||||
|
||||
</choose>
|
||||
</trim>
|
||||
</sql>
|
||||
|
||||
<sql id="condition">
|
||||
<trim prefix="(" suffix=")">
|
||||
<choose>
|
||||
<when test="${condition}.refMainTableName == 'business_title' and ${condition}.name == 'company_number'">
|
||||
<if test="condition.operator == 'CONTAINS'">
|
||||
CONCAT('CO.NO.', LPAD(${column}, 8, '0'))
|
||||
LIKE CONCAT('%', #{condition.value}, '%')
|
||||
</if>
|
||||
<if test="condition.operator == 'NOT_CONTAINS'">
|
||||
CONCAT('CO.NO.', LPAD(${column}, 8, '0'))
|
||||
NOT LIKE CONCAT('%', #{condition.value}, '%')
|
||||
or combine_0.company_number is null
|
||||
</if>
|
||||
<if test="condition.operator == 'EMPTY'">
|
||||
${column} IS NULL OR ${column} = ''
|
||||
</if>
|
||||
<if test="condition.operator == 'NOT_EMPTY'">
|
||||
${column} IS NOT NULL AND ${column} != ''
|
||||
</if>
|
||||
<if test="condition.operator == 'EQUALS'">
|
||||
CONCAT('CO.NO.', LPAD(${column}, 8, '0')) = #{condition.value}
|
||||
</if>
|
||||
<if test="condition.operator == 'NOT_EQUALS'">
|
||||
CONCAT('CO.NO.', LPAD(${column}, 8, '0')) != #{condition.value}
|
||||
or ${column} is null
|
||||
</if>
|
||||
</when>
|
||||
<otherwise>
|
||||
<choose>
|
||||
<when test="${condition}.operator == 'CONTAINS'">
|
||||
<foreach collection="${condition}.value.split(' ')" item="item" separator="and">
|
||||
${column} like CONCAT('%', #{item},'%')
|
||||
</foreach>
|
||||
</when>
|
||||
<when test="${condition}.operator == 'NOT_CONTAINS'">
|
||||
<foreach collection="${condition}.value.split(' ')" item="item" separator="and">
|
||||
${column} not like CONCAT('%', #{item},'%')
|
||||
or ${column} is null
|
||||
</foreach>
|
||||
</when>
|
||||
<when test="${condition}.operator == 'IN'">
|
||||
<choose>
|
||||
<when test="${condition}.type != null and ${condition}.type == 'LOCATION'">
|
||||
<foreach collection="${condition}.value" item="v" open="(" separator=" OR "
|
||||
close=")">
|
||||
<if test="v == 'CHN'">
|
||||
${column} REGEXP '^[0-9]{6}(-.*)?$' or ${column} = 'CHN-'
|
||||
</if>
|
||||
<if test="v != 'CHN'">
|
||||
<if test="'${column}' == 'value'">
|
||||
`value`
|
||||
</if>
|
||||
<if test="'${column}' != 'value'">
|
||||
${column}
|
||||
</if>
|
||||
LIKE CONCAT(#{v}, '%')
|
||||
</if>
|
||||
</foreach>
|
||||
</when>
|
||||
<otherwise>
|
||||
<if test="'${column}' == 'value'">
|
||||
`value`
|
||||
</if>
|
||||
<if test="'${column}' != 'value'">
|
||||
${column}
|
||||
</if>
|
||||
in
|
||||
<foreach collection="${condition}.value" item="v" separator="," open="(" close=")">
|
||||
#{v}
|
||||
</foreach>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</when>
|
||||
<when test="${condition}.operator == 'NOT_IN'">
|
||||
<choose>
|
||||
<when test="${condition}.type != null and ${condition}.type == 'LOCATION'">
|
||||
(!(
|
||||
<foreach collection="${condition}.value" item="v" open="(" separator=" OR "
|
||||
close=")">
|
||||
<if test="v == 'CHN'">
|
||||
${column} REGEXP '^[0-9]{6}(-.*)?$'
|
||||
</if>
|
||||
<if test="v != 'CHN'">
|
||||
<if test="'${column}' == 'value'">
|
||||
`value`
|
||||
</if>
|
||||
<if test="'${column}' != 'value'">
|
||||
${column}
|
||||
</if>
|
||||
LIKE CONCAT(#{v}, '%')
|
||||
</if>
|
||||
</foreach>
|
||||
)
|
||||
<foreach collection="${condition}.value" item="v">
|
||||
<if test="v == 'CHN'">
|
||||
AND ${column} != 'CHN-'
|
||||
</if>
|
||||
</foreach>
|
||||
)
|
||||
or ${column} is null
|
||||
</when>
|
||||
<otherwise>
|
||||
!(
|
||||
<if test="'${column}' == 'value'">
|
||||
`value`
|
||||
</if>
|
||||
<if test="'${column}' != 'value'">
|
||||
${column}
|
||||
</if>
|
||||
in
|
||||
<foreach collection="${condition}.value" item="v" separator="," open="(" close=")">
|
||||
#{v}
|
||||
</foreach>
|
||||
)
|
||||
or ${column} is null
|
||||
</otherwise>
|
||||
</choose>
|
||||
</when>
|
||||
<when test="${condition}.operator == 'BETWEEN' || ${condition}.operator == 'DYNAMICS'">
|
||||
${column} between #{condition.value[0]} and #{condition.value[1]}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'GT'">
|
||||
${column} is not null and ${column} > #{condition.value}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'LT'">
|
||||
${column} is not null and ${column} < #{condition.value}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'GE'">
|
||||
${column} is not null and ${column} >= #{condition.value}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'LE'">
|
||||
${column} is not null and ${column} <= #{condition.value}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'EMPTY'">
|
||||
${column} is null or ${column} = ''
|
||||
</when>
|
||||
<when test="${condition}.operator == 'NOT_EMPTY'">
|
||||
${column} is not null and ${column} != ''
|
||||
</when>
|
||||
<when test="${condition}.operator == 'EQUALS'">
|
||||
${column} = #{condition.value}
|
||||
</when>
|
||||
<when test="${condition}.operator == 'NOT_EQUALS'">
|
||||
${column} != #{condition.value}
|
||||
or ${column} is null
|
||||
</when>
|
||||
</choose>
|
||||
</otherwise>
|
||||
</choose>
|
||||
|
||||
|
||||
</trim>
|
||||
</sql>
|
||||
|
||||
|
||||
|
||||
<sql id="dataSourceCondition">
|
||||
<trim prefix="(" suffix=")">
|
||||
<choose>
|
||||
<when test="${condition}.operator == 'IN'">
|
||||
${column} in (
|
||||
<foreach collection="${condition}.value" item="valueItem" separator=",">
|
||||
#{valueItem}
|
||||
</foreach>)
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'NOT_IN'">
|
||||
${column} NOT IN (
|
||||
<foreach collection="${condition}.value" item="valueItem" separator=",">
|
||||
#{valueItem}
|
||||
</foreach>)
|
||||
OR ${column} IS NULL
|
||||
</when>
|
||||
|
||||
<!-- EMPTY / NOT_EMPTY -->
|
||||
<when test="${condition}.operator == 'EMPTY'">
|
||||
(${column} IS NULL OR ${column} = '')
|
||||
</when>
|
||||
|
||||
<when test="${condition}.operator == 'NOT_EMPTY'">
|
||||
(${column} IS NOT NULL AND ${column} != '')
|
||||
</when>
|
||||
|
||||
</choose>
|
||||
</trim>
|
||||
</sql>
|
||||
|
||||
<sql id="searchMode">
|
||||
<choose>
|
||||
<when test="${searchMode} == 'AND'">
|
||||
AND
|
||||
</when>
|
||||
<when test="${searchMode} == 'OR'">
|
||||
OR
|
||||
</when>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<sql id="filterInWrapper">
|
||||
<foreach collection="values" item="value" separator="," open="(" close=")">
|
||||
#{value}
|
||||
</foreach>
|
||||
</sql>
|
||||
|
||||
<sql id="filterMultipleWrapper">
|
||||
JSON_OVERLAPS(${column},
|
||||
<choose>
|
||||
<when test="values != null and values.size() > 1">
|
||||
JSON_ARRAY(
|
||||
<foreach collection="values" item="value" separator=",">
|
||||
CAST(#{value} AS SIGNED)
|
||||
</foreach>)
|
||||
</when>
|
||||
<otherwise>
|
||||
JSON_ARRAY(CAST(#{values[0]} AS SIGNED))
|
||||
</otherwise>
|
||||
</choose>)
|
||||
</sql>
|
||||
|
||||
<sql id="sort">
|
||||
<choose>
|
||||
<when test="${sort} != null and ${sort}.valid()">
|
||||
<bind name="sortName" value="${sort}.name"/>
|
||||
<bind name="sortType" value="${sort}.type"/>
|
||||
order by
|
||||
<choose>
|
||||
<when test="'${prefix}' != null and '${prefix}' != ''">
|
||||
${prefix}.${sortName} ${sortType}
|
||||
</when>
|
||||
<otherwise>
|
||||
${sortName} ${sortType}
|
||||
</otherwise>
|
||||
</choose>
|
||||
</when>
|
||||
<otherwise>
|
||||
<if test="'${defaultSort}' != null and '${defaultSort}' != ''">
|
||||
order by
|
||||
<choose>
|
||||
<when test="'${prefix}' != null and '${prefix}' != ''">
|
||||
${prefix}.${defaultSort}
|
||||
</when>
|
||||
<otherwise>
|
||||
${defaultSort}
|
||||
</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
<select id="checkAddExist" resultType="java.lang.Boolean">
|
||||
select count(1)
|
||||
from `${tableName}`
|
||||
where `${fieldName}` = #{fieldValue} and organization_id = #{orgId}
|
||||
<if test="tableName == 'clue'">
|
||||
and (transition_type != 'CUSTOMER' or transition_type is null)
|
||||
</if>
|
||||
limit 1
|
||||
</select>
|
||||
<select id="checkUpdateExist" resultType="java.lang.Boolean">
|
||||
select count(1)
|
||||
from `${tableName}`
|
||||
where `${fieldName}` = #{fieldValue} and organization_id = #{orgId} and id not in
|
||||
<foreach collection="excludeIds" item="id" separator="," open="(" close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
<if test="tableName == 'clue'">
|
||||
and (transition_type != 'CUSTOMER' or transition_type is null)
|
||||
</if>
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="getCheckValList" resultType="java.lang.String">
|
||||
select `${fieldName}`
|
||||
from `${tableName}` t
|
||||
where organization_id = #{orgId}
|
||||
<if test="tableName == 'clue'">
|
||||
and (t.transition_type != 'CUSTOMER' or t.transition_type is null) and t.in_shared_pool = false
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="checkFieldRepeatName" resultType="java.lang.String">
|
||||
select d.name from `${fieldTable}` f join `${dataTable}` d on f.resource_id = d.id
|
||||
where f.field_id = #{fieldId} and f.field_value = #{fieldValue} and d.organization_id = #{orgId}
|
||||
<if test="dataTable == 'clue'">
|
||||
and (d.transition_type != 'CUSTOMER' or d.transition_type is null)
|
||||
</if>
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="getCheckFieldValList" resultType="java.lang.String">
|
||||
select f.field_value from `${fieldTable}` f join `${dataTable}` d on f.resource_id = d.id
|
||||
where f.field_id = #{fieldId} and d.organization_id = #{orgId}
|
||||
<if test="dataTable == 'clue'">
|
||||
and (d.transition_type != 'CUSTOMER' or d.transition_type is null)
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="checkInternalRepeatName" resultType="java.lang.String">
|
||||
select d.name from `${dataTable}` d
|
||||
where d.`${businessName}` = #{value} and d.organization_id = #{orgId}
|
||||
<if test="dataTable == 'clue'">
|
||||
and (d.transition_type != 'CUSTOMER' or d.transition_type is null)
|
||||
</if>
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<sql id="chartSelect">
|
||||
<if test="request.categoryAxisParam.businessField">
|
||||
<if test="request.categoryAxisParam.blob">
|
||||
categoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.blob">
|
||||
<if test="request.categoryAxisParam.businessFieldName == 'department_id'">
|
||||
categoryAxis.department_id
|
||||
</if>
|
||||
<if test="request.categoryAxisParam.businessFieldName != 'department_id'">
|
||||
${mainTable}.${request.categoryAxisParam.businessFieldName}
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.businessField">
|
||||
<if test="request.categoryAxisParam.blob">
|
||||
categoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.blob">
|
||||
categoryAxis.field_value
|
||||
</if>
|
||||
</if>
|
||||
as categoryAxis,
|
||||
|
||||
<if test="request.subCategoryAxisParam != null">
|
||||
<if test="request.subCategoryAxisParam.businessField">
|
||||
<if test="request.subCategoryAxisParam.blob">
|
||||
subCategoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.subCategoryAxisParam.blob">
|
||||
<if test="request.subCategoryAxisParam.businessFieldName == 'department_id'">
|
||||
subCategoryAxis.department_id
|
||||
</if>
|
||||
<if test="request.subCategoryAxisParam.businessFieldName != 'department_id'">
|
||||
${mainTable}.${request.subCategoryAxisParam.businessFieldName}
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
<if test="!request.subCategoryAxisParam.businessField">
|
||||
<if test="request.subCategoryAxisParam.blob">
|
||||
subCategoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.subCategoryAxisParam.blob">
|
||||
subCategoryAxis.field_value
|
||||
</if>
|
||||
</if>
|
||||
as subCategoryAxis,
|
||||
</if>
|
||||
|
||||
${request.valueAxisParam.aggregateMethod}(
|
||||
<if test="request.valueAxisParam.aggregateMethod == 'COUNT'">
|
||||
1
|
||||
</if>
|
||||
<if test="request.valueAxisParam.aggregateMethod != 'COUNT'">
|
||||
<if test="request.valueAxisParam.businessField">
|
||||
${mainTable}.${request.valueAxisParam.businessFieldName}
|
||||
</if>
|
||||
<if test="!request.valueAxisParam.businessField">
|
||||
valueAxis.field_value
|
||||
</if>
|
||||
</if>
|
||||
) as valueAxis
|
||||
</sql>
|
||||
|
||||
<sql id="chartGroupBy">
|
||||
group by
|
||||
<if test="request.categoryAxisParam.businessField">
|
||||
<if test="request.categoryAxisParam.blob">
|
||||
categoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.blob">
|
||||
<if test="request.categoryAxisParam.businessFieldName == 'department_id'">
|
||||
categoryAxis.department_id
|
||||
</if>
|
||||
<if test="request.categoryAxisParam.businessFieldName != 'department_id'">
|
||||
${mainTable}.${request.categoryAxisParam.businessFieldName}
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.businessField">
|
||||
<if test="request.categoryAxisParam.blob">
|
||||
categoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.blob">
|
||||
categoryAxis.field_value
|
||||
</if>
|
||||
</if>
|
||||
<if test="request.subCategoryAxisParam != null">
|
||||
,
|
||||
<if test="request.subCategoryAxisParam.businessField">
|
||||
<if test="request.categoryAxisParam.blob">
|
||||
subCategoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.blob">
|
||||
<if test="request.subCategoryAxisParam.businessFieldName == 'department_id'">
|
||||
subCategoryAxis.department_id
|
||||
</if>
|
||||
<if test="request.subCategoryAxisParam.businessFieldName != 'department_id'">
|
||||
${mainTable}.${request.subCategoryAxisParam.businessFieldName}
|
||||
</if>
|
||||
</if>
|
||||
|
||||
</if>
|
||||
<if test="!request.subCategoryAxisParam.businessField">
|
||||
<if test="request.categoryAxisParam.blob">
|
||||
subCategoryAxis_multiple.id
|
||||
</if>
|
||||
<if test="!request.categoryAxisParam.blob">
|
||||
subCategoryAxis.field_value
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
limit 500
|
||||
</sql>
|
||||
|
||||
<sql id="chartAxisJoin">
|
||||
<if test="${axisParam}.businessField and ${axisParam}.businessFieldName == 'products'">
|
||||
left join JSON_TABLE(
|
||||
${mainTablePrefix}.products,
|
||||
'$[*]' COLUMNS (
|
||||
id VARCHAR(50) PATH '$'
|
||||
)
|
||||
) AS ${tablePrefix}_multiple on true
|
||||
</if>
|
||||
<if test="!${axisParam}.businessField and ${axisParam}.fieldId != null and ${axisParam}.fieldId != ''">
|
||||
<if test="${axisParam}.blob">
|
||||
left join ${mainTable}_field_blob ${tablePrefix}
|
||||
on ${mainTablePrefix}.id = ${tablePrefix}.resource_id and ${tablePrefix}.field_id = #{${axisParam}.fieldId}
|
||||
left join JSON_TABLE(
|
||||
${tablePrefix}.field_value,
|
||||
'$[*]' COLUMNS (
|
||||
id VARCHAR(50) PATH '$'
|
||||
)
|
||||
) AS ${tablePrefix}_multiple on true
|
||||
</if>
|
||||
<if test="!${axisParam}.blob">
|
||||
left join ${mainTable}_field ${tablePrefix}
|
||||
on ${mainTablePrefix}.id = ${tablePrefix}.resource_id and ${tablePrefix}.field_id = #{${axisParam}.fieldId}
|
||||
</if>
|
||||
</if>
|
||||
<if test="${axisParam}.businessFieldName == 'department_id'">
|
||||
left join sys_organization_user ${tablePrefix}
|
||||
on ${tablePrefix}.user_id = ${mainTablePrefix}.owner
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="refFieldConditionJoin">
|
||||
<if test="!condition.refMainCustomField">
|
||||
<if test="!condition.customField">
|
||||
left join ${condition.refMainTableName} ${fieldTable}
|
||||
on ${mainTableAlias}.${condition.refMainFieldName} = ${fieldTable}.id
|
||||
</if>
|
||||
<if test="condition.customField">
|
||||
left join ${condition.refMainTableName} ${fieldTable}_main
|
||||
on ${mainTableAlias}.${condition.refMainFieldName} = ${fieldTable}_main.id
|
||||
left join
|
||||
<if test="condition.blob">
|
||||
${condition.refMainTableName}_field_blob
|
||||
</if>
|
||||
<if test="!condition.blob">
|
||||
${condition.refMainTableName}_field
|
||||
</if>
|
||||
${fieldTable}
|
||||
on ${fieldTable}_main.id = ${fieldTable}.resource_id and ${fieldTable}.field_id = #{condition.name}
|
||||
</if>
|
||||
</if>
|
||||
<if test="condition.refMainCustomField">
|
||||
left join ${mainTable}_field ${fieldTable}_custom
|
||||
on ${mainTableAlias}.id = ${fieldTable}_custom.resource_id and ${fieldTable}_custom.field_id = #{condition.refMainFieldName}
|
||||
<if test="!condition.customField">
|
||||
left join ${condition.refMainTableName} ${fieldTable}
|
||||
on ${fieldTable}_custom.field_value = ${fieldTable}.id
|
||||
</if>
|
||||
<if test="condition.customField">
|
||||
left join ${condition.refMainTableName} ${fieldTable}_main
|
||||
on ${fieldTable}_custom.field_value = ${fieldTable}_main.id
|
||||
left join
|
||||
<if test="condition.blob">
|
||||
${condition.refMainTableName}_field_blob
|
||||
</if>
|
||||
<if test="!condition.blob">
|
||||
${condition.refMainTableName}_field
|
||||
</if>
|
||||
${fieldTable}
|
||||
on ${fieldTable}_main.id = ${fieldTable}.resource_id and ${fieldTable}.field_id = #{condition.name}
|
||||
</if>
|
||||
</if>
|
||||
</sql>
|
||||
</mapper>
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 批量操作权限校验注解
|
||||
* <p>
|
||||
* 仅校验角色权限位和数据权限,不校验待办和审批状态权限
|
||||
* 状态权限业务代理里有校验,待办没有批量操作
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CsBatchPermission {
|
||||
|
||||
/**
|
||||
* 权限码,如 "ORDER:UPDATE"
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* 资源ID的SpEL表达式,支持解析为单个ID或List<String>
|
||||
* 为空时仅校验角色权限位
|
||||
*/
|
||||
String resourceId() default "";
|
||||
|
||||
/**
|
||||
* 表单类型,如 "order",用于数据权限校验
|
||||
*/
|
||||
String formType() default "";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 自定义权限校验注解
|
||||
* 该注解可以替换 @RequiresPermissions,支持更复杂的权限校验逻辑,包括资源ID和审批状态权限。
|
||||
* 注:该注解不支持校验多个权限码,多个请使用 @RequiresPermissions 注解。
|
||||
* <p>
|
||||
* 四个判断依据:当前资源是你待办的资源 or (角色的权限位 && 角色的数据权限 && 审批流的状态权限)
|
||||
* <p>
|
||||
* 当 resourceId 为空时,仅校验角色权限位;
|
||||
* 指定 resourceId 时,若 approvalTaskId 不为空,则校验审批状态权限;否则校order验(角色的权限位 && 角色的数据权限 && 审批流的状态权限)。
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CsPermission {
|
||||
|
||||
/**
|
||||
* 权限码,如 "ORDER:READ"
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* 资源ID的SpEL表达式,如 "{#id}" 或 "{#request.id}"
|
||||
* 为空时仅校验角色权限位
|
||||
*/
|
||||
String resourceId() default "";
|
||||
|
||||
/**
|
||||
* 表单类型,如 "order",用于审批状态权限校验
|
||||
* 为空时跳过审批状态权限校验
|
||||
*/
|
||||
String formType() default "";
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import cn.cordys.context.OrganizationContext;
|
||||
import cn.cordys.security.SessionUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @CsPermission 注解切面
|
||||
* <p>
|
||||
* 拦截标注了 @CsPermission 的方法,执行权限校验
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class CsPermissionAspect {
|
||||
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
private final StandardReflectionParameterNameDiscoverer discoverer = new StandardReflectionParameterNameDiscoverer();
|
||||
|
||||
@Resource
|
||||
private ResourcePermissionService resourcePermissionService;
|
||||
|
||||
|
||||
@Before("@annotation(cn.cordys.common.permission.CsPermission)")
|
||||
public void checkPermission(JoinPoint joinPoint) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
CsPermission annotation = method.getAnnotation(CsPermission.class);
|
||||
if (annotation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String permission = annotation.value();
|
||||
String resourceIdExpr = annotation.resourceId();
|
||||
|
||||
if (resourceIdExpr.isEmpty()) {
|
||||
// 仅校验角色权限位
|
||||
resourcePermissionService.checkPermission(permission);
|
||||
} else {
|
||||
String formType = annotation.formType();
|
||||
String userId = SessionUtils.getUserId();
|
||||
String orgId = OrganizationContext.getOrganizationId();
|
||||
// 从方法参数中解析资源ID
|
||||
String resourceId = resolveResourceId(method, joinPoint.getArgs(), resourceIdExpr);
|
||||
// 校验 (1 && 2 && 3)
|
||||
resourcePermissionService.checkResourcePermission(permission, resourceId, formType, userId, orgId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从方法参数中解析资源ID(支持SpEL表达式)
|
||||
*/
|
||||
private String resolveResourceId(Method method, Object[] args, String expression) {
|
||||
String[] params = discoverer.getParameterNames(method);
|
||||
if (params == null || args == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
context.setVariable(params[i], args[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
var exp = parser.parseExpression(expression);
|
||||
Object value = exp.getValue(context, Object.class);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof List<?> list) {
|
||||
return list.isEmpty() ? null : list.getFirst().toString();
|
||||
}
|
||||
return value.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作权限校验:仅校验角色权限位 + 数据权限,不校验待办和审批状态权限
|
||||
*/
|
||||
@Before("@annotation(cn.cordys.common.permission.CsBatchPermission)")
|
||||
public void checkBatchPermission(JoinPoint joinPoint) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
CsBatchPermission annotation = method.getAnnotation(CsBatchPermission.class);
|
||||
if (annotation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String permission = annotation.value();
|
||||
String resourceIdExpr = annotation.resourceId();
|
||||
String formType = annotation.formType();
|
||||
String userId = SessionUtils.getUserId();
|
||||
String orgId = OrganizationContext.getOrganizationId();
|
||||
|
||||
if (resourceIdExpr.isEmpty()) {
|
||||
resourcePermissionService.checkPermission(permission);
|
||||
} else {
|
||||
List<String> resourceIds = resolveResourceIds(method, joinPoint.getArgs(), resourceIdExpr);
|
||||
resourcePermissionService.checkBatchResourcePermission(permission, resourceIds, formType, userId, orgId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从方法参数中解析资源ID列表(支持SpEL表达式,结果为List<String>)
|
||||
* <p>
|
||||
* SpEL {expr} 会将结果包一层 list,如 {#request.ids} 中 ids 为 List 时,
|
||||
* 结果为 List<List<String>>,需要展平内层
|
||||
*/
|
||||
private List<String> resolveResourceIds(Method method, Object[] args, String expression) {
|
||||
String[] params = discoverer.getParameterNames(method);
|
||||
if (params == null || args == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
context.setVariable(params[i], args[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
var exp = parser.parseExpression(expression);
|
||||
Object value = exp.getValue(context, Object.class);
|
||||
if (value == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (value instanceof List<?> list) {
|
||||
// SpEL {expr} 包了一层 list,如果唯一元素也是 list,则展平
|
||||
if (list.size() == 1 && list.getFirst() instanceof List<?> inner) {
|
||||
return inner.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Object::toString)
|
||||
.toList();
|
||||
}
|
||||
return list.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Object::toString)
|
||||
.toList();
|
||||
}
|
||||
return List.of(value.toString());
|
||||
} catch (Exception e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 权限信息
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "权限信息")
|
||||
public class Permission implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "权限ID")
|
||||
private String id;
|
||||
@Schema(description = "权限名称")
|
||||
private String name;
|
||||
@Schema(description = "是否启用该权限")
|
||||
private Boolean enable = false;
|
||||
@Schema(description = "是否是企业权限")
|
||||
private Boolean license = false;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import cn.cordys.common.dto.RoleDataScopeDTO;
|
||||
import cn.cordys.common.dto.RolePermissionDTO;
|
||||
import cn.cordys.common.util.BeanUtils;
|
||||
import cn.cordys.common.util.CommonBeanFactory;
|
||||
import cn.cordys.crm.system.domain.RolePermission;
|
||||
import cn.cordys.crm.system.service.RoleService;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 权限缓存
|
||||
*
|
||||
* @author jianxing
|
||||
*/
|
||||
@Component
|
||||
public class PermissionCache {
|
||||
|
||||
@Cacheable(value = "permission_cache", key = "#userId + ':' + #orgId")
|
||||
public List<RolePermissionDTO> getRolePermissions(String userId, String orgId) {
|
||||
RoleService roleService = CommonBeanFactory.getBean(RoleService.class);
|
||||
// 获取角色
|
||||
List<RoleDataScopeDTO> roleOptions = Objects.requireNonNull(roleService).getRoleOptions(userId, orgId);
|
||||
List<String> roleIds = roleOptions.stream()
|
||||
.map(RoleDataScopeDTO::getId).collect(Collectors.toList());
|
||||
|
||||
if (CollectionUtils.isEmpty(roleIds)) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
|
||||
// 获取角色权限
|
||||
List<RolePermission> permissions = roleService.getPermissions(roleIds);
|
||||
Map<String, List<RolePermission>> rolePermissionMap = permissions.stream()
|
||||
.collect(Collectors.groupingBy(RolePermission::getRoleId));
|
||||
|
||||
// 缓存角色和权限
|
||||
return roleOptions.stream()
|
||||
.map(roleDataScopeDTO -> {
|
||||
RolePermissionDTO rolePermission = BeanUtils.copyBean(new RolePermissionDTO(), roleDataScopeDTO);
|
||||
List<RolePermission> rolePermissions = rolePermissionMap.get(roleDataScopeDTO.getId());
|
||||
if (CollectionUtils.isEmpty(rolePermissions)) {
|
||||
rolePermission.setPermissions(Set.of());
|
||||
return rolePermission;
|
||||
}
|
||||
rolePermission.setPermissions(
|
||||
rolePermissions
|
||||
.stream()
|
||||
.map(RolePermission::getPermissionId)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
return rolePermission;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Set<String> getPermissionIds(String userId, String orgId) {
|
||||
List<RolePermissionDTO> rolePermissions = Objects.requireNonNull(CommonBeanFactory.getBean(PermissionCache.class)).getRolePermissions(userId, orgId);
|
||||
return rolePermissions.stream()
|
||||
.flatMap(rolePermissionDTO -> rolePermissionDTO.getPermissions().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@CacheEvict(value = "permission_cache", key = "#userId + ':' + #orgId", beforeInvocation = true)
|
||||
public void clearCache(String userId, String orgId) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "权限设置菜单项")
|
||||
public class PermissionDefinitionItem {
|
||||
@Schema(description = "菜单项ID")
|
||||
private String id;
|
||||
@Schema(description = "菜单项名称")
|
||||
private String name;
|
||||
@Schema(description = "是否是企业版菜单")
|
||||
private Boolean license = false;
|
||||
@Schema(description = "菜单是否全选")
|
||||
private Boolean enable = false;
|
||||
@Schema(description = "菜单下的权限列表")
|
||||
private List<Permission> permissions;
|
||||
@Schema(description = "子菜单")
|
||||
private List<PermissionDefinitionItem> children;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import cn.cordys.common.constants.InternalUser;
|
||||
import cn.cordys.common.constants.RoleDataScope;
|
||||
import cn.cordys.common.dto.ResourceTabEnableDTO;
|
||||
import cn.cordys.common.dto.RolePermissionDTO;
|
||||
import cn.cordys.common.util.CommonBeanFactory;
|
||||
import cn.cordys.context.OrganizationContext;
|
||||
import cn.cordys.security.SessionUser;
|
||||
import cn.cordys.security.SessionUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
public class PermissionUtils {
|
||||
public static boolean hasPermission(String permission) {
|
||||
PermissionCache permissionCache = CommonBeanFactory.getBean(PermissionCache.class);
|
||||
String userId = SessionUtils.getUserId();
|
||||
String organizationId = OrganizationContext.getOrganizationId();
|
||||
Set<String> permissionIds = Objects.requireNonNull(permissionCache).getPermissionIds(userId, organizationId);
|
||||
SessionUser user = Objects.requireNonNull(SessionUtils.getUser());
|
||||
if (Strings.CS.equals(InternalUser.ADMIN.getValue(), user.getId())) {
|
||||
// admin 用户拥有所有权限
|
||||
return true;
|
||||
}
|
||||
// 判断是否拥有权限
|
||||
return permissionIds.contains(permission);
|
||||
}
|
||||
|
||||
public static ResourceTabEnableDTO getTabEnableConfig(String userId, String permission, List<RolePermissionDTO> rolePermissions) {
|
||||
ResourceTabEnableDTO resourceTabEnableDTO = new ResourceTabEnableDTO();
|
||||
if (Strings.CS.equals(userId, InternalUser.ADMIN.getValue())) {
|
||||
resourceTabEnableDTO.setAll(true);
|
||||
resourceTabEnableDTO.setDept(true);
|
||||
}
|
||||
for (RolePermissionDTO rolePermission : rolePermissions) {
|
||||
if (!rolePermission.getPermissions().contains(permission)) {
|
||||
// 判断权限
|
||||
continue;
|
||||
}
|
||||
if (Strings.CS.equalsAny(rolePermission.getDataScope(), RoleDataScope.ALL.name(), RoleDataScope.DEPT_CUSTOM.name())) {
|
||||
// 数据权限为全部或指定部门,显示所有tab
|
||||
resourceTabEnableDTO.setAll(true);
|
||||
}
|
||||
if (Strings.CS.equalsAny(rolePermission.getDataScope(), RoleDataScope.ALL.name(), RoleDataScope.DEPT_CUSTOM.name(),
|
||||
RoleDataScope.DEPT_AND_CHILD.name())) {
|
||||
// 数据权限为全部或部门,显示部门tab
|
||||
resourceTabEnableDTO.setDept(true);
|
||||
}
|
||||
}
|
||||
return resourceTabEnableDTO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 资源访问上下文,包含权限校验所需的资源信息
|
||||
*/
|
||||
@Data
|
||||
public class ResourceAccessContext {
|
||||
|
||||
/**
|
||||
* 资源负责人ID
|
||||
*/
|
||||
private String ownerId;
|
||||
|
||||
/**
|
||||
* 审批状态
|
||||
*/
|
||||
private String approvalStatus;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 资源访问上下文提供者
|
||||
* <p>
|
||||
* 各业务模块实现此接口,提供资源负责人和审批状态信息
|
||||
*/
|
||||
public interface ResourceAccessContextProvider {
|
||||
|
||||
/**
|
||||
* 返回此提供者处理的表单类型,如 "order"、"contract"
|
||||
*/
|
||||
String getFormType();
|
||||
|
||||
/**
|
||||
* 获取资源访问上下文
|
||||
*
|
||||
* @param resourceId 资源ID
|
||||
* @param orgId 组织ID
|
||||
* @return 资源访问上下文,资源不存在时返回 null
|
||||
*/
|
||||
ResourceAccessContext getAccessContext(String resourceId, String orgId);
|
||||
|
||||
/**
|
||||
* 批量获取资源负责人ID
|
||||
* <p>
|
||||
* 默认逐条调用 getAccessContext,子类可重写以优化为批量查询
|
||||
*
|
||||
* @param resourceIds 资源ID列表
|
||||
* @param orgId 组织ID
|
||||
* @return 资源ID -> 负责人ID 的映射,不包含负责人为空的资源
|
||||
*/
|
||||
Map<String, String> batchGetOwnerIds(List<String> resourceIds, String orgId);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package cn.cordys.common.permission;
|
||||
|
||||
import cn.cordys.common.exception.GenericException;
|
||||
import cn.cordys.common.response.result.CrmHttpResultCode;
|
||||
import cn.cordys.common.service.DataScopeService;
|
||||
import cn.cordys.crm.approval.constants.ApprovalStatus;
|
||||
import cn.cordys.crm.approval.domain.ApprovalInstance;
|
||||
import cn.cordys.crm.approval.domain.ApprovalTask;
|
||||
import cn.cordys.crm.approval.service.ApprovalFlowService;
|
||||
import cn.cordys.mybatis.BaseMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 资源权限校验服务
|
||||
* <p>
|
||||
* 校验逻辑:(角色权限位 && 角色数据权限 && 审批状态权限)
|
||||
*/
|
||||
@Service
|
||||
public class ResourcePermissionService {
|
||||
|
||||
@Resource
|
||||
private DataScopeService dataScopeService;
|
||||
@Resource
|
||||
private ApprovalFlowService approvalFlowService;
|
||||
@Resource
|
||||
private BaseMapper<ApprovalTask> approvalTaskMapper;
|
||||
@Resource
|
||||
private BaseMapper<ApprovalInstance> approvalInstanceMapper;
|
||||
|
||||
/**
|
||||
* 仅校验角色权限位
|
||||
*/
|
||||
public void checkPermission(String permission) {
|
||||
if (!PermissionUtils.hasPermission(permission)) {
|
||||
throw new GenericException(CrmHttpResultCode.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整权限校验:(角色权限位 && 角色数据权限 && 审批状态权限)
|
||||
*
|
||||
* @param permission 权限码
|
||||
* @param resourceId 资源ID
|
||||
* @param formType 表单类型
|
||||
* @param userId 当前用户ID
|
||||
* @param orgId 组织ID
|
||||
*/
|
||||
public void checkResourcePermission(String permission, String resourceId, String formType, String userId, String orgId) {
|
||||
if (StringUtils.isBlank(resourceId)) {
|
||||
checkPermission(permission);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从HTTP请求中获取待办任务ID
|
||||
String approvalTaskId = resolveApprovalTaskId();
|
||||
// 如果传了待办任务ID,只校验当前用户是否是该待办的所有人
|
||||
if (StringUtils.isNotBlank(approvalTaskId)) {
|
||||
if (!isTaskOwner(approvalTaskId, resourceId, userId)) {
|
||||
throw new GenericException(CrmHttpResultCode.FORBIDDEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceAccessContextProvider provider = getProvider(formType);
|
||||
ResourceAccessContext context = provider != null ? provider.getAccessContext(resourceId, orgId) : null;
|
||||
boolean permitted = checkRoleAndDataAndStatusPermission(permission, formType, userId, orgId, context);
|
||||
|
||||
if (!permitted) {
|
||||
throw new GenericException(CrmHttpResultCode.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前用户是否是指定待办任务的所有人,且该任务关联的资源与请求资源一致
|
||||
*/
|
||||
private boolean isTaskOwner(String approvalTaskId, String resourceId, String userId) {
|
||||
if (StringUtils.isBlank(approvalTaskId) || StringUtils.isBlank(userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ApprovalTask task = approvalTaskMapper.selectByPrimaryKey(approvalTaskId);
|
||||
if (task == null || !userId.equals(task.getApproverId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验任务关联的审批实例资源是否与当前请求的资源一致
|
||||
ApprovalInstance instance = approvalInstanceMapper.selectByPrimaryKey(task.getInstanceId());
|
||||
return instance != null && resourceId.equals(instance.getResourceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HTTP请求中提取待办任务ID
|
||||
* 优先从请求头 X-Pending-Task-Id 获取,其次从查询参数 pendingTaskId 获取
|
||||
*/
|
||||
private String resolveApprovalTaskId() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String pendingTaskId = request.getHeader("Approval-Task-Id");
|
||||
if (pendingTaskId == null || pendingTaskId.isBlank()) {
|
||||
pendingTaskId = request.getParameter("approvalTaskId");
|
||||
}
|
||||
return (pendingTaskId != null && !pendingTaskId.isBlank()) ? pendingTaskId.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量权限校验:仅校验角色权限位 + 数据权限(跳过审批状态权限)
|
||||
*/
|
||||
public void checkBatchResourcePermission(String permission, List<String> resourceIds, String formType, String userId, String orgId) {
|
||||
if (!PermissionUtils.hasPermission(permission)) {
|
||||
throw new GenericException(CrmHttpResultCode.FORBIDDEN);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(formType) && CollectionUtils.isNotEmpty(resourceIds)) {
|
||||
ResourceAccessContextProvider provider = getProvider(formType);
|
||||
if (provider == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> ownerMap = provider.batchGetOwnerIds(resourceIds, orgId);
|
||||
List<String> ownerIds = ownerMap.values().stream()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (!ownerIds.isEmpty()) {
|
||||
dataScopeService.checkDataPermission(userId, orgId, ownerIds, permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验角色权限位 + 数据权限 + 审批状态权限:(1 && 2 && 3)
|
||||
*/
|
||||
private boolean checkRoleAndDataAndStatusPermission(String permission, String formType,
|
||||
String userId, String orgId, ResourceAccessContext context) {
|
||||
|
||||
String ownerId = context != null ? context.getOwnerId() : null;
|
||||
String approvalStatus = context != null ? context.getApprovalStatus() : null;
|
||||
|
||||
// Check 1: 角色权限位
|
||||
if (!PermissionUtils.hasPermission(permission)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check 2: 角色的数据权限
|
||||
if (ownerId != null && !dataScopeService.hasDataPermission(userId, orgId, ownerId, permission)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check 3: 审批流的状态权限
|
||||
if (StringUtils.isNotBlank(formType) && !checkStatusPermission(permission, formType, approvalStatus, orgId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验审批状态权限
|
||||
* 如果审批状态为 NONE 或无审批流配置,默认允许
|
||||
*/
|
||||
private boolean checkStatusPermission(String permission, String formType, String approvalStatus, String orgId) {
|
||||
if (StringUtils.isBlank(approvalStatus) || ApprovalStatus.NONE.name().equals(approvalStatus)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 查询状态权限配置
|
||||
var setting = approvalFlowService.getStatusPermissionsByFormType(formType, orgId);
|
||||
if (setting == null || CollectionUtils.isEmpty(setting.getStatusPermissions())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 查找当前审批状态对应的权限配置
|
||||
for (var sp : setting.getStatusPermissions()) {
|
||||
if (permission.equals(sp.getPermission()) && approvalStatus.equals(sp.getApprovalStatus())) {
|
||||
return Boolean.TRUE.equals(sp.getEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
// 未找到对应配置,默认允许
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 formType 获取对应的 ResourceAccessContextProvider
|
||||
*/
|
||||
private ResourceAccessContextProvider getProvider(String formType) {
|
||||
if (StringUtils.isBlank(formType)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, ResourceAccessContextProvider> providers = cn.cordys.common.util.CommonBeanFactory
|
||||
.getBeansOfType(ResourceAccessContextProvider.class);
|
||||
if (providers == null) {
|
||||
return null;
|
||||
}
|
||||
return providers.values().stream()
|
||||
.filter(p -> formType.equals(p.getFormType()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cn.cordys.common.redis;
|
||||
|
||||
import cn.cordys.common.constants.TopicConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MessagePublisher {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
public MessagePublisher(StringRedisTemplate redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息到默认主题
|
||||
*
|
||||
* @param message 要发布的消息内容
|
||||
*/
|
||||
public void publish(String message) {
|
||||
publish(TopicConstants.DOWNLOAD_TOPIC, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息到指定主题
|
||||
*
|
||||
* @param topicName 主题名称
|
||||
* @param message 要发布的消息内容
|
||||
*/
|
||||
public void publish(String topicName, String message) {
|
||||
try {
|
||||
ChannelTopic topic = new ChannelTopic(topicName);
|
||||
redisTemplate.convertAndSend(topic.getTopic(), message);
|
||||
} catch (Exception e) {
|
||||
log.error("发布消息到主题失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cn.cordys.common.redis;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Redis消息订阅处理器
|
||||
* 负责接收并处理来自Redis频道的消息
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MessageSubscriber implements MessageListener {
|
||||
|
||||
/**
|
||||
* 所有Topic的消费者集合
|
||||
*/
|
||||
static Map<String, TopicConsumer> consumerMap = new HashMap<>();
|
||||
|
||||
|
||||
public MessageSubscriber(List<TopicConsumer> consumers) {
|
||||
consumers.forEach(consumer -> consumerMap.put(consumer.getChannel(), consumer));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从Redis接收的消息
|
||||
*
|
||||
* @param message Redis消息对象
|
||||
* @param pattern 订阅的模式
|
||||
*/
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String channel = new String(pattern);
|
||||
String messageBody = new String(message.getBody());
|
||||
// 处理消息
|
||||
processMessage(messageBody, channel);
|
||||
} catch (Exception e) {
|
||||
log.error("处理订阅消息时发生异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理解析后的消息
|
||||
*
|
||||
* @param message 消息内容
|
||||
* @param channel 消息来源频道
|
||||
*/
|
||||
private void processMessage(String message, String channel) {
|
||||
// 根据频道区分不同的业务逻辑处理
|
||||
TopicConsumer consumer = consumerMap.get(channel);
|
||||
if (consumer == null) {
|
||||
log.error("未找到对应的消费者处理频道: {}", channel);
|
||||
return;
|
||||
}
|
||||
consumer.consume(message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.cordys.common.redis;
|
||||
|
||||
/**
|
||||
* redis 各种topic消费者的公共接口
|
||||
*/
|
||||
public interface TopicConsumer {
|
||||
|
||||
String getChannel();
|
||||
|
||||
void consume(String message);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.cordys.common.request;
|
||||
|
||||
import cn.cordys.common.util.rsa.RsaKey;
|
||||
import cn.cordys.common.util.rsa.RsaUtils;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* <p>登录请求的 DTO 类。</p>
|
||||
* <p>包含用户名、密码以及认证信息。用户名和密码经过 RSA 解密后使用。</p>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class LoginRequest {
|
||||
|
||||
/**
|
||||
* 用户名,不能为空,最大长度为 256。
|
||||
*/
|
||||
@NotBlank(message = "{user_name_is_null}")
|
||||
@Size(max = 256, message = "{user_name_length_too_long}")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码,不能为空,最大长度为 256。
|
||||
*/
|
||||
@NotBlank(message = "{password_is_null}")
|
||||
@Size(max = 256, message = "{password_length_too_long}")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 认证信息,可选字段。
|
||||
*/
|
||||
private String authenticate;
|
||||
|
||||
/**
|
||||
* 登录地,可选字段。
|
||||
*/
|
||||
private String loginAddress;
|
||||
|
||||
/**
|
||||
* 平台
|
||||
*/
|
||||
@NotBlank(message = "{platform_is_null}")
|
||||
private String platform;
|
||||
|
||||
/**
|
||||
* 获取解密后的用户名。
|
||||
* <p>如果解密失败,将返回原始的用户名。</p>
|
||||
*
|
||||
* @return 解密后的用户名
|
||||
*/
|
||||
public String getUsername() {
|
||||
try {
|
||||
RsaKey rsaKey = RsaUtils.getRsaKey();
|
||||
return RsaUtils.privateDecrypt(username, rsaKey.getPrivateKey());
|
||||
} catch (Exception e) {
|
||||
// 解密失败,返回原始用户名
|
||||
throw new RuntimeException("解密用户名失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解密后的密码。
|
||||
* <p>如果解密失败,将返回原始的密码。</p>
|
||||
*
|
||||
* @return 解密后的密码
|
||||
*/
|
||||
public String getPassword() {
|
||||
try {
|
||||
RsaKey rsaKey = RsaUtils.getRsaKey();
|
||||
return RsaUtils.privateDecrypt(password, rsaKey.getPrivateKey());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解密密码失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
|
||||
import cn.cordys.common.exception.GenericException;
|
||||
import cn.cordys.common.util.JSON;
|
||||
import cn.cordys.common.util.Translator;
|
||||
import cn.cordys.crm.system.dto.field.base.BaseField;
|
||||
import cn.cordys.crm.system.dto.field.base.OptionProp;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.cordys.common.constants.CommonResultCode.FIELD_OPTION_VALUE_ERROR;
|
||||
import static cn.cordys.common.constants.CommonResultCode.FIELD_VALIDATE_ERROR;
|
||||
|
||||
|
||||
/**
|
||||
* @author jainxing
|
||||
*/
|
||||
public abstract class AbstractModuleFieldResolver<T extends BaseField> {
|
||||
|
||||
/**
|
||||
* 校验参数是否合法
|
||||
*
|
||||
* @param customField
|
||||
* @param value
|
||||
*/
|
||||
abstract public void validate(T customField, Object value);
|
||||
|
||||
/**
|
||||
* 将数据库的字符串值转换为对应的参数值
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public Object convertToValue(T selectField, String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据库的字符串值转换为对应的参数值
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public Object transformToValue(T selectField, String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段文本 => 值
|
||||
*
|
||||
* @param field 字段
|
||||
* @param text 文本
|
||||
* @return 字段值
|
||||
*/
|
||||
public Object textToValue(T field, String text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将对应的参数值转换成字符串
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public String convertToString(T selectField, Object value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
protected void throwValidateException(String name) {
|
||||
throw new GenericException(FIELD_VALIDATE_ERROR, Translator.getWithArgs(FIELD_VALIDATE_ERROR.getMessage(), name));
|
||||
}
|
||||
|
||||
protected void throwOptionException(String name) {
|
||||
throw new GenericException(FIELD_OPTION_VALUE_ERROR, Translator.getWithArgs(FIELD_OPTION_VALUE_ERROR.getMessage(), name));
|
||||
}
|
||||
|
||||
protected void validateRequired(T customField, Object value) {
|
||||
// 移动端,不一定需要校验必填,暂时不校验
|
||||
}
|
||||
|
||||
protected void validateArray(String name, Object value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
if (value instanceof List<?> list) {
|
||||
list.forEach(v -> validateString(name, v));
|
||||
} else {
|
||||
throwValidateException(name);
|
||||
}
|
||||
}
|
||||
|
||||
protected void validateString(String name, Object v) {
|
||||
if (v != null && !(v instanceof String)) {
|
||||
throwValidateException(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 校验选项值是否合法,空值,"",不校验
|
||||
protected void validateOptions(String name, Object value, List<OptionProp> options) {
|
||||
if (value == null || StringUtils.isBlank(value.toString())) {
|
||||
return;
|
||||
}
|
||||
if (options == null) {
|
||||
options = List.of();
|
||||
}
|
||||
Set<String> values = options.stream()
|
||||
.map(OptionProp::getValue)
|
||||
.collect(Collectors.toSet());
|
||||
if (!values.contains(value)) {
|
||||
throwOptionException(name);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getStringValue(Object value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
protected Object parse2Array(String value) {
|
||||
return value == null ? null : JSON.parseArray(value);
|
||||
}
|
||||
|
||||
protected Object parse2Long(String value) {
|
||||
return value == null ? null : Long.valueOf(value);
|
||||
}
|
||||
|
||||
protected String getJsonString(Object value) {
|
||||
return value == null ? null : JSON.toJSONString(value);
|
||||
}
|
||||
|
||||
protected List<String> parseFakeJsonArray(String content) {
|
||||
return Arrays.stream(content.replaceAll("^\\[|]$", "").replaceAll("['\"]", "").split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
import cn.cordys.common.util.CommonBeanFactory;
|
||||
import cn.cordys.common.util.JSON;
|
||||
import cn.cordys.crm.system.dto.field.AttachmentField;
|
||||
import cn.cordys.crm.system.mapper.ExtAttachmentMapper;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class AttachmentFieldResolver extends AbstractModuleFieldResolver<AttachmentField> {
|
||||
|
||||
|
||||
private static final ExtAttachmentMapper extAttachmentMapper;
|
||||
|
||||
static {
|
||||
extAttachmentMapper = CommonBeanFactory.getBean(ExtAttachmentMapper.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(AttachmentField customField, Object value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToString(AttachmentField attachmentField, Object value) {
|
||||
return getJsonString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToValue(AttachmentField attachmentField, String value) {
|
||||
return parse2Array(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object transformToValue(AttachmentField attachmentField, String value) {
|
||||
if (StringUtils.isBlank(value) || Strings.CS.equals(value, "[]")) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> ids = JSON.parseArray(value, String.class);
|
||||
|
||||
List<String> names = Objects.requireNonNull(extAttachmentMapper).selectNameByIds(ids);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(names)) {
|
||||
return String.join(",", JSON.parseArray(JSON.toJSONString(names), String.class));
|
||||
}
|
||||
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
|
||||
import cn.cordys.common.util.JSON;
|
||||
import cn.cordys.crm.system.dto.field.CheckBoxField;
|
||||
import cn.cordys.crm.system.dto.field.base.OptionProp;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
public class CheckBoxResolver extends AbstractModuleFieldResolver<CheckBoxField> {
|
||||
|
||||
@Override
|
||||
public void validate(CheckBoxField checkBoxField, Object value) {
|
||||
// 校验必填
|
||||
validateRequired(checkBoxField, value);
|
||||
|
||||
// 校验值类型
|
||||
validateArray(checkBoxField.getName(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToString(CheckBoxField checkBoxField, Object value) {
|
||||
return JSON.toJSONString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToValue(CheckBoxField checkBoxField, String value) {
|
||||
return parse2Array(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object transformToValue(CheckBoxField checkBoxField, String value) {
|
||||
if (StringUtils.isBlank(value) || Strings.CS.equals(value, "[]")) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> list = JSON.parseArray(value, String.class);
|
||||
List<String> result = new ArrayList<>();
|
||||
Map<String, String> optionValueMap = checkBoxField.getOptions().stream().collect(Collectors.toMap(OptionProp::getValue, OptionProp::getLabel));
|
||||
list.forEach(item -> {
|
||||
if (optionValueMap.containsKey(item)) {
|
||||
result.add(optionValueMap.get(item));
|
||||
}
|
||||
});
|
||||
return String.join(",", JSON.parseArray(JSON.toJSONString(result)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object textToValue(CheckBoxField field, String text) {
|
||||
if (StringUtils.isBlank(text) || Strings.CS.equals(text, "[]")) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
try {
|
||||
List<String> texts = parseFakeJsonArray(text);
|
||||
if (CollectionUtils.isEmpty(texts)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
Map<String, String> optionMap = field.getOptions().stream()
|
||||
.collect(Collectors.toMap(OptionProp::getLabel, OptionProp::getValue, (v1, v2) -> v1));
|
||||
List<String> values = texts.stream()
|
||||
.filter(item -> item != null && optionMap.containsKey(item))
|
||||
.map(optionMap::get)
|
||||
.collect(Collectors.toList());
|
||||
return CollectionUtils.isEmpty(values) ? texts : values;
|
||||
} catch (Exception e) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
import cn.cordys.common.util.CommonBeanFactory;
|
||||
import cn.cordys.common.util.JSON;
|
||||
import cn.cordys.crm.clue.domain.Clue;
|
||||
import cn.cordys.crm.clue.service.ClueService;
|
||||
import cn.cordys.crm.contract.domain.BusinessTitle;
|
||||
import cn.cordys.crm.contract.domain.Contract;
|
||||
import cn.cordys.crm.contract.domain.ContractPaymentPlan;
|
||||
import cn.cordys.crm.contract.domain.ContractPaymentRecord;
|
||||
import cn.cordys.crm.contract.service.BusinessTitleService;
|
||||
import cn.cordys.crm.contract.service.ContractPaymentPlanService;
|
||||
import cn.cordys.crm.contract.service.ContractPaymentRecordService;
|
||||
import cn.cordys.crm.contract.service.ContractService;
|
||||
import cn.cordys.crm.customer.domain.Customer;
|
||||
import cn.cordys.crm.customer.domain.CustomerContact;
|
||||
import cn.cordys.crm.customer.service.CustomerContactService;
|
||||
import cn.cordys.crm.customer.service.CustomerService;
|
||||
import cn.cordys.crm.form.domain.CustomFormData;
|
||||
import cn.cordys.crm.form.service.CustomFormDataService;
|
||||
import cn.cordys.crm.opportunity.domain.Opportunity;
|
||||
import cn.cordys.crm.opportunity.domain.OpportunityQuotation;
|
||||
import cn.cordys.crm.opportunity.service.OpportunityQuotationService;
|
||||
import cn.cordys.crm.opportunity.service.OpportunityService;
|
||||
import cn.cordys.crm.order.domain.Order;
|
||||
import cn.cordys.crm.order.service.OrderService;
|
||||
import cn.cordys.crm.product.domain.Product;
|
||||
import cn.cordys.crm.product.domain.ProductPrice;
|
||||
import cn.cordys.crm.product.service.ProductPriceService;
|
||||
import cn.cordys.crm.product.service.ProductService;
|
||||
import cn.cordys.crm.system.constants.FieldSourceType;
|
||||
import cn.cordys.crm.system.dto.field.DatasourceMultipleField;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DatasourceMultipleResolver extends AbstractModuleFieldResolver<DatasourceMultipleField> {
|
||||
|
||||
private static final CustomerService customerService;
|
||||
private static final OpportunityService opportunityService;
|
||||
private static final ClueService clueService;
|
||||
private static final CustomerContactService contactService;
|
||||
private static final ProductService productService;
|
||||
private static final ProductPriceService productPriceService;
|
||||
private static final OpportunityQuotationService opportunityQuotationService;
|
||||
private static final ContractPaymentPlanService contractPaymentPlanService;
|
||||
private static final ContractPaymentRecordService contractPaymentRecordService;
|
||||
private static final BusinessTitleService businessTitleService;
|
||||
private static final OrderService orderService;
|
||||
private static final ContractService contractService;
|
||||
private static final CustomFormDataService customFormDataService;
|
||||
|
||||
public static final String EMPTY_ARRAY_STRING = "[]";
|
||||
|
||||
static {
|
||||
customerService = CommonBeanFactory.getBean(CustomerService.class);
|
||||
opportunityService = CommonBeanFactory.getBean(OpportunityService.class);
|
||||
clueService = CommonBeanFactory.getBean(ClueService.class);
|
||||
contactService = CommonBeanFactory.getBean(CustomerContactService.class);
|
||||
productService = CommonBeanFactory.getBean(ProductService.class);
|
||||
productPriceService = CommonBeanFactory.getBean(ProductPriceService.class);
|
||||
opportunityQuotationService = CommonBeanFactory.getBean(OpportunityQuotationService.class);
|
||||
contractPaymentRecordService = CommonBeanFactory.getBean(ContractPaymentRecordService.class);
|
||||
contractPaymentPlanService = CommonBeanFactory.getBean(ContractPaymentPlanService.class);
|
||||
businessTitleService = CommonBeanFactory.getBean(BusinessTitleService.class);
|
||||
orderService = CommonBeanFactory.getBean(OrderService.class);
|
||||
contractService = CommonBeanFactory.getBean(ContractService.class);
|
||||
customFormDataService = CommonBeanFactory.getBean(CustomFormDataService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(DatasourceMultipleField customField, Object value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToValue(DatasourceMultipleField customField, String value) {
|
||||
return parse2Array(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToString(DatasourceMultipleField customField, Object value) {
|
||||
return getJsonString(value);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object transformToValue(DatasourceMultipleField datasourceMultipleField, String value) {
|
||||
if (StringUtils.isBlank(value) || Strings.CS.equals(value, EMPTY_ARRAY_STRING)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
var list = JSON.parseArray(value, String.class);
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.CUSTOMER.name())) {
|
||||
return Objects.requireNonNull(customerService).getCustomerNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.CONTACT.name())) {
|
||||
return Objects.requireNonNull(contactService).getContactNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.OPPORTUNITY.name())) {
|
||||
return Objects.requireNonNull(opportunityService).getOpportunityNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.CLUE.name())) {
|
||||
return Objects.requireNonNull(clueService).getClueNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.PRODUCT.name())) {
|
||||
return Objects.requireNonNull(productService).getProductNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.PRICE.name())) {
|
||||
return Objects.requireNonNull(productPriceService).getProductPriceNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.QUOTATION.name())) {
|
||||
return Objects.requireNonNull(opportunityQuotationService).getQuotationNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.CONTRACT_PAYMENT_RECORD.name())) {
|
||||
return Objects.requireNonNull(contractPaymentRecordService).getRecordNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.PAYMENT_PLAN.name())) {
|
||||
return Objects.requireNonNull(contractPaymentPlanService).getPlanNameByIds(list);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.BUSINESS_TITLE.name())) {
|
||||
return Objects.requireNonNull(businessTitleService).getTitleNameByIds(list);
|
||||
}
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.ORDER.name())) {
|
||||
return Objects.requireNonNull(orderService).getOrderNameByIds(list);
|
||||
}
|
||||
if (Strings.CI.equals(datasourceMultipleField.getDataSourceType(), FieldSourceType.CONTRACT.name())) {
|
||||
return Objects.requireNonNull(contractService).getContractNameByIds(list);
|
||||
}
|
||||
|
||||
return Objects.requireNonNull(customFormDataService).getNameStrByIds(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object textToValue(DatasourceMultipleField field, String text) {
|
||||
if (StringUtils.isBlank(text) || Strings.CS.equals(text, EMPTY_ARRAY_STRING)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> names = parseFakeJsonArray(text);
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CUSTOMER.name())) {
|
||||
List<Customer> customerList = Objects.requireNonNull(customerService).getCustomerListByNames(names);
|
||||
List<String> ids = customerList.stream().map(Customer::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.OPPORTUNITY.name())) {
|
||||
List<Opportunity> opportunityList = Objects.requireNonNull(opportunityService).getOpportunityListByNames(names);
|
||||
List<String> ids = opportunityList.stream().map(Opportunity::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CLUE.name())) {
|
||||
List<Clue> clueList = Objects.requireNonNull(clueService).getClueListByNames(names);
|
||||
List<String> ids = clueList.stream().map(Clue::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTACT.name())) {
|
||||
List<CustomerContact> contactList = Objects.requireNonNull(contactService).getContactListByNames(names);
|
||||
List<String> ids = contactList.stream().map(CustomerContact::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.PRODUCT.name())) {
|
||||
List<Product> productList = Objects.requireNonNull(productService).getProductListByNames(names);
|
||||
List<String> ids = productList.stream().map(Product::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.PRICE.name())) {
|
||||
List<ProductPrice> prices = Objects.requireNonNull(productPriceService).getProductPriceListByNames(names);
|
||||
List<String> ids = prices.stream().map(ProductPrice::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.QUOTATION.name())) {
|
||||
List<OpportunityQuotation> quotations = Objects.requireNonNull(opportunityQuotationService).getQuotationListByNames(names);
|
||||
List<String> ids = quotations.stream().map(OpportunityQuotation::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTRACT_PAYMENT_RECORD.name())) {
|
||||
List<ContractPaymentRecord> records = Objects.requireNonNull(contractPaymentRecordService).getRecordListByNames(names);
|
||||
List<String> ids = records.stream().map(ContractPaymentRecord::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.PAYMENT_PLAN.name())) {
|
||||
List<ContractPaymentPlan> plans = Objects.requireNonNull(contractPaymentPlanService).getPlanListByNames(names);
|
||||
List<String> ids = plans.stream().map(ContractPaymentPlan::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.BUSINESS_TITLE.name())) {
|
||||
List<BusinessTitle> titles = Objects.requireNonNull(businessTitleService).getBusinessTitleListByNames(names);
|
||||
List<String> ids = titles.stream().map(BusinessTitle::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.ORDER.name())) {
|
||||
List<Order> orders = Objects.requireNonNull(orderService).getOrderListByNames(names);
|
||||
List<String> ids = orders.stream().map(Order::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTRACT.name())) {
|
||||
List<Contract> contracts = Objects.requireNonNull(contractService).getContractListByNames(names);
|
||||
List<String> ids = contracts.stream().map(Contract::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
|
||||
List<CustomFormData> contracts = Objects.requireNonNull(customFormDataService).selectByNames(names);
|
||||
List<String> ids = contracts.stream().map(CustomFormData::getId).toList();
|
||||
return CollectionUtils.isEmpty(ids) ? names : ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
import cn.cordys.common.util.CommonBeanFactory;
|
||||
import cn.cordys.crm.clue.domain.Clue;
|
||||
import cn.cordys.crm.clue.service.ClueService;
|
||||
import cn.cordys.crm.contract.domain.BusinessTitle;
|
||||
import cn.cordys.crm.contract.domain.Contract;
|
||||
import cn.cordys.crm.contract.domain.ContractPaymentPlan;
|
||||
import cn.cordys.crm.contract.domain.ContractPaymentRecord;
|
||||
import cn.cordys.crm.contract.service.BusinessTitleService;
|
||||
import cn.cordys.crm.contract.service.ContractPaymentPlanService;
|
||||
import cn.cordys.crm.contract.service.ContractPaymentRecordService;
|
||||
import cn.cordys.crm.contract.service.ContractService;
|
||||
import cn.cordys.crm.customer.domain.Customer;
|
||||
import cn.cordys.crm.customer.domain.CustomerContact;
|
||||
import cn.cordys.crm.customer.service.CustomerContactService;
|
||||
import cn.cordys.crm.customer.service.CustomerService;
|
||||
import cn.cordys.crm.form.domain.CustomFormData;
|
||||
import cn.cordys.crm.form.service.CustomFormDataService;
|
||||
import cn.cordys.crm.opportunity.domain.Opportunity;
|
||||
import cn.cordys.crm.opportunity.domain.OpportunityQuotation;
|
||||
import cn.cordys.crm.opportunity.service.OpportunityQuotationService;
|
||||
import cn.cordys.crm.opportunity.service.OpportunityService;
|
||||
import cn.cordys.crm.order.domain.Order;
|
||||
import cn.cordys.crm.order.service.OrderService;
|
||||
import cn.cordys.crm.product.domain.Product;
|
||||
import cn.cordys.crm.product.domain.ProductPrice;
|
||||
import cn.cordys.crm.product.service.ProductPriceService;
|
||||
import cn.cordys.crm.product.service.ProductService;
|
||||
import cn.cordys.crm.system.constants.FieldSourceType;
|
||||
import cn.cordys.crm.system.dto.field.DatasourceField;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DatasourceResolver extends AbstractModuleFieldResolver<DatasourceField> {
|
||||
|
||||
private static final CustomerService customerService;
|
||||
private static final OpportunityService opportunityService;
|
||||
private static final ClueService clueService;
|
||||
private static final CustomerContactService contactService;
|
||||
private static final ProductService productService;
|
||||
private static final ProductPriceService productPriceService;
|
||||
private static final OpportunityQuotationService opportunityQuotationService;
|
||||
private static final ContractService contractService;
|
||||
private static final ContractPaymentPlanService contractPaymentPlanService;
|
||||
private static final ContractPaymentRecordService contractPaymentRecordService;
|
||||
private static final BusinessTitleService businessTitleService;
|
||||
private static final OrderService orderService;
|
||||
private static final CustomFormDataService customFormDataService;
|
||||
|
||||
static {
|
||||
customerService = CommonBeanFactory.getBean(CustomerService.class);
|
||||
opportunityService = CommonBeanFactory.getBean(OpportunityService.class);
|
||||
clueService = CommonBeanFactory.getBean(ClueService.class);
|
||||
contactService = CommonBeanFactory.getBean(CustomerContactService.class);
|
||||
productService = CommonBeanFactory.getBean(ProductService.class);
|
||||
productPriceService = CommonBeanFactory.getBean(ProductPriceService.class);
|
||||
opportunityQuotationService = CommonBeanFactory.getBean(OpportunityQuotationService.class);
|
||||
contractService = CommonBeanFactory.getBean(ContractService.class);
|
||||
contractPaymentRecordService = CommonBeanFactory.getBean(ContractPaymentRecordService.class);
|
||||
contractPaymentPlanService = CommonBeanFactory.getBean(ContractPaymentPlanService.class);
|
||||
businessTitleService = CommonBeanFactory.getBean(BusinessTitleService.class);
|
||||
orderService = CommonBeanFactory.getBean(OrderService.class);
|
||||
customFormDataService = CommonBeanFactory.getBean(CustomFormDataService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(DatasourceField customField, Object value) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object transformToValue(DatasourceField datasourceField, String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.CUSTOMER.name())) {
|
||||
return Objects.requireNonNull(customerService).getCustomerName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.CONTACT.name())) {
|
||||
return Objects.requireNonNull(contactService).getContactName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.OPPORTUNITY.name())) {
|
||||
return Objects.requireNonNull(opportunityService).getOpportunityName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.CLUE.name())) {
|
||||
return Objects.requireNonNull(clueService).getClueName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.PRODUCT.name())) {
|
||||
return Objects.requireNonNull(productService).getProductName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.PRICE.name())) {
|
||||
return Objects.requireNonNull(productPriceService).getProductPriceName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.QUOTATION.name())) {
|
||||
return Objects.requireNonNull(opportunityQuotationService).getQuotationName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.PAYMENT_PLAN.name())) {
|
||||
return Objects.requireNonNull(contractPaymentPlanService).getPlanName(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.CONTRACT_PAYMENT_RECORD.name())) {
|
||||
return Objects.requireNonNull(contractPaymentRecordService).getRecordNameById(value);
|
||||
}
|
||||
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.BUSINESS_TITLE.name())) {
|
||||
return Objects.requireNonNull(businessTitleService).getBusinessTitleName(value);
|
||||
}
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.ORDER.name())) {
|
||||
return Objects.requireNonNull(orderService).getOrderName(value);
|
||||
}
|
||||
if (Strings.CI.equals(datasourceField.getDataSourceType(), FieldSourceType.CONTRACT.name())) {
|
||||
return Objects.requireNonNull(contractService).getContractName(value);
|
||||
}
|
||||
|
||||
return Objects.requireNonNull(customFormDataService).getNameById(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object textToValue(DatasourceField field, String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CUSTOMER.name())) {
|
||||
List<Customer> customerList = Objects.requireNonNull(customerService).getCustomerListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(customerList) ? StringUtils.EMPTY : customerList.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.OPPORTUNITY.name())) {
|
||||
List<Opportunity> opportunityList = Objects.requireNonNull(opportunityService).getOpportunityListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(opportunityList) ? StringUtils.EMPTY : opportunityList.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CLUE.name())) {
|
||||
List<Clue> clueList = Objects.requireNonNull(clueService).getClueListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(clueList) ? StringUtils.EMPTY : clueList.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTACT.name())) {
|
||||
List<CustomerContact> contactList = Objects.requireNonNull(contactService).getContactListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(contactList) ? StringUtils.EMPTY : contactList.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.PRODUCT.name())) {
|
||||
List<Product> productList = Objects.requireNonNull(productService).getProductListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(productList) ? StringUtils.EMPTY : productList.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.PRICE.name())) {
|
||||
List<ProductPrice> productPrices = Objects.requireNonNull(productPriceService).getProductPriceListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(productPrices) ? StringUtils.EMPTY : productPrices.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.QUOTATION.name())) {
|
||||
List<OpportunityQuotation> quotations = Objects.requireNonNull(opportunityQuotationService).getQuotationListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(quotations) ? StringUtils.EMPTY : quotations.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTRACT.name())) {
|
||||
List<Contract> contracts = Objects.requireNonNull(contractService).getContractListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(contracts) ? StringUtils.EMPTY : contracts.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.PAYMENT_PLAN.name())) {
|
||||
List<ContractPaymentPlan> plans = Objects.requireNonNull(contractPaymentPlanService).getPlanListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(plans) ? StringUtils.EMPTY : plans.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTRACT_PAYMENT_RECORD.name())) {
|
||||
List<ContractPaymentRecord> records = Objects.requireNonNull(contractPaymentRecordService).getRecordListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(records) ? StringUtils.EMPTY : records.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.BUSINESS_TITLE.name())) {
|
||||
List<BusinessTitle> businessTitles = Objects.requireNonNull(businessTitleService).getBusinessTitleListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(businessTitles) ? StringUtils.EMPTY : businessTitles.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.ORDER.name())) {
|
||||
List<Order> orders = Objects.requireNonNull(orderService).getOrderListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(orders) ? StringUtils.EMPTY : orders.getFirst().getId();
|
||||
}
|
||||
if (Strings.CI.equals(field.getDataSourceType(), FieldSourceType.CONTRACT.name())) {
|
||||
List<Contract> contracts = Objects.requireNonNull(contractService).getContractListByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(contracts) ? StringUtils.EMPTY : contracts.getFirst().getId();
|
||||
}
|
||||
|
||||
List<CustomFormData> customFormDataList = Objects.requireNonNull(customFormDataService).selectByNames(List.of(text));
|
||||
return CollectionUtils.isEmpty(customFormDataList) ? StringUtils.EMPTY : customFormDataList.getFirst().getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
import cn.cordys.common.util.TimeUtils;
|
||||
import cn.cordys.crm.system.dto.field.DateTimeField;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
|
||||
/**
|
||||
* @author jianxing
|
||||
*/
|
||||
public class DateTimeResolver extends AbstractModuleFieldResolver<DateTimeField> {
|
||||
|
||||
public static final String DATE = "date";
|
||||
public static final String DATETIME = "datetime";
|
||||
public static final String MONTH = "month";
|
||||
|
||||
@Override
|
||||
public void validate(DateTimeField dateTimeField, Object value) {
|
||||
validateRequired(dateTimeField, value);
|
||||
|
||||
validateLong(dateTimeField.getName(), value);
|
||||
}
|
||||
|
||||
protected void validateLong(String name, Object value) {
|
||||
if (value != null && !(value instanceof Long)) {
|
||||
throwValidateException(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToValue(DateTimeField dateTimeField, String value) {
|
||||
return parse2Long(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object transformToValue(DateTimeField dateTimeField, String value) {
|
||||
if (Strings.CI.equals(dateTimeField.getDateType(), DATE)) {
|
||||
return TimeUtils.getDateStr(Long.valueOf(value));
|
||||
}
|
||||
if (Strings.CI.equals(dateTimeField.getDateType(), DATETIME)) {
|
||||
return TimeUtils.getDateTimeStr(Long.valueOf(value));
|
||||
}
|
||||
if (Strings.CI.equals(dateTimeField.getDateType(), MONTH)) {
|
||||
return TimeUtils.getMonthStr(Long.valueOf(value));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object textToValue(DateTimeField field, String text) {
|
||||
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy-M-d H:m:s"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy/M/d H:m:s"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy-M-d H:m"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy/M/d H:m"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy-M-d"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy/M/d"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy-M"))
|
||||
.appendOptional(DateTimeFormatter.ofPattern("yyyy/M"))
|
||||
.toFormatter();
|
||||
|
||||
TemporalAccessor parsed = formatter.parseBest(text,
|
||||
LocalDateTime::from,
|
||||
LocalDate::from,
|
||||
YearMonth::from);
|
||||
|
||||
Instant instant = switch (parsed) {
|
||||
case LocalDateTime localDateTime -> localDateTime.atZone(ZoneId.systemDefault()).toInstant();
|
||||
case LocalDate localDate -> localDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
case YearMonth yearMonth -> yearMonth.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
default -> throw new DateTimeParseException("无法解析日期时间: " + text, text, 0);
|
||||
};
|
||||
|
||||
return instant.toEpochMilli();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
import cn.cordys.crm.system.dto.field.base.BaseField;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Author: jianxing
|
||||
* @CreateTime: 2025-03-06 16:07
|
||||
*/
|
||||
public class DefaultModuleFieldResolver extends AbstractModuleFieldResolver {
|
||||
@Override
|
||||
public void validate(BaseField customField, Object value) {
|
||||
// 校验必填
|
||||
validateRequired(customField, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToString(BaseField selectField, Object value) {
|
||||
if (value instanceof List) {
|
||||
return getJsonString(value);
|
||||
}
|
||||
return getStringValue(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.cordys.common.resolver.field;
|
||||
|
||||
import cn.cordys.common.util.CommonBeanFactory;
|
||||
import cn.cordys.common.util.JSON;
|
||||
import cn.cordys.crm.system.dto.field.DepartmentMultipleField;
|
||||
import cn.cordys.crm.system.mapper.ExtDepartmentMapper;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Strings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DepartmentMultipleResolver extends AbstractModuleFieldResolver<DepartmentMultipleField> {
|
||||
|
||||
private static final ExtDepartmentMapper extDepartmentMapper;
|
||||
|
||||
static {
|
||||
extDepartmentMapper = CommonBeanFactory.getBean(ExtDepartmentMapper.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(DepartmentMultipleField departmentField, Object value) {
|
||||
validateRequired(departmentField, value);
|
||||
validateArray(departmentField.getName(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToString(DepartmentMultipleField departmentField, Object value) {
|
||||
return getJsonString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToValue(DepartmentMultipleField departmentField, String value) {
|
||||
return parse2Array(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object transformToValue(DepartmentMultipleField departmentMultipleField, String value) {
|
||||
if (StringUtils.isBlank(value) || Strings.CS.equals(value, "[]")) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> ids = JSON.parseArray(value, String.class);
|
||||
|
||||
List<String> names = Objects.requireNonNull(extDepartmentMapper).getNameByIds(ids);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(names)) {
|
||||
return String.join(",", JSON.parseArray(JSON.toJSONString(names), String.class));
|
||||
}
|
||||
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object textToValue(DepartmentMultipleField field, String text) {
|
||||
if (StringUtils.isBlank(text) || Strings.CS.equals(text, "[]")) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
List<String> names = parseFakeJsonArray(text);
|
||||
List<String> ids = Objects.requireNonNull(extDepartmentMapper).getIdsByNames(names);
|
||||
if (CollectionUtils.isNotEmpty(ids)) {
|
||||
return ids;
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user