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

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

38
backend/.gitignore vendored Normal file
View 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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

34
backend/crm/pom.xml Normal file
View 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>

View File

@@ -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);
}
}
/**
* 字段 keyfield.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);
}
}

View File

@@ -0,0 +1,21 @@
package cn.cordys.common.constants;
/**
* 客户,线索,商机等业务数据的搜索类型
*
* @author jianxing
*/
public enum BusinessSearchType {
/**
* 全部数据
*/
ALL,
/**
* 负责人是我的数据
*/
SELF,
/**
* 有数据权限的部门的数据
*/
DEPARTMENT
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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";
}

View File

@@ -0,0 +1,13 @@
package cn.cordys.common.constants;
public enum HttpMethodConstants {
GET,
HEAD,
POST,
PUT,
PATCH,
DELETE,
OPTIONS,
TRACE,
CONNECT
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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,
}

View File

@@ -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;
}
}

View File

@@ -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 ------*/
}

View File

@@ -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
}

View File

@@ -0,0 +1,13 @@
package cn.cordys.common.constants;
public class RuleValidatorConstants {
/**
* 必填
*/
public static final String REQUIRED = "required";
/**
* 唯一
*/
public static final String UNIQUE = "unique";
}

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -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() {
// 私有构造函数,防止实例化
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.common.context;
@FunctionalInterface
public interface CustomFunction<T, R> {
R apply(T t) throws InterruptedException;
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.common.context;
@FunctionalInterface
public interface ExportTaskFunction {
void apply() throws Exception;
}

View File

@@ -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();
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}

View File

@@ -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;
};
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<>();
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.dto;
import lombok.Data;
@Data
public class RedisMessage {
/**
* redis 发布订阅消息主体
*/
private String message;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<>();
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
// 本拦截器不需要设置属性
}
}

View File

@@ -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);
}

View File

@@ -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}) &gt; #{condition.value}
</when>
<when test="${condition}.operator == 'COUNT_LT'">
<choose>
<when test="${condition}.value == 0">
false
</when>
<otherwise>
(JSON_LENGTH(${column}) &lt; #{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} &gt; #{condition.value}
</when>
<when test="${condition}.operator == 'LT'">
${column} is not null and ${column} &lt; #{condition.value}
</when>
<when test="${condition}.operator == 'GE'">
${column} is not null and ${column} &gt;= #{condition.value}
</when>
<when test="${condition}.operator == 'LE'">
${column} is not null and ${column} &lt;= #{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>

View File

@@ -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 "";
}

View File

@@ -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 "";
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,20 @@
package cn.cordys.common.permission;
import lombok.Data;
/**
* 资源访问上下文,包含权限校验所需的资源信息
*/
@Data
public class ResourceAccessContext {
/**
* 资源负责人ID
*/
private String ownerId;
/**
* 审批状态
*/
private String approvalStatus;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.redis;
/**
* redis 各种topic消费者的公共接口
*/
public interface TopicConsumer {
String getChannel();
void consume(String message);
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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